Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,262 @@
# Javascript template for HTMLWriter
JS_INCLUDE = """
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<script language="javascript">
function isInternetExplorer() {
ua = navigator.userAgent;
/* MSIE used to detect old browsers and Trident used to newer ones*/
return ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
}
/* Define the Animation class */
function Animation(frames, img_id, slider_id, interval, loop_select_id){
this.img_id = img_id;
this.slider_id = slider_id;
this.loop_select_id = loop_select_id;
this.interval = interval;
this.current_frame = 0;
this.direction = 0;
this.timer = null;
this.frames = new Array(frames.length);
for (var i=0; i<frames.length; i++)
{
this.frames[i] = new Image();
this.frames[i].src = frames[i];
}
var slider = document.getElementById(this.slider_id);
slider.max = this.frames.length - 1;
if (isInternetExplorer()) {
// switch from oninput to onchange because IE <= 11 does not conform
// with W3C specification. It ignores oninput and onchange behaves
// like oninput. In contrast, Mircosoft Edge behaves correctly.
slider.setAttribute('onchange', slider.getAttribute('oninput'));
slider.setAttribute('oninput', null);
}
this.set_frame(this.current_frame);
}
Animation.prototype.get_loop_state = function(){
var button_group = document[this.loop_select_id].state;
for (var i = 0; i < button_group.length; i++) {
var button = button_group[i];
if (button.checked) {
return button.value;
}
}
return undefined;
}
Animation.prototype.set_frame = function(frame){
this.current_frame = frame;
document.getElementById(this.img_id).src =
this.frames[this.current_frame].src;
document.getElementById(this.slider_id).value = this.current_frame;
}
Animation.prototype.next_frame = function()
{
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
}
Animation.prototype.previous_frame = function()
{
this.set_frame(Math.max(0, this.current_frame - 1));
}
Animation.prototype.first_frame = function()
{
this.set_frame(0);
}
Animation.prototype.last_frame = function()
{
this.set_frame(this.frames.length - 1);
}
Animation.prototype.slower = function()
{
this.interval /= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.faster = function()
{
this.interval *= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.anim_step_forward = function()
{
this.current_frame += 1;
if(this.current_frame < this.frames.length){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.first_frame();
}else if(loop_state == "reflect"){
this.last_frame();
this.reverse_animation();
}else{
this.pause_animation();
this.last_frame();
}
}
}
Animation.prototype.anim_step_reverse = function()
{
this.current_frame -= 1;
if(this.current_frame >= 0){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.last_frame();
}else if(loop_state == "reflect"){
this.first_frame();
this.play_animation();
}else{
this.pause_animation();
this.first_frame();
}
}
}
Animation.prototype.pause_animation = function()
{
this.direction = 0;
if (this.timer){
clearInterval(this.timer);
this.timer = null;
}
}
Animation.prototype.play_animation = function()
{
this.pause_animation();
this.direction = 1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_forward();
}, this.interval);
}
Animation.prototype.reverse_animation = function()
{
this.pause_animation();
this.direction = -1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_reverse();
}, this.interval);
}
</script>
"""
# Style definitions for the HTML template
STYLE_INCLUDE = """
<style>
.animation {
display: inline-block;
text-align: center;
}
input[type=range].anim-slider {
width: 374px;
margin-left: auto;
margin-right: auto;
}
.anim-buttons {
margin: 8px 0px;
}
.anim-buttons button {
padding: 0;
width: 36px;
}
.anim-state label {
margin-right: 8px;
}
.anim-state input {
margin: 0;
vertical-align: middle;
}
</style>
"""
# HTML template for HTMLWriter
DISPLAY_TEMPLATE = """
<div class="animation">
<img id="_anim_img{id}">
<div class="anim-controls">
<input id="_anim_slider{id}" type="range" class="anim-slider"
name="points" min="0" max="1" step="1" value="0"
oninput="anim{id}.set_frame(parseInt(this.value));"></input>
<div class="anim-buttons">
<button title="Decrease speed" onclick="anim{id}.slower()">
<i class="fa fa-minus"></i></button>
<button title="First frame" onclick="anim{id}.first_frame()">
<i class="fa fa-fast-backward"></i></button>
<button title="Previous frame" onclick="anim{id}.previous_frame()">
<i class="fa fa-step-backward"></i></button>
<button title="Play backwards" onclick="anim{id}.reverse_animation()">
<i class="fa fa-play fa-flip-horizontal"></i></button>
<button title="Pause" onclick="anim{id}.pause_animation()">
<i class="fa fa-pause"></i></button>
<button title="Play" onclick="anim{id}.play_animation()">
<i class="fa fa-play"></i></button>
<button title="Next frame" onclick="anim{id}.next_frame()">
<i class="fa fa-step-forward"></i></button>
<button title="Last frame" onclick="anim{id}.last_frame()">
<i class="fa fa-fast-forward"></i></button>
<button title="Increase speed" onclick="anim{id}.faster()">
<i class="fa fa-plus"></i></button>
</div>
<form title="Repetition mode" action="#n" name="_anim_loop_select{id}"
class="anim-state">
<input type="radio" name="state" value="once" id="_anim_radio1_{id}"
{once_checked}>
<label for="_anim_radio1_{id}">Once</label>
<input type="radio" name="state" value="loop" id="_anim_radio2_{id}"
{loop_checked}>
<label for="_anim_radio2_{id}">Loop</label>
<input type="radio" name="state" value="reflect" id="_anim_radio3_{id}"
{reflect_checked}>
<label for="_anim_radio3_{id}">Reflect</label>
</form>
</div>
</div>
<script language="javascript">
/* Instantiate the Animation class. */
/* The IDs given should match those used in the template above. */
(function() {{
var img_id = "_anim_img{id}";
var slider_id = "_anim_slider{id}";
var loop_select_id = "_anim_loop_select{id}";
var frames = new Array({Nframes});
{fill_frames}
/* set a timeout to make sure all the above elements are created before
the object is initialized. */
setTimeout(function() {{
anim{id} = new Animation(frames, img_id, slider_id, {interval},
loop_select_id);
}}, 0);
}})()
</script>
"""
INCLUDED_FRAMES = """
for (var i=0; i<{Nframes}; i++){{
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
".{frame_format}";
}}
"""

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,662 @@
"""
Adjust subplot layouts so that there are no overlapping axes or axes
decorations. All axes decorations are dealt with (labels, ticks, titles,
ticklabels) and some dependent artists are also dealt with (colorbar, suptitle,
legend).
Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec,
so it is possible to have overlapping axes if the gridspecs overlap (i.e.
using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using
``figure.subplots()`` or ``figure.add_subplots()`` will participate in the
layout. Axes manually placed via ``figure.add_axes()`` will not.
See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide`
"""
# Development Notes:
# What gets a layoutbox:
# - figure
# - gridspec
# - subplotspec
# EITHER:
# - axes + pos for the axes (i.e. the total area taken by axis and
# the actual "position" argument that needs to be sent to
# ax.set_position.)
# - The axes layout box will also encompass the legend, and that is
# how legends get included (axes legends, not figure legends)
# - colorbars are siblings of the axes if they are single-axes
# colorbars
# OR:
# - a gridspec can be inside a subplotspec.
# - subplotspec
# EITHER:
# - axes...
# OR:
# - gridspec... with arbitrary nesting...
# - colorbars are siblings of the subplotspecs if they are multi-axes
# colorbars.
# - suptitle:
# - right now suptitles are just stacked atop everything else in figure.
# Could imagine suptitles being gridspec suptitles, but not implemented
#
# Todo: AnchoredOffsetbox connected to gridspecs or axes. This would
# be more general way to add extra-axes annotations.
import logging
import numpy as np
import matplotlib.cbook as cbook
import matplotlib._layoutbox as layoutbox
_log = logging.getLogger(__name__)
def _spans_overlap(span0, span1):
return span0.start in span1 or span1.start in span0
def _axes_all_finite_sized(fig):
"""Return whether all axes in the figure have a finite width and height."""
for ax in fig.axes:
if ax._layoutbox is not None:
newpos = ax._poslayoutbox.get_rect()
if newpos[2] <= 0 or newpos[3] <= 0:
return False
return True
######################################################
def do_constrained_layout(fig, renderer, h_pad, w_pad,
hspace=None, wspace=None):
"""
Do the constrained_layout. Called at draw time in
``figure.constrained_layout()``
Parameters
----------
fig : Figure
is the ``figure`` instance to do the layout in.
renderer : Renderer
the renderer to use.
h_pad, w_pad : float
are in figure-normalized units, and are a padding around the axes
elements.
hspace, wspace : float
are in fractions of the subplot sizes.
"""
# Steps:
#
# 1. get a list of unique gridspecs in this figure. Each gridspec will be
# constrained separately.
# 2. Check for gaps in the gridspecs. i.e. if not every axes slot in the
# gridspec has been filled. If empty, add a ghost axis that is made so
# that it cannot be seen (though visible=True). This is needed to make
# a blank spot in the layout.
# 3. Compare the tight_bbox of each axes to its `position`, and assume that
# the difference is the space needed by the elements around the edge of
# the axes (decorations) like the title, ticklabels, x-labels, etc. This
# can include legends who overspill the axes boundaries.
# 4. Constrain gridspec elements to line up:
# a) if colnum0 != colnumC, the two subplotspecs are stacked next to
# each other, with the appropriate order.
# b) if colnum0 == colnumC, line up the left or right side of the
# _poslayoutbox (depending if it is the min or max num that is equal).
# c) do the same for rows...
# 5. The above doesn't constrain relative sizes of the _poslayoutboxes
# at all, and indeed zero-size is a solution that the solver often finds
# more convenient than expanding the sizes. Right now the solution is to
# compare subplotspec sizes (i.e. drowsC and drows0) and constrain the
# larger _poslayoutbox to be larger than the ratio of the sizes. i.e. if
# drows0 > drowsC, then ax._poslayoutbox > axc._poslayoutbox*drowsC/drows0.
# This works fine *if* the decorations are similar between the axes.
# If the larger subplotspec has much larger axes decorations, then the
# constraint above is incorrect.
#
# We need the greater than in the above, in general, rather than an equals
# sign. Consider the case of the left column having 2 rows, and the right
# column having 1 row. We want the top and bottom of the _poslayoutboxes
# to line up. So that means if there are decorations on the left column
# axes they will be smaller than half as large as the right hand axis.
#
# This can break down if the decoration size for the right hand axis (the
# margins) is very large. There must be a math way to check for this case.
invTransFig = fig.transFigure.inverted().transform_bbox
# list of unique gridspecs that contain child axes:
gss = set()
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
if gs._layoutbox is not None:
gss.add(gs)
if len(gss) == 0:
cbook._warn_external('There are no gridspecs with layoutboxes. '
'Possibly did not call parent GridSpec with the'
' figure= keyword')
if fig._layoutbox.constrained_layout_called < 1:
for gs in gss:
# fill in any empty gridspec slots w/ ghost axes...
_make_ghost_gridspec_slots(fig, gs)
for _ in range(2):
# do the algorithm twice. This has to be done because decorators
# change size after the first re-position (i.e. x/yticklabels get
# larger/smaller). This second reposition tends to be much milder,
# so doing twice makes things work OK.
for ax in fig.axes:
_log.debug(ax._layoutbox)
if ax._layoutbox is not None:
# make margins for each layout box based on the size of
# the decorators.
_make_layout_margins(ax, renderer, h_pad, w_pad)
# do layout for suptitle.
suptitle = fig._suptitle
do_suptitle = (suptitle is not None and
suptitle._layoutbox is not None and
suptitle.get_in_layout())
if do_suptitle:
bbox = invTransFig(
suptitle.get_window_extent(renderer=renderer))
height = bbox.height
if np.isfinite(height):
# reserve at top of figure include an h_pad above and below
suptitle._layoutbox.edit_height(height + h_pad * 2)
# OK, the above lines up ax._poslayoutbox with ax._layoutbox
# now we need to
# 1) arrange the subplotspecs. We do it at this level because
# the subplotspecs are meant to contain other dependent axes
# like colorbars or legends.
# 2) line up the right and left side of the ax._poslayoutbox
# that have the same subplotspec maxes.
if fig._layoutbox.constrained_layout_called < 1:
# arrange the subplotspecs... This is all done relative to each
# other. Some subplotspecs contain axes, and others contain
# gridspecs the ones that contain gridspecs are a set proportion
# of their parent gridspec. The ones that contain axes are
# not so constrained.
figlb = fig._layoutbox
for child in figlb.children:
if child._is_gridspec_layoutbox():
# This routine makes all the subplot spec containers
# have the correct arrangement. It just stacks the
# subplot layoutboxes in the correct order...
_arrange_subplotspecs(child, hspace=hspace, wspace=wspace)
for gs in gss:
_align_spines(fig, gs)
fig._layoutbox.constrained_layout_called += 1
fig._layoutbox.update_variables()
# check if any axes collapsed to zero. If not, don't change positions:
if _axes_all_finite_sized(fig):
# Now set the position of the axes...
for ax in fig.axes:
if ax._layoutbox is not None:
newpos = ax._poslayoutbox.get_rect()
# Now set the new position.
# ax.set_position will zero out the layout for
# this axis, allowing users to hard-code the position,
# so this does the same w/o zeroing layout.
ax._set_position(newpos, which='original')
if do_suptitle:
newpos = suptitle._layoutbox.get_rect()
suptitle.set_y(1.0 - h_pad)
else:
if suptitle is not None and suptitle._layoutbox is not None:
suptitle._layoutbox.edit_height(0)
else:
cbook._warn_external('constrained_layout not applied. At least '
'one axes collapsed to zero width or height.')
def _make_ghost_gridspec_slots(fig, gs):
"""
Check for unoccupied gridspec slots and make ghost axes for these
slots... Do for each gs separately. This is a pretty big kludge
but shouldn't have too much ill effect. The worst is that
someone querying the figure will wonder why there are more
axes than they thought.
"""
nrows, ncols = gs.get_geometry()
hassubplotspec = np.zeros(nrows * ncols, dtype=bool)
axs = []
for ax in fig.axes:
if (hasattr(ax, 'get_subplotspec')
and ax._layoutbox is not None
and ax.get_subplotspec().get_gridspec() == gs):
axs += [ax]
for ax in axs:
ss0 = ax.get_subplotspec()
hassubplotspec[ss0.num1:(ss0.num2 + 1)] = True
for nn, hss in enumerate(hassubplotspec):
if not hss:
# this gridspec slot doesn't have an axis so we
# make a "ghost".
ax = fig.add_subplot(gs[nn])
ax.set_visible(False)
def _make_layout_margins(ax, renderer, h_pad, w_pad):
"""
For each axes, make a margin between the *pos* layoutbox and the
*axes* layoutbox be a minimum size that can accommodate the
decorations on the axis.
"""
fig = ax.figure
invTransFig = fig.transFigure.inverted().transform_bbox
pos = ax.get_position(original=True)
try:
tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True)
except TypeError:
tightbbox = ax.get_tightbbox(renderer=renderer)
if tightbbox is None:
bbox = pos
else:
bbox = invTransFig(tightbbox)
# this can go wrong:
if not (np.isfinite(bbox.width) and np.isfinite(bbox.height)):
# just abort, this is likely a bad set of coordinates that
# is transitory...
return
# use stored h_pad if it exists
h_padt = ax._poslayoutbox.h_pad
if h_padt is None:
h_padt = h_pad
w_padt = ax._poslayoutbox.w_pad
if w_padt is None:
w_padt = w_pad
ax._poslayoutbox.edit_left_margin_min(-bbox.x0 + pos.x0 + w_padt)
ax._poslayoutbox.edit_right_margin_min(bbox.x1 - pos.x1 + w_padt)
ax._poslayoutbox.edit_bottom_margin_min(-bbox.y0 + pos.y0 + h_padt)
ax._poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt)
_log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad))
_log.debug('right %f', (bbox.x1 - pos.x1 + w_pad))
_log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt))
_log.debug('bbox.y0 %f', bbox.y0)
_log.debug('pos.y0 %f', pos.y0)
# Sometimes its possible for the solver to collapse
# rather than expand axes, so they all have zero height
# or width. This stops that... It *should* have been
# taken into account w/ pref_width...
if fig._layoutbox.constrained_layout_called < 1:
ax._poslayoutbox.constrain_height_min(20, strength='weak')
ax._poslayoutbox.constrain_width_min(20, strength='weak')
ax._layoutbox.constrain_height_min(20, strength='weak')
ax._layoutbox.constrain_width_min(20, strength='weak')
ax._poslayoutbox.constrain_top_margin(0, strength='weak')
ax._poslayoutbox.constrain_bottom_margin(0, strength='weak')
ax._poslayoutbox.constrain_right_margin(0, strength='weak')
ax._poslayoutbox.constrain_left_margin(0, strength='weak')
def _align_spines(fig, gs):
"""
- Align right/left and bottom/top spines of appropriate subplots.
- Compare size of subplotspec including height and width ratios
and make sure that the axes spines are at least as large
as they should be.
"""
# for each gridspec...
nrows, ncols = gs.get_geometry()
width_ratios = gs.get_width_ratios()
height_ratios = gs.get_height_ratios()
if width_ratios is None:
width_ratios = np.ones(ncols)
if height_ratios is None:
height_ratios = np.ones(nrows)
# get axes in this gridspec....
axs = [ax for ax in fig.axes
if (hasattr(ax, 'get_subplotspec')
and ax._layoutbox is not None
and ax.get_subplotspec().get_gridspec() == gs)]
rowspans = []
colspans = []
heights = []
widths = []
for ax in axs:
ss0 = ax.get_subplotspec()
rowspan = ss0.rowspan
colspan = ss0.colspan
rowspans.append(rowspan)
colspans.append(colspan)
heights.append(sum(height_ratios[rowspan.start:rowspan.stop]))
widths.append(sum(width_ratios[colspan.start:colspan.stop]))
for idx0, ax0 in enumerate(axs):
# Compare ax to all other axs: If the subplotspecs start (/stop) at
# the same column, then line up their left (/right) sides; likewise
# for rows/top/bottom.
rowspan0 = rowspans[idx0]
colspan0 = colspans[idx0]
height0 = heights[idx0]
width0 = widths[idx0]
alignleft = False
alignright = False
alignbot = False
aligntop = False
alignheight = False
alignwidth = False
for idx1 in range(idx0 + 1, len(axs)):
ax1 = axs[idx1]
rowspan1 = rowspans[idx1]
colspan1 = colspans[idx1]
width1 = widths[idx1]
height1 = heights[idx1]
# Horizontally align axes spines if they have the same min or max:
if not alignleft and colspan0.start == colspan1.start:
_log.debug('same start columns; line up layoutbox lefts')
layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox],
'left')
alignleft = True
if not alignright and colspan0.stop == colspan1.stop:
_log.debug('same stop columns; line up layoutbox rights')
layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox],
'right')
alignright = True
# Vertically align axes spines if they have the same min or max:
if not aligntop and rowspan0.start == rowspan1.start:
_log.debug('same start rows; line up layoutbox tops')
layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox],
'top')
aligntop = True
if not alignbot and rowspan0.stop == rowspan1.stop:
_log.debug('same stop rows; line up layoutbox bottoms')
layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox],
'bottom')
alignbot = True
# Now we make the widths and heights of position boxes
# similar. (i.e the spine locations)
# This allows vertically stacked subplots to have different sizes
# if they occupy different amounts of the gridspec, e.g. if
# gs = gridspec.GridSpec(3, 1)
# ax0 = gs[0, :]
# ax1 = gs[1:, :]
# then len(rowspan0) = 1, and len(rowspan1) = 2,
# and ax1 should be at least twice as large as ax0.
# But it can be more than twice as large because
# it needs less room for the labeling.
# For heights, do it if the subplots share a column.
if not alignheight and len(rowspan0) == len(rowspan1):
ax0._poslayoutbox.constrain_height(
ax1._poslayoutbox.height * height0 / height1)
alignheight = True
elif _spans_overlap(colspan0, colspan1):
if height0 > height1:
ax0._poslayoutbox.constrain_height_min(
ax1._poslayoutbox.height * height0 / height1)
elif height0 < height1:
ax1._poslayoutbox.constrain_height_min(
ax0._poslayoutbox.height * height1 / height0)
# For widths, do it if the subplots share a row.
if not alignwidth and len(colspan0) == len(colspan1):
ax0._poslayoutbox.constrain_width(
ax1._poslayoutbox.width * width0 / width1)
alignwidth = True
elif _spans_overlap(rowspan0, rowspan1):
if width0 > width1:
ax0._poslayoutbox.constrain_width_min(
ax1._poslayoutbox.width * width0 / width1)
elif width0 < width1:
ax1._poslayoutbox.constrain_width_min(
ax0._poslayoutbox.width * width1 / width0)
def _arrange_subplotspecs(gs, hspace=0, wspace=0):
"""Recursively arrange the subplotspec children of the given gridspec."""
sschildren = []
for child in gs.children:
if child._is_subplotspec_layoutbox():
for child2 in child.children:
# check for gridspec children...
if child2._is_gridspec_layoutbox():
_arrange_subplotspecs(child2, hspace=hspace, wspace=wspace)
sschildren += [child]
# now arrange the subplots...
for child0 in sschildren:
ss0 = child0.artist
nrows, ncols = ss0.get_gridspec().get_geometry()
rowspan0 = ss0.rowspan
colspan0 = ss0.colspan
sschildren = sschildren[1:]
for child1 in sschildren:
ss1 = child1.artist
rowspan1 = ss1.rowspan
colspan1 = ss1.colspan
# OK, this tells us the relative layout of child0 with child1.
pad = wspace / ncols
if colspan0.stop <= colspan1.start:
layoutbox.hstack([ss0._layoutbox, ss1._layoutbox], padding=pad)
if colspan1.stop <= colspan0.start:
layoutbox.hstack([ss1._layoutbox, ss0._layoutbox], padding=pad)
# vertical alignment
pad = hspace / nrows
if rowspan0.stop <= rowspan1.start:
layoutbox.vstack([ss0._layoutbox, ss1._layoutbox], padding=pad)
if rowspan1.stop <= rowspan0.start:
layoutbox.vstack([ss1._layoutbox, ss0._layoutbox], padding=pad)
def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05):
"""
Do the layout for a colorbar, to not overly pollute colorbar.py
*pad* is in fraction of the original axis size.
"""
axlb = ax._layoutbox
axpos = ax._poslayoutbox
axsslb = ax.get_subplotspec()._layoutbox
lb = layoutbox.LayoutBox(
parent=axsslb,
name=axsslb.name + '.cbar',
artist=cax)
if location in ('left', 'right'):
lbpos = layoutbox.LayoutBox(
parent=lb,
name=lb.name + '.pos',
tightwidth=False,
pos=True,
subplot=False,
artist=cax)
if location == 'right':
# arrange to right of parent axis
layoutbox.hstack([axlb, lb], padding=pad * axlb.width,
strength='strong')
else:
layoutbox.hstack([lb, axlb], padding=pad * axlb.width)
# constrain the height and center...
layoutbox.match_heights([axpos, lbpos], [1, shrink])
layoutbox.align([axpos, lbpos], 'v_center')
# set the width of the pos box
lbpos.constrain_width(shrink * axpos.height * (1/aspect),
strength='strong')
elif location in ('bottom', 'top'):
lbpos = layoutbox.LayoutBox(
parent=lb,
name=lb.name + '.pos',
tightheight=True,
pos=True,
subplot=False,
artist=cax)
if location == 'bottom':
layoutbox.vstack([axlb, lb], padding=pad * axlb.height)
else:
layoutbox.vstack([lb, axlb], padding=pad * axlb.height)
# constrain the height and center...
layoutbox.match_widths([axpos, lbpos],
[1, shrink], strength='strong')
layoutbox.align([axpos, lbpos], 'h_center')
# set the height of the pos box
lbpos.constrain_height(axpos.width * aspect * shrink,
strength='medium')
return lb, lbpos
def _getmaxminrowcolumn(axs):
"""
Find axes covering the first and last rows and columns of a list of axes.
"""
startrow = startcol = np.inf
stoprow = stopcol = -np.inf
startax_row = startax_col = stopax_row = stopax_col = None
for ax in axs:
subspec = ax.get_subplotspec()
if subspec.rowspan.start < startrow:
startrow = subspec.rowspan.start
startax_row = ax
if subspec.rowspan.stop > stoprow:
stoprow = subspec.rowspan.stop
stopax_row = ax
if subspec.colspan.start < startcol:
startcol = subspec.colspan.start
startax_col = ax
if subspec.colspan.stop > stopcol:
stopcol = subspec.colspan.stop
stopax_col = ax
return (startrow, stoprow - 1, startax_row, stopax_row,
startcol, stopcol - 1, startax_col, stopax_col)
def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05):
"""
Do the layout for a colorbar, to not overly pollute colorbar.py
*pad* is in fraction of the original axis size.
"""
gs = parents[0].get_subplotspec().get_gridspec()
# parent layout box....
gslb = gs._layoutbox
lb = layoutbox.LayoutBox(parent=gslb.parent,
name=gslb.parent.name + '.cbar',
artist=cax)
# figure out the row and column extent of the parents.
(minrow, maxrow, minax_row, maxax_row,
mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents)
if location in ('left', 'right'):
lbpos = layoutbox.LayoutBox(
parent=lb,
name=lb.name + '.pos',
tightwidth=False,
pos=True,
subplot=False,
artist=cax)
for ax in parents:
if location == 'right':
order = [ax._layoutbox, lb]
else:
order = [lb, ax._layoutbox]
layoutbox.hstack(order, padding=pad * gslb.width,
strength='strong')
# constrain the height and center...
# This isn't quite right. We'd like the colorbar
# pos to line up w/ the axes poss, not the size of the
# gs.
# Horizontal Layout: need to check all the axes in this gridspec
for ch in gslb.children:
subspec = ch.artist
if location == 'right':
if subspec.colspan.stop - 1 <= maxcol:
order = [subspec._layoutbox, lb]
# arrange to right of the parents
elif subspec.colspan.start > maxcol:
order = [lb, subspec._layoutbox]
elif location == 'left':
if subspec.colspan.start >= mincol:
order = [lb, subspec._layoutbox]
elif subspec.colspan.stop - 1 < mincol:
order = [subspec._layoutbox, lb]
layoutbox.hstack(order, padding=pad * gslb.width,
strength='strong')
# Vertical layout:
maxposlb = minax_row._poslayoutbox
minposlb = maxax_row._poslayoutbox
# now we want the height of the colorbar pos to be
# set by the top and bottom of the min/max axes...
# bottom top
# b t
# h = (top-bottom)*shrink
# b = bottom + (top-bottom - h) / 2.
lbpos.constrain_height(
(maxposlb.top - minposlb.bottom) *
shrink, strength='strong')
lbpos.constrain_bottom(
(maxposlb.top - minposlb.bottom) *
(1 - shrink)/2 + minposlb.bottom,
strength='strong')
# set the width of the pos box
lbpos.constrain_width(lbpos.height * (shrink / aspect),
strength='strong')
elif location in ('bottom', 'top'):
lbpos = layoutbox.LayoutBox(
parent=lb,
name=lb.name + '.pos',
tightheight=True,
pos=True,
subplot=False,
artist=cax)
for ax in parents:
if location == 'bottom':
order = [ax._layoutbox, lb]
else:
order = [lb, ax._layoutbox]
layoutbox.vstack(order, padding=pad * gslb.width,
strength='strong')
# Vertical Layout: need to check all the axes in this gridspec
for ch in gslb.children:
subspec = ch.artist
if location == 'bottom':
if subspec.rowspan.stop - 1 <= minrow:
order = [subspec._layoutbox, lb]
elif subspec.rowspan.start > maxrow:
order = [lb, subspec._layoutbox]
elif location == 'top':
if subspec.rowspan.stop - 1 < minrow:
order = [subspec._layoutbox, lb]
elif subspec.rowspan.start >= maxrow:
order = [lb, subspec._layoutbox]
layoutbox.vstack(order, padding=pad * gslb.width,
strength='strong')
# Do horizontal layout...
maxposlb = maxax_col._poslayoutbox
minposlb = minax_col._poslayoutbox
lbpos.constrain_width((maxposlb.right - minposlb.left) *
shrink)
lbpos.constrain_left(
(maxposlb.right - minposlb.left) *
(1-shrink)/2 + minposlb.left)
# set the height of the pos box
lbpos.constrain_height(lbpos.width * shrink * aspect,
strength='medium')
return lb, lbpos

View file

@ -0,0 +1,64 @@
"""
Internal debugging utilities, that are not expected to be used in the rest of
the codebase.
WARNING: Code in this module may change without prior notice!
"""
from io import StringIO
from pathlib import Path
import subprocess
from matplotlib.transforms import TransformNode
def graphviz_dump_transform(transform, dest, *, highlight=None):
"""
Generate a graphical representation of the transform tree for *transform*
using the :program:`dot` program (which this function depends on). The
output format (png, dot, etc.) is determined from the suffix of *dest*.
Parameters
----------
transform : `~matplotlib.transform.Transform`
The represented transform.
dest : str
Output filename. The extension must be one of the formats supported
by :program:`dot`, e.g. png, svg, dot, ...
(see https://www.graphviz.org/doc/info/output.html).
highlight : list of `~matplotlib.transform.Transform` or None
The transforms in the tree to be drawn in bold.
If *None*, *transform* is highlighted.
"""
if highlight is None:
highlight = [transform]
seen = set()
def recurse(root, buf):
if id(root) in seen:
return
seen.add(id(root))
props = {}
label = type(root).__name__
if root._invalid:
label = f'[{label}]'
if root in highlight:
props['style'] = 'bold'
props['shape'] = 'box'
props['label'] = '"%s"' % label
props = ' '.join(map('{0[0]}={0[1]}'.format, props.items()))
buf.write(f'{id(root)} [{props}];\n')
for key, val in vars(root).items():
if isinstance(val, TransformNode) and id(root) in val._parents:
buf.write(f'"{id(root)}" -> "{id(val)}" '
f'[label="{key}", fontsize=10];\n')
recurse(val, buf)
buf = StringIO()
buf.write('digraph G {\n')
recurse(transform, buf)
buf.write('}\n')
subprocess.run(
['dot', '-T', Path(dest).suffix[1:], '-o', dest],
input=buf.getvalue().encode('utf-8'), check=True)

View file

@ -0,0 +1,695 @@
"""
Conventions:
"constrain_x" means to constrain the variable with either
another kiwisolver variable, or a float. i.e. `constrain_width(0.2)`
will set a constraint that the width has to be 0.2 and this constraint is
permanent - i.e. it will not be removed if it becomes obsolete.
"edit_x" means to set x to a value (just a float), and that this value can
change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)`
will allow it to change to 0.3 later. Note that these values are still just
"suggestions" in `kiwisolver` parlance, and could be over-ridden by
other constrains.
"""
import itertools
import kiwisolver as kiwi
import logging
import numpy as np
_log = logging.getLogger(__name__)
# renderers can be complicated
def get_renderer(fig):
if fig._cachedRenderer:
renderer = fig._cachedRenderer
else:
canvas = fig.canvas
if canvas and hasattr(canvas, "get_renderer"):
renderer = canvas.get_renderer()
else:
# not sure if this can happen
# seems to with PDF...
_log.info("constrained_layout : falling back to Agg renderer")
from matplotlib.backends.backend_agg import FigureCanvasAgg
canvas = FigureCanvasAgg(fig)
renderer = canvas.get_renderer()
return renderer
class LayoutBox:
"""
Basic rectangle representation using kiwi solver variables
"""
def __init__(self, parent=None, name='', tightwidth=False,
tightheight=False, artist=None,
lower_left=(0, 0), upper_right=(1, 1), pos=False,
subplot=False, h_pad=None, w_pad=None):
Variable = kiwi.Variable
self.parent = parent
self.name = name
sn = self.name + '_'
if parent is None:
self.solver = kiwi.Solver()
self.constrained_layout_called = 0
else:
self.solver = parent.solver
self.constrained_layout_called = None
# parent wants to know about this child!
parent.add_child(self)
# keep track of artist associated w/ this layout. Can be none
self.artist = artist
# keep track if this box is supposed to be a pos that is constrained
# by the parent.
self.pos = pos
# keep track of whether we need to match this subplot up with others.
self.subplot = subplot
self.top = Variable(sn + 'top')
self.bottom = Variable(sn + 'bottom')
self.left = Variable(sn + 'left')
self.right = Variable(sn + 'right')
self.width = Variable(sn + 'width')
self.height = Variable(sn + 'height')
self.h_center = Variable(sn + 'h_center')
self.v_center = Variable(sn + 'v_center')
self.min_width = Variable(sn + 'min_width')
self.min_height = Variable(sn + 'min_height')
self.pref_width = Variable(sn + 'pref_width')
self.pref_height = Variable(sn + 'pref_height')
# margins are only used for axes-position layout boxes. maybe should
# be a separate subclass:
self.left_margin = Variable(sn + 'left_margin')
self.right_margin = Variable(sn + 'right_margin')
self.bottom_margin = Variable(sn + 'bottom_margin')
self.top_margin = Variable(sn + 'top_margin')
# mins
self.left_margin_min = Variable(sn + 'left_margin_min')
self.right_margin_min = Variable(sn + 'right_margin_min')
self.bottom_margin_min = Variable(sn + 'bottom_margin_min')
self.top_margin_min = Variable(sn + 'top_margin_min')
right, top = upper_right
left, bottom = lower_left
self.tightheight = tightheight
self.tightwidth = tightwidth
self.add_constraints()
self.children = []
self.subplotspec = None
if self.pos:
self.constrain_margins()
self.h_pad = h_pad
self.w_pad = w_pad
def constrain_margins(self):
"""
Only do this for pos. This sets a variable distance
margin between the position of the axes and the outer edge of
the axes.
Margins are variable because they change with the figure size.
Margin minimums are set to make room for axes decorations. However,
the margins can be larger if we are mathicng the position size to
other axes.
"""
sol = self.solver
# left
if not sol.hasEditVariable(self.left_margin_min):
sol.addEditVariable(self.left_margin_min, 'strong')
sol.suggestValue(self.left_margin_min, 0.0001)
c = (self.left_margin == self.left - self.parent.left)
self.solver.addConstraint(c | 'required')
c = (self.left_margin >= self.left_margin_min)
self.solver.addConstraint(c | 'strong')
# right
if not sol.hasEditVariable(self.right_margin_min):
sol.addEditVariable(self.right_margin_min, 'strong')
sol.suggestValue(self.right_margin_min, 0.0001)
c = (self.right_margin == self.parent.right - self.right)
self.solver.addConstraint(c | 'required')
c = (self.right_margin >= self.right_margin_min)
self.solver.addConstraint(c | 'required')
# bottom
if not sol.hasEditVariable(self.bottom_margin_min):
sol.addEditVariable(self.bottom_margin_min, 'strong')
sol.suggestValue(self.bottom_margin_min, 0.0001)
c = (self.bottom_margin == self.bottom - self.parent.bottom)
self.solver.addConstraint(c | 'required')
c = (self.bottom_margin >= self.bottom_margin_min)
self.solver.addConstraint(c | 'required')
# top
if not sol.hasEditVariable(self.top_margin_min):
sol.addEditVariable(self.top_margin_min, 'strong')
sol.suggestValue(self.top_margin_min, 0.0001)
c = (self.top_margin == self.parent.top - self.top)
self.solver.addConstraint(c | 'required')
c = (self.top_margin >= self.top_margin_min)
self.solver.addConstraint(c | 'required')
def add_child(self, child):
self.children += [child]
def remove_child(self, child):
try:
self.children.remove(child)
except ValueError:
_log.info("Tried to remove child that doesn't belong to parent")
def add_constraints(self):
sol = self.solver
# never let width and height go negative.
for i in [self.min_width, self.min_height]:
sol.addEditVariable(i, 1e9)
sol.suggestValue(i, 0.0)
# define relation ships between things thing width and right and left
self.hard_constraints()
# self.soft_constraints()
if self.parent:
self.parent_constrain()
# sol.updateVariables()
def parent_constrain(self):
parent = self.parent
hc = [self.left >= parent.left,
self.bottom >= parent.bottom,
self.top <= parent.top,
self.right <= parent.right]
for c in hc:
self.solver.addConstraint(c | 'required')
def hard_constraints(self):
hc = [self.width == self.right - self.left,
self.height == self.top - self.bottom,
self.h_center == (self.left + self.right) * 0.5,
self.v_center == (self.top + self.bottom) * 0.5,
self.width >= self.min_width,
self.height >= self.min_height]
for c in hc:
self.solver.addConstraint(c | 'required')
def soft_constraints(self):
sol = self.solver
if self.tightwidth:
suggest = 0.
else:
suggest = 20.
c = (self.pref_width == suggest)
for i in c:
sol.addConstraint(i | 'required')
if self.tightheight:
suggest = 0.
else:
suggest = 20.
c = (self.pref_height == suggest)
for i in c:
sol.addConstraint(i | 'required')
c = [(self.width >= suggest),
(self.height >= suggest)]
for i in c:
sol.addConstraint(i | 150000)
def set_parent(self, parent):
"""Replace the parent of this with the new parent."""
self.parent = parent
self.parent_constrain()
def constrain_geometry(self, left, bottom, right, top, strength='strong'):
hc = [self.left == left,
self.right == right,
self.bottom == bottom,
self.top == top]
for c in hc:
self.solver.addConstraint(c | strength)
# self.solver.updateVariables()
def constrain_same(self, other, strength='strong'):
"""
Make the layoutbox have same position as other layoutbox
"""
hc = [self.left == other.left,
self.right == other.right,
self.bottom == other.bottom,
self.top == other.top]
for c in hc:
self.solver.addConstraint(c | strength)
def constrain_left_margin(self, margin, strength='strong'):
c = (self.left == self.parent.left + margin)
self.solver.addConstraint(c | strength)
def edit_left_margin_min(self, margin):
self.solver.suggestValue(self.left_margin_min, margin)
def constrain_right_margin(self, margin, strength='strong'):
c = (self.right == self.parent.right - margin)
self.solver.addConstraint(c | strength)
def edit_right_margin_min(self, margin):
self.solver.suggestValue(self.right_margin_min, margin)
def constrain_bottom_margin(self, margin, strength='strong'):
c = (self.bottom == self.parent.bottom + margin)
self.solver.addConstraint(c | strength)
def edit_bottom_margin_min(self, margin):
self.solver.suggestValue(self.bottom_margin_min, margin)
def constrain_top_margin(self, margin, strength='strong'):
c = (self.top == self.parent.top - margin)
self.solver.addConstraint(c | strength)
def edit_top_margin_min(self, margin):
self.solver.suggestValue(self.top_margin_min, margin)
def get_rect(self):
return (self.left.value(), self.bottom.value(),
self.width.value(), self.height.value())
def update_variables(self):
"""
Update *all* the variables that are part of the solver this LayoutBox
is created with.
"""
self.solver.updateVariables()
def edit_height(self, height, strength='strong'):
"""
Set the height of the layout box.
This is done as an editable variable so that the value can change
due to resizing.
"""
sol = self.solver
for i in [self.height]:
if not sol.hasEditVariable(i):
sol.addEditVariable(i, strength)
sol.suggestValue(self.height, height)
def constrain_height(self, height, strength='strong'):
"""
Constrain the height of the layout box. height is
either a float or a layoutbox.height.
"""
c = (self.height == height)
self.solver.addConstraint(c | strength)
def constrain_height_min(self, height, strength='strong'):
c = (self.height >= height)
self.solver.addConstraint(c | strength)
def edit_width(self, width, strength='strong'):
sol = self.solver
for i in [self.width]:
if not sol.hasEditVariable(i):
sol.addEditVariable(i, strength)
sol.suggestValue(self.width, width)
def constrain_width(self, width, strength='strong'):
"""
Constrain the width of the layout box. *width* is
either a float or a layoutbox.width.
"""
c = (self.width == width)
self.solver.addConstraint(c | strength)
def constrain_width_min(self, width, strength='strong'):
c = (self.width >= width)
self.solver.addConstraint(c | strength)
def constrain_left(self, left, strength='strong'):
c = (self.left == left)
self.solver.addConstraint(c | strength)
def constrain_bottom(self, bottom, strength='strong'):
c = (self.bottom == bottom)
self.solver.addConstraint(c | strength)
def constrain_right(self, right, strength='strong'):
c = (self.right == right)
self.solver.addConstraint(c | strength)
def constrain_top(self, top, strength='strong'):
c = (self.top == top)
self.solver.addConstraint(c | strength)
def _is_subplotspec_layoutbox(self):
"""
Helper to check if this layoutbox is the layoutbox of a subplotspec.
"""
name = self.name.split('.')[-1]
return name[:2] == 'ss'
def _is_gridspec_layoutbox(self):
"""
Helper to check if this layoutbox is the layoutbox of a gridspec.
"""
name = self.name.split('.')[-1]
return name[:8] == 'gridspec'
def find_child_subplots(self):
"""
Find children of this layout box that are subplots. We want to line
poss up, and this is an easy way to find them all.
"""
if self.subplot:
subplots = [self]
else:
subplots = []
for child in self.children:
subplots += child.find_child_subplots()
return subplots
def layout_from_subplotspec(self, subspec,
name='', artist=None, pos=False):
"""
Make a layout box from a subplotspec. The layout box is
constrained to be a fraction of the width/height of the parent,
and be a fraction of the parent width/height from the left/bottom
of the parent. Therefore the parent can move around and the
layout for the subplot spec should move with it.
The parent is *usually* the gridspec that made the subplotspec.??
"""
lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos)
gs = subspec.get_gridspec()
nrows, ncols = gs.get_geometry()
parent = self.parent
# OK, now, we want to set the position of this subplotspec
# based on its subplotspec parameters. The new gridspec will inherit
# from gridspec. prob should be new method in gridspec
left = 0.0
right = 1.0
bottom = 0.0
top = 1.0
totWidth = right-left
totHeight = top-bottom
hspace = 0.
wspace = 0.
# calculate accumulated heights of columns
cellH = totHeight / (nrows + hspace * (nrows - 1))
sepH = hspace * cellH
if gs._row_height_ratios is not None:
netHeight = cellH * nrows
tr = sum(gs._row_height_ratios)
cellHeights = [netHeight * r / tr for r in gs._row_height_ratios]
else:
cellHeights = [cellH] * nrows
sepHeights = [0] + ([sepH] * (nrows - 1))
cellHs = np.cumsum(np.column_stack([sepHeights, cellHeights]).flat)
# calculate accumulated widths of rows
cellW = totWidth / (ncols + wspace * (ncols - 1))
sepW = wspace * cellW
if gs._col_width_ratios is not None:
netWidth = cellW * ncols
tr = sum(gs._col_width_ratios)
cellWidths = [netWidth * r / tr for r in gs._col_width_ratios]
else:
cellWidths = [cellW] * ncols
sepWidths = [0] + ([sepW] * (ncols - 1))
cellWs = np.cumsum(np.column_stack([sepWidths, cellWidths]).flat)
figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)]
figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)]
figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)]
figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)]
rowNum1, colNum1 = divmod(subspec.num1, ncols)
rowNum2, colNum2 = divmod(subspec.num2, ncols)
figBottom = min(figBottoms[rowNum1], figBottoms[rowNum2])
figTop = max(figTops[rowNum1], figTops[rowNum2])
figLeft = min(figLefts[colNum1], figLefts[colNum2])
figRight = max(figRights[colNum1], figRights[colNum2])
# These are numbers relative to (0, 0, 1, 1). Need to constrain
# relative to parent.
width = figRight - figLeft
height = figTop - figBottom
parent = self.parent
cs = [self.left == parent.left + parent.width * figLeft,
self.bottom == parent.bottom + parent.height * figBottom,
self.width == parent.width * width,
self.height == parent.height * height]
for c in cs:
self.solver.addConstraint(c | 'required')
return lb
def __repr__(self):
return (f'LayoutBox: {self.name:25s}, '
f'(left: {self.left.value():1.3f}) '
f'(bot: {self.bottom.value():1.3f}) '
f'(right: {self.right.value():1.3f}) '
f'(top: {self.top.value():1.3f})')
# Utility functions that act on layoutboxes...
def hstack(boxes, padding=0, strength='strong'):
"""
Stack LayoutBox instances from left to right.
*padding* is in figure-relative units.
"""
for i in range(1, len(boxes)):
c = (boxes[i-1].right + padding <= boxes[i].left)
boxes[i].solver.addConstraint(c | strength)
def hpack(boxes, padding=0, strength='strong'):
"""Stack LayoutBox instances from left to right."""
for i in range(1, len(boxes)):
c = (boxes[i-1].right + padding == boxes[i].left)
boxes[i].solver.addConstraint(c | strength)
def vstack(boxes, padding=0, strength='strong'):
"""Stack LayoutBox instances from top to bottom."""
for i in range(1, len(boxes)):
c = (boxes[i-1].bottom - padding >= boxes[i].top)
boxes[i].solver.addConstraint(c | strength)
def vpack(boxes, padding=0, strength='strong'):
"""Stack LayoutBox instances from top to bottom."""
for i in range(1, len(boxes)):
c = (boxes[i-1].bottom - padding >= boxes[i].top)
boxes[i].solver.addConstraint(c | strength)
def match_heights(boxes, height_ratios=None, strength='medium'):
"""Stack LayoutBox instances from top to bottom."""
if height_ratios is None:
height_ratios = np.ones(len(boxes))
for i in range(1, len(boxes)):
c = (boxes[i-1].height ==
boxes[i].height*height_ratios[i-1]/height_ratios[i])
boxes[i].solver.addConstraint(c | strength)
def match_widths(boxes, width_ratios=None, strength='medium'):
"""Stack LayoutBox instances from top to bottom."""
if width_ratios is None:
width_ratios = np.ones(len(boxes))
for i in range(1, len(boxes)):
c = (boxes[i-1].width ==
boxes[i].width*width_ratios[i-1]/width_ratios[i])
boxes[i].solver.addConstraint(c | strength)
def vstackeq(boxes, padding=0, height_ratios=None):
vstack(boxes, padding=padding)
match_heights(boxes, height_ratios=height_ratios)
def hstackeq(boxes, padding=0, width_ratios=None):
hstack(boxes, padding=padding)
match_widths(boxes, width_ratios=width_ratios)
def align(boxes, attr, strength='strong'):
cons = []
for box in boxes[1:]:
cons = (getattr(boxes[0], attr) == getattr(box, attr))
boxes[0].solver.addConstraint(cons | strength)
def match_top_margins(boxes, levels=1):
box0 = boxes[0]
top0 = box0
for n in range(levels):
top0 = top0.parent
for box in boxes[1:]:
topb = box
for n in range(levels):
topb = topb.parent
c = (box0.top-top0.top == box.top-topb.top)
box0.solver.addConstraint(c | 'strong')
def match_bottom_margins(boxes, levels=1):
box0 = boxes[0]
top0 = box0
for n in range(levels):
top0 = top0.parent
for box in boxes[1:]:
topb = box
for n in range(levels):
topb = topb.parent
c = (box0.bottom-top0.bottom == box.bottom-topb.bottom)
box0.solver.addConstraint(c | 'strong')
def match_left_margins(boxes, levels=1):
box0 = boxes[0]
top0 = box0
for n in range(levels):
top0 = top0.parent
for box in boxes[1:]:
topb = box
for n in range(levels):
topb = topb.parent
c = (box0.left-top0.left == box.left-topb.left)
box0.solver.addConstraint(c | 'strong')
def match_right_margins(boxes, levels=1):
box0 = boxes[0]
top0 = box0
for n in range(levels):
top0 = top0.parent
for box in boxes[1:]:
topb = box
for n in range(levels):
topb = topb.parent
c = (box0.right-top0.right == box.right-topb.right)
box0.solver.addConstraint(c | 'strong')
def match_width_margins(boxes, levels=1):
match_left_margins(boxes, levels=levels)
match_right_margins(boxes, levels=levels)
def match_height_margins(boxes, levels=1):
match_top_margins(boxes, levels=levels)
match_bottom_margins(boxes, levels=levels)
def match_margins(boxes, levels=1):
match_width_margins(boxes, levels=levels)
match_height_margins(boxes, levels=levels)
_layoutboxobjnum = itertools.count()
def seq_id():
"""Generate a short sequential id for layoutbox objects."""
return '%06d' % next(_layoutboxobjnum)
def print_children(lb):
"""Print the children of the layoutbox."""
print(lb)
for child in lb.children:
print_children(child)
def nonetree(lb):
"""
Make all elements in this tree None, signalling not to do any more layout.
"""
if lb is not None:
if lb.parent is None:
# Clear the solver. Hopefully this garbage collects.
lb.solver.reset()
nonechildren(lb)
else:
nonetree(lb.parent)
def nonechildren(lb):
for child in lb.children:
nonechildren(child)
lb.artist._layoutbox = None
lb = None
def print_tree(lb):
"""Print the tree of layoutboxes."""
if lb.parent is None:
print('LayoutBox Tree\n')
print('==============\n')
print_children(lb)
print('\n')
else:
print_tree(lb.parent)
def plot_children(fig, box, level=0, printit=True):
"""Simple plotting to show where boxes are."""
import matplotlib
import matplotlib.pyplot as plt
if isinstance(fig, matplotlib.figure.Figure):
ax = fig.add_axes([0., 0., 1., 1.])
ax.set_facecolor([1., 1., 1., 0.7])
ax.set_alpha(0.3)
fig.draw(fig.canvas.get_renderer())
else:
ax = fig
import matplotlib.patches as patches
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
if printit:
print("Level:", level)
for child in box.children:
if printit:
print(child)
ax.add_patch(
patches.Rectangle(
(child.left.value(), child.bottom.value()), # (x, y)
child.width.value(), # width
child.height.value(), # height
fc='none',
alpha=0.8,
ec=colors[level]
)
)
if level > 0:
name = child.name.split('.')[-1]
if level % 2 == 0:
ax.text(child.left.value(), child.bottom.value(), name,
size=12-level, color=colors[level])
else:
ax.text(child.right.value(), child.top.value(), name,
ha='right', va='top', size=12-level,
color=colors[level])
plot_children(ax, child, level=level+1, printit=printit)

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,141 @@
"""
Manage figures for the pyplot interface.
"""
import atexit
from collections import OrderedDict
import gc
class Gcf:
"""
Singleton to maintain the relation between figures and their managers, and
keep track of and "active" figure and manager.
The canvas of a figure created through pyplot is associated with a figure
manager, which handles the interaction between the figure and the backend.
pyplot keeps track of figure managers using an identifier, the "figure
number" or "manager number" (which can actually be any hashable value);
this number is available as the :attr:`number` attribute of the manager.
This class is never instantiated; it consists of an `OrderedDict` mapping
figure/manager numbers to managers, and a set of class methods that
manipulate this `OrderedDict`.
Attributes
----------
figs : OrderedDict
`OrderedDict` mapping numbers to managers; the active manager is at the
end.
"""
figs = OrderedDict()
@classmethod
def get_fig_manager(cls, num):
"""
If manager number *num* exists, make it the active one and return it;
otherwise return *None*.
"""
manager = cls.figs.get(num, None)
if manager is not None:
cls.set_active(manager)
return manager
@classmethod
def destroy(cls, num):
"""
Destroy manager *num* -- either a manager instance or a manager number.
In the interactive backends, this is bound to the window "destroy" and
"delete" events.
It is recommended to pass a manager instance, to avoid confusion when
two managers share the same number.
"""
if all(hasattr(num, attr) for attr in ["num", "_cidgcf", "destroy"]):
manager = num
if cls.figs.get(manager.num) is manager:
cls.figs.pop(manager.num)
else:
return
else:
try:
manager = cls.figs.pop(num)
except KeyError:
return
manager.canvas.mpl_disconnect(manager._cidgcf)
manager.destroy()
gc.collect(1)
@classmethod
def destroy_fig(cls, fig):
"""Destroy figure *fig*."""
num = next((manager.num for manager in cls.figs.values()
if manager.canvas.figure == fig), None)
if num is not None:
cls.destroy(num)
@classmethod
def destroy_all(cls):
"""Destroy all figures."""
# Reimport gc in case the module globals have already been removed
# during interpreter shutdown.
import gc
for manager in list(cls.figs.values()):
manager.canvas.mpl_disconnect(manager._cidgcf)
manager.destroy()
cls.figs.clear()
gc.collect(1)
@classmethod
def has_fignum(cls, num):
"""Return whether figure number *num* exists."""
return num in cls.figs
@classmethod
def get_all_fig_managers(cls):
"""Return a list of figure managers."""
return list(cls.figs.values())
@classmethod
def get_num_fig_managers(cls):
"""Return the number of figures being managed."""
return len(cls.figs)
@classmethod
def get_active(cls):
"""Return the active manager, or *None* if there is no manager."""
return next(reversed(cls.figs.values())) if cls.figs else None
@classmethod
def _set_new_active_manager(cls, manager):
"""Adopt *manager* into pyplot and make it the active manager."""
if not hasattr(manager, "_cidgcf"):
manager._cidgcf = manager.canvas.mpl_connect(
"button_press_event", lambda event: cls.set_active(manager))
fig = manager.canvas.figure
fig.number = manager.num
label = fig.get_label()
if label:
manager.set_window_title(label)
cls.set_active(manager)
@classmethod
def set_active(cls, manager):
"""Make *manager* the active manager."""
cls.figs[manager.num] = manager
cls.figs.move_to_end(manager.num)
@classmethod
def draw_all(cls, force=False):
"""
Redraw all stale managed figures, or, if *force* is True, all managed
figures.
"""
for manager in cls.get_all_fig_managers():
if force or manager.canvas.figure.stale:
manager.canvas.draw_idle()
atexit.register(Gcf.destroy_all)

View file

@ -0,0 +1,38 @@
"""
Text layouting utilities.
"""
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
"""
Render *string* with *font*. For each character in *string*, yield a
(glyph-index, x-position) pair. When such a pair is yielded, the font's
glyph is set to the corresponding character.
Parameters
----------
string : str
The string to be rendered.
font : FT2Font
The font.
kern_mode : int
A FreeType kerning mode.
Yields
------
glyph_index : int
x_position : float
"""
x = 0
last_glyph_idx = None
for char in string:
glyph_idx = font.get_char_index(ord(char))
kern = (font.get_kerning(last_glyph_idx, glyph_idx, kern_mode)
if last_glyph_idx is not None else 0) / 64
x += kern
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
yield glyph_idx, x
x += glyph.linearHoriAdvance / 65536
last_glyph_idx = glyph_idx

Binary file not shown.

View file

@ -0,0 +1,21 @@
# This file was generated by 'versioneer.py' (0.15) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
import json
import sys
version_json = '''
{
"dirty": false,
"error": null,
"full-revisionid": "6e4d72c663c9930115720ac469341ed56a9505ec",
"version": "3.3.2"
}
''' # END VERSION_JSON
def get_versions():
return json.loads(version_json)

View file

@ -0,0 +1,528 @@
"""
A python interface to Adobe Font Metrics Files.
Although a number of other python implementations exist, and may be more
complete than this, it was decided not to go with them because they were
either:
1) copyrighted or used a non-BSD compatible license
2) had too many dependencies and a free standing lib was needed
3) did more than needed and it was easier to write afresh rather than
figure out how to get just what was needed.
It is pretty easy to use, and has no external dependencies:
>>> import matplotlib as mpl
>>> from pathlib import Path
>>> afm_path = Path(mpl.get_data_path(), 'fonts', 'afm', 'ptmr8a.afm')
>>>
>>> from matplotlib.afm import AFM
>>> with afm_path.open('rb') as fh:
... afm = AFM(fh)
>>> afm.string_width_height('What the heck?')
(6220.0, 694)
>>> afm.get_fontname()
'Times-Roman'
>>> afm.get_kern_dist('A', 'f')
0
>>> afm.get_kern_dist('A', 'y')
-92.0
>>> afm.get_bbox_char('!')
[130, -9, 238, 676]
As in the Adobe Font Metrics File Format Specification, all dimensions
are given in units of 1/1000 of the scale factor (point size) of the font
being used.
"""
from collections import namedtuple
import logging
import re
from ._mathtext_data import uni2type1
_log = logging.getLogger(__name__)
def _to_int(x):
# Some AFM files have floats where we are expecting ints -- there is
# probably a better way to handle this (support floats, round rather than
# truncate). But I don't know what the best approach is now and this
# change to _to_int should at least prevent Matplotlib from crashing on
# these. JDH (2009-11-06)
return int(float(x))
def _to_float(x):
# Some AFM files use "," instead of "." as decimal separator -- this
# shouldn't be ambiguous (unless someone is wicked enough to use "," as
# thousands separator...).
if isinstance(x, bytes):
# Encoding doesn't really matter -- if we have codepoints >127 the call
# to float() will error anyways.
x = x.decode('latin-1')
return float(x.replace(',', '.'))
def _to_str(x):
return x.decode('utf8')
def _to_list_of_ints(s):
s = s.replace(b',', b' ')
return [_to_int(val) for val in s.split()]
def _to_list_of_floats(s):
return [_to_float(val) for val in s.split()]
def _to_bool(s):
if s.lower().strip() in (b'false', b'0', b'no'):
return False
else:
return True
def _parse_header(fh):
"""
Read the font metrics header (up to the char metrics) and returns
a dictionary mapping *key* to *val*. *val* will be converted to the
appropriate python type as necessary; e.g.:
* 'False'->False
* '0'->0
* '-168 -218 1000 898'-> [-168, -218, 1000, 898]
Dictionary keys are
StartFontMetrics, FontName, FullName, FamilyName, Weight,
ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition,
UnderlineThickness, Version, Notice, EncodingScheme, CapHeight,
XHeight, Ascender, Descender, StartCharMetrics
"""
header_converters = {
b'StartFontMetrics': _to_float,
b'FontName': _to_str,
b'FullName': _to_str,
b'FamilyName': _to_str,
b'Weight': _to_str,
b'ItalicAngle': _to_float,
b'IsFixedPitch': _to_bool,
b'FontBBox': _to_list_of_ints,
b'UnderlinePosition': _to_float,
b'UnderlineThickness': _to_float,
b'Version': _to_str,
# Some AFM files have non-ASCII characters (which are not allowed by
# the spec). Given that there is actually no public API to even access
# this field, just return it as straight bytes.
b'Notice': lambda x: x,
b'EncodingScheme': _to_str,
b'CapHeight': _to_float, # Is the second version a mistake, or
b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS
b'XHeight': _to_float,
b'Ascender': _to_float,
b'Descender': _to_float,
b'StdHW': _to_float,
b'StdVW': _to_float,
b'StartCharMetrics': _to_int,
b'CharacterSet': _to_str,
b'Characters': _to_int,
}
d = {}
first_line = True
for line in fh:
line = line.rstrip()
if line.startswith(b'Comment'):
continue
lst = line.split(b' ', 1)
key = lst[0]
if first_line:
# AFM spec, Section 4: The StartFontMetrics keyword
# [followed by a version number] must be the first line in
# the file, and the EndFontMetrics keyword must be the
# last non-empty line in the file. We just check the
# first header entry.
if key != b'StartFontMetrics':
raise RuntimeError('Not an AFM file')
first_line = False
if len(lst) == 2:
val = lst[1]
else:
val = b''
try:
converter = header_converters[key]
except KeyError:
_log.error('Found an unknown keyword in AFM header (was %r)' % key)
continue
try:
d[key] = converter(val)
except ValueError:
_log.error('Value error parsing header in AFM: %s, %s', key, val)
continue
if key == b'StartCharMetrics':
break
else:
raise RuntimeError('Bad parse')
return d
CharMetrics = namedtuple('CharMetrics', 'width, name, bbox')
CharMetrics.__doc__ = """
Represents the character metrics of a single character.
Notes
-----
The fields do currently only describe a subset of character metrics
information defined in the AFM standard.
"""
CharMetrics.width.__doc__ = """The character width (WX)."""
CharMetrics.name.__doc__ = """The character name (N)."""
CharMetrics.bbox.__doc__ = """
The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
def _parse_char_metrics(fh):
"""
Parse the given filehandle for character metrics information and return
the information as dicts.
It is assumed that the file cursor is on the line behind
'StartCharMetrics'.
Returns
-------
ascii_d : dict
A mapping "ASCII num of the character" to `.CharMetrics`.
name_d : dict
A mapping "character name" to `.CharMetrics`.
Notes
-----
This function is incomplete per the standard, but thus far parses
all the sample afm files tried.
"""
required_keys = {'C', 'WX', 'N', 'B'}
ascii_d = {}
name_d = {}
for line in fh:
# We are defensively letting values be utf8. The spec requires
# ascii, but there are non-compliant fonts in circulation
line = _to_str(line.rstrip()) # Convert from byte-literal
if line.startswith('EndCharMetrics'):
return ascii_d, name_d
# Split the metric line into a dictionary, keyed by metric identifiers
vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s)
# There may be other metrics present, but only these are needed
if not required_keys.issubset(vals):
raise RuntimeError('Bad char metrics line: %s' % line)
num = _to_int(vals['C'])
wx = _to_float(vals['WX'])
name = vals['N']
bbox = _to_list_of_floats(vals['B'])
bbox = list(map(int, bbox))
metrics = CharMetrics(wx, name, bbox)
# Workaround: If the character name is 'Euro', give it the
# corresponding character code, according to WinAnsiEncoding (see PDF
# Reference).
if name == 'Euro':
num = 128
elif name == 'minus':
num = ord("\N{MINUS SIGN}") # 0x2212
if num != -1:
ascii_d[num] = metrics
name_d[name] = metrics
raise RuntimeError('Bad parse')
def _parse_kern_pairs(fh):
"""
Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and
values are the kern pair value. For example, a kern pairs line like
``KPX A y -50``
will be represented as::
d[ ('A', 'y') ] = -50
"""
line = next(fh)
if not line.startswith(b'StartKernPairs'):
raise RuntimeError('Bad start of kern pairs data: %s' % line)
d = {}
for line in fh:
line = line.rstrip()
if not line:
continue
if line.startswith(b'EndKernPairs'):
next(fh) # EndKernData
return d
vals = line.split()
if len(vals) != 4 or vals[0] != b'KPX':
raise RuntimeError('Bad kern pairs line: %s' % line)
c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3])
d[(c1, c2)] = val
raise RuntimeError('Bad kern pairs parse')
CompositePart = namedtuple('CompositePart', 'name, dx, dy')
CompositePart.__doc__ = """
Represents the information on a composite element of a composite char."""
CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'."""
CompositePart.dx.__doc__ = """x-displacement of the part from the origin."""
CompositePart.dy.__doc__ = """y-displacement of the part from the origin."""
def _parse_composites(fh):
"""
Parse the given filehandle for composites information return them as a
dict.
It is assumed that the file cursor is on the line behind 'StartComposites'.
Returns
-------
dict
A dict mapping composite character names to a parts list. The parts
list is a list of `.CompositePart` entries describing the parts of
the composite.
Examples
--------
A composite definition line::
CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ;
will be represented as::
composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0),
CompositePart(name='acute', dx=160, dy=170)]
"""
composites = {}
for line in fh:
line = line.rstrip()
if not line:
continue
if line.startswith(b'EndComposites'):
return composites
vals = line.split(b';')
cc = vals[0].split()
name, numParts = cc[1], _to_int(cc[2])
pccParts = []
for s in vals[1:-1]:
pcc = s.split()
part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3]))
pccParts.append(part)
composites[name] = pccParts
raise RuntimeError('Bad composites parse')
def _parse_optional(fh):
"""
Parse the optional fields for kern pair data and composites.
Returns
-------
kern_data : dict
A dict containing kerning information. May be empty.
See `._parse_kern_pairs`.
composites : dict
A dict containing composite information. May be empty.
See `._parse_composites`.
"""
optional = {
b'StartKernData': _parse_kern_pairs,
b'StartComposites': _parse_composites,
}
d = {b'StartKernData': {},
b'StartComposites': {}}
for line in fh:
line = line.rstrip()
if not line:
continue
key = line.split()[0]
if key in optional:
d[key] = optional[key](fh)
return d[b'StartKernData'], d[b'StartComposites']
class AFM:
def __init__(self, fh):
"""Parse the AFM file in file object *fh*."""
self._header = _parse_header(fh)
self._metrics, self._metrics_by_name = _parse_char_metrics(fh)
self._kern, self._composite = _parse_optional(fh)
def get_bbox_char(self, c, isord=False):
if not isord:
c = ord(c)
return self._metrics[c].bbox
def string_width_height(self, s):
"""
Return the string width (including kerning) and string height
as a (*w*, *h*) tuple.
"""
if not len(s):
return 0, 0
total_width = 0
namelast = None
miny = 1e9
maxy = 0
for c in s:
if c == '\n':
continue
wx, name, bbox = self._metrics[ord(c)]
total_width += wx + self._kern.get((namelast, name), 0)
l, b, w, h = bbox
miny = min(miny, b)
maxy = max(maxy, b + h)
namelast = name
return total_width, maxy - miny
def get_str_bbox_and_descent(self, s):
"""Return the string bounding box and the maximal descent."""
if not len(s):
return 0, 0, 0, 0, 0
total_width = 0
namelast = None
miny = 1e9
maxy = 0
left = 0
if not isinstance(s, str):
s = _to_str(s)
for c in s:
if c == '\n':
continue
name = uni2type1.get(ord(c), f"uni{ord(c):04X}")
try:
wx, _, bbox = self._metrics_by_name[name]
except KeyError:
name = 'question'
wx, _, bbox = self._metrics_by_name[name]
total_width += wx + self._kern.get((namelast, name), 0)
l, b, w, h = bbox
left = min(left, l)
miny = min(miny, b)
maxy = max(maxy, b + h)
namelast = name
return left, miny, total_width, maxy - miny, -miny
def get_str_bbox(self, s):
"""Return the string bounding box."""
return self.get_str_bbox_and_descent(s)[:4]
def get_name_char(self, c, isord=False):
"""Get the name of the character, i.e., ';' is 'semicolon'."""
if not isord:
c = ord(c)
return self._metrics[c].name
def get_width_char(self, c, isord=False):
"""
Get the width of the character from the character metric WX field.
"""
if not isord:
c = ord(c)
return self._metrics[c].width
def get_width_from_char_name(self, name):
"""Get the width of the character from a type1 character name."""
return self._metrics_by_name[name].width
def get_height_char(self, c, isord=False):
"""Get the bounding box (ink) height of character *c* (space is 0)."""
if not isord:
c = ord(c)
return self._metrics[c].bbox[-1]
def get_kern_dist(self, c1, c2):
"""
Return the kerning pair distance (possibly 0) for chars *c1* and *c2*.
"""
name1, name2 = self.get_name_char(c1), self.get_name_char(c2)
return self.get_kern_dist_from_name(name1, name2)
def get_kern_dist_from_name(self, name1, name2):
"""
Return the kerning pair distance (possibly 0) for chars
*name1* and *name2*.
"""
return self._kern.get((name1, name2), 0)
def get_fontname(self):
"""Return the font name, e.g., 'Times-Roman'."""
return self._header[b'FontName']
def get_fullname(self):
"""Return the font full name, e.g., 'Times-Roman'."""
name = self._header.get(b'FullName')
if name is None: # use FontName as a substitute
name = self._header[b'FontName']
return name
def get_familyname(self):
"""Return the font family name, e.g., 'Times'."""
name = self._header.get(b'FamilyName')
if name is not None:
return name
# FamilyName not specified so we'll make a guess
name = self.get_fullname()
extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|'
r'light|ultralight|extra|condensed))+$')
return re.sub(extras, '', name)
@property
def family_name(self):
"""The font family name, e.g., 'Times'."""
return self.get_familyname()
def get_weight(self):
"""Return the font weight, e.g., 'Bold' or 'Roman'."""
return self._header[b'Weight']
def get_angle(self):
"""Return the fontangle as float."""
return self._header[b'ItalicAngle']
def get_capheight(self):
"""Return the cap height as float."""
return self._header[b'CapHeight']
def get_xheight(self):
"""Return the xheight as float."""
return self._header[b'XHeight']
def get_underline_thickness(self):
"""Return the underline thickness as float."""
return self._header[b'UnderlineThickness']
def get_horizontal_stem_width(self):
"""
Return the standard horizontal stem width as float, or *None* if
not specified in AFM file.
"""
return self._header.get(b'StdHW', None)
def get_vertical_stem_width(self):
"""
Return the standard vertical stem width as float, or *None* if
not specified in AFM file.
"""
return self._header.get(b'StdVW', None)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
from ._subplots import *
from ._axes import *

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,386 @@
import numpy as np
import matplotlib.cbook as cbook
import matplotlib.docstring as docstring
import matplotlib.ticker as mticker
import matplotlib.transforms as mtransforms
from matplotlib.axes._base import _AxesBase
def _make_secondary_locator(rect, parent):
"""
Helper function to locate the secondary axes.
A locator gets used in `Axes.set_aspect` to override the default
locations... It is a function that takes an axes object and
a renderer and tells `set_aspect` where it is to be placed.
This locator make the transform be in axes-relative co-coordinates
because that is how we specify the "location" of the secondary axes.
Here *rect* is a rectangle [l, b, w, h] that specifies the
location for the axes in the transform given by *trans* on the
*parent*.
"""
_rect = mtransforms.Bbox.from_bounds(*rect)
def secondary_locator(ax, renderer):
# delay evaluating transform until draw time because the
# parent transform may have changed (i.e. if window reesized)
bb = mtransforms.TransformedBbox(_rect, parent.transAxes)
tr = parent.figure.transFigure.inverted()
bb = mtransforms.TransformedBbox(bb, tr)
return bb
return secondary_locator
class SecondaryAxis(_AxesBase):
"""
General class to hold a Secondary_X/Yaxis.
"""
def __init__(self, parent, orientation, location, functions, **kwargs):
"""
See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
While there is no need for this to be private, it should really be
called by those higher level functions.
"""
self._functions = functions
self._parent = parent
self._orientation = orientation
self._ticks_set = False
if self._orientation == 'x':
super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
self._axis = self.xaxis
self._locstrings = ['top', 'bottom']
self._otherstrings = ['left', 'right']
elif self._orientation == 'y':
super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs)
self._axis = self.yaxis
self._locstrings = ['right', 'left']
self._otherstrings = ['top', 'bottom']
self._parentscale = None
# this gets positioned w/o constrained_layout so exclude:
self._layoutbox = None
self._poslayoutbox = None
self.set_location(location)
self.set_functions(functions)
# styling:
if self._orientation == 'x':
otheraxis = self.yaxis
else:
otheraxis = self.xaxis
otheraxis.set_major_locator(mticker.NullLocator())
otheraxis.set_ticks_position('none')
for st in self._otherstrings:
self.spines[st].set_visible(False)
for st in self._locstrings:
self.spines[st].set_visible(True)
if self._pos < 0.5:
# flip the location strings...
self._locstrings = self._locstrings[::-1]
self.set_alignment(self._locstrings[0])
def set_alignment(self, align):
"""
Set if axes spine and labels are drawn at top or bottom (or left/right)
of the axes.
Parameters
----------
align : str
either 'top' or 'bottom' for orientation='x' or
'left' or 'right' for orientation='y' axis.
"""
cbook._check_in_list(self._locstrings, align=align)
if align == self._locstrings[1]: # Need to change the orientation.
self._locstrings = self._locstrings[::-1]
self.spines[self._locstrings[0]].set_visible(True)
self.spines[self._locstrings[1]].set_visible(False)
self._axis.set_ticks_position(align)
self._axis.set_label_position(align)
def set_location(self, location):
"""
Set the vertical or horizontal location of the axes in
parent-normalized coordinates.
Parameters
----------
location : {'top', 'bottom', 'left', 'right'} or float
The position to put the secondary axis. Strings can be 'top' or
'bottom' for orientation='x' and 'right' or 'left' for
orientation='y'. A float indicates the relative position on the
parent axes to put the new axes, 0.0 being the bottom (or left)
and 1.0 being the top (or right).
"""
# This puts the rectangle into figure-relative coordinates.
if isinstance(location, str):
if location in ['top', 'right']:
self._pos = 1.
elif location in ['bottom', 'left']:
self._pos = 0.
else:
raise ValueError(
f"location must be {self._locstrings[0]!r}, "
f"{self._locstrings[1]!r}, or a float, not {location!r}")
else:
self._pos = location
self._loc = location
if self._orientation == 'x':
bounds = [0, self._pos, 1., 1e-10]
else:
bounds = [self._pos, 0, 1e-10, 1]
secondary_locator = _make_secondary_locator(bounds, self._parent)
# this locator lets the axes move in the parent axes coordinates.
# so it never needs to know where the parent is explicitly in
# figure coordinates.
# it gets called in `ax.apply_aspect() (of all places)
self.set_axes_locator(secondary_locator)
def apply_aspect(self, position=None):
# docstring inherited.
self._set_lims()
super().apply_aspect(position)
@cbook._make_keyword_only("3.2", "minor")
def set_ticks(self, ticks, minor=False):
"""
Set the x ticks with list of *ticks*
Parameters
----------
ticks : list
List of x-axis tick locations.
minor : bool, default: False
If ``False`` sets major ticks, if ``True`` sets minor ticks.
"""
ret = self._axis.set_ticks(ticks, minor=minor)
self.stale = True
self._ticks_set = True
return ret
def set_functions(self, functions):
"""
Set how the secondary axis converts limits from the parent axes.
Parameters
----------
functions : 2-tuple of func, or `Transform` with an inverse.
Transform between the parent axis values and the secondary axis
values.
If supplied as a 2-tuple of functions, the first function is
the forward transform function and the second is the inverse
transform.
If a transform is supplied, then the transform must have an
inverse.
"""
if (isinstance(functions, tuple) and len(functions) == 2 and
callable(functions[0]) and callable(functions[1])):
# make an arbitrary convert from a two-tuple of functions
# forward and inverse.
self._functions = functions
elif functions is None:
self._functions = (lambda x: x, lambda x: x)
else:
raise ValueError('functions argument of secondary axes '
'must be a two-tuple of callable functions '
'with the first function being the transform '
'and the second being the inverse')
self._set_scale()
# Should be changed to draw(self, renderer) once the deprecation of
# renderer=None and of inframe expires.
def draw(self, *args, **kwargs):
"""
Draw the secondary axes.
Consults the parent axes for its limits and converts them
using the converter specified by
`~.axes._secondary_axes.set_functions` (or *functions*
parameter when axes initialized.)
"""
self._set_lims()
# this sets the scale in case the parent has set its scale.
self._set_scale()
super().draw(*args, **kwargs)
def _set_scale(self):
"""
Check if parent has set its scale
"""
if self._orientation == 'x':
pscale = self._parent.xaxis.get_scale()
set_scale = self.set_xscale
if self._orientation == 'y':
pscale = self._parent.yaxis.get_scale()
set_scale = self.set_yscale
if pscale == self._parentscale:
return
if pscale == 'log':
defscale = 'functionlog'
else:
defscale = 'function'
if self._ticks_set:
ticks = self._axis.get_ticklocs()
# need to invert the roles here for the ticks to line up.
set_scale(defscale, functions=self._functions[::-1])
# OK, set_scale sets the locators, but if we've called
# axsecond.set_ticks, we want to keep those.
if self._ticks_set:
self._axis.set_major_locator(mticker.FixedLocator(ticks))
# If the parent scale doesn't change, we can skip this next time.
self._parentscale = pscale
def _set_lims(self):
"""
Set the limits based on parent limits and the convert method
between the parent and this secondary axes.
"""
if self._orientation == 'x':
lims = self._parent.get_xlim()
set_lim = self.set_xlim
if self._orientation == 'y':
lims = self._parent.get_ylim()
set_lim = self.set_ylim
order = lims[0] < lims[1]
lims = self._functions[0](np.array(lims))
neworder = lims[0] < lims[1]
if neworder != order:
# Flip because the transform will take care of the flipping.
lims = lims[::-1]
set_lim(lims)
def set_aspect(self, *args, **kwargs):
"""
Secondary axes cannot set the aspect ratio, so calling this just
sets a warning.
"""
cbook._warn_external("Secondary axes can't set the aspect ratio")
def set_xlabel(self, xlabel, fontdict=None, labelpad=None, **kwargs):
"""
Set the label for the x-axis.
Parameters
----------
xlabel : str
The label text.
labelpad : float, default: ``self.xaxis.labelpad``
Spacing in points between the label and the x-axis.
Other Parameters
----------------
**kwargs : `.Text` properties
`.Text` properties control the appearance of the label.
See Also
--------
text : Documents the properties supported by `.Text`.
"""
if labelpad is not None:
self.xaxis.labelpad = labelpad
return self.xaxis.set_label_text(xlabel, fontdict, **kwargs)
def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs):
"""
Set the label for the y-axis.
Parameters
----------
ylabel : str
The label text.
labelpad : float, default: ``self.yaxis.labelpad``
Spacing in points between the label and the y-axis.
Other Parameters
----------------
**kwargs : `.Text` properties
`.Text` properties control the appearance of the label.
See Also
--------
text : Documents the properties supported by `.Text`.
"""
if labelpad is not None:
self.yaxis.labelpad = labelpad
return self.yaxis.set_label_text(ylabel, fontdict, **kwargs)
def set_color(self, color):
"""
Change the color of the secondary axes and all decorators.
Parameters
----------
color : color
"""
if self._orientation == 'x':
self.tick_params(axis='x', colors=color)
self.spines['bottom'].set_color(color)
self.spines['top'].set_color(color)
self.xaxis.label.set_color(color)
else:
self.tick_params(axis='y', colors=color)
self.spines['left'].set_color(color)
self.spines['right'].set_color(color)
self.yaxis.label.set_color(color)
_secax_docstring = '''
Warnings
--------
This method is experimental as of 3.1, and the API may change.
Parameters
----------
location : {'top', 'bottom', 'left', 'right'} or float
The position to put the secondary axis. Strings can be 'top' or
'bottom' for orientation='x' and 'right' or 'left' for
orientation='y'. A float indicates the relative position on the
parent axes to put the new axes, 0.0 being the bottom (or left)
and 1.0 being the top (or right).
functions : 2-tuple of func, or Transform with an inverse
If a 2-tuple of functions, the user specifies the transform
function and its inverse. i.e.
``functions=(lambda x: 2 / x, lambda x: 2 / x)`` would be an
reciprocal transform with a factor of 2.
The user can also directly supply a subclass of
`.transforms.Transform` so long as it has an inverse.
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
for examples of making these conversions.
Returns
-------
ax : axes._secondary_axes.SecondaryAxis
Other Parameters
----------------
**kwargs : `~matplotlib.axes.Axes` properties.
Other miscellaneous axes parameters.
'''
docstring.interpd.update(_secax_docstring=_secax_docstring)

View file

@ -0,0 +1,242 @@
import functools
import uuid
from matplotlib import cbook, docstring
import matplotlib.artist as martist
from matplotlib.axes._axes import Axes
from matplotlib.gridspec import GridSpec, SubplotSpec
import matplotlib._layoutbox as layoutbox
class SubplotBase:
"""
Base class for subplots, which are :class:`Axes` instances with
additional methods to facilitate generating and manipulating a set
of :class:`Axes` within a figure.
"""
def __init__(self, fig, *args, **kwargs):
"""
Parameters
----------
fig : `matplotlib.figure.Figure`
*args : tuple (*nrows*, *ncols*, *index*) or int
The array of subplots in the figure has dimensions ``(nrows,
ncols)``, and *index* is the index of the subplot being created.
*index* starts at 1 in the upper left corner and increases to the
right.
If *nrows*, *ncols*, and *index* are all single digit numbers, then
*args* can be passed as a single 3-digit number (e.g. 234 for
(2, 3, 4)).
**kwargs
Keyword arguments are passed to the Axes (sub)class constructor.
"""
self.figure = fig
self._subplotspec = SubplotSpec._from_subplot_args(fig, args)
self.update_params()
# _axes_class is set in the subplot_class_factory
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
# add a layout box to this, for both the full axis, and the poss
# of the axis. We need both because the axes may become smaller
# due to parasitic axes and hence no longer fill the subplotspec.
if self._subplotspec._layoutbox is None:
self._layoutbox = None
self._poslayoutbox = None
else:
name = self._subplotspec._layoutbox.name + '.ax'
name = name + layoutbox.seq_id()
self._layoutbox = layoutbox.LayoutBox(
parent=self._subplotspec._layoutbox,
name=name,
artist=self)
self._poslayoutbox = layoutbox.LayoutBox(
parent=self._layoutbox,
name=self._layoutbox.name+'.pos',
pos=True, subplot=True, artist=self)
def __reduce__(self):
# get the first axes class which does not inherit from a subplotbase
axes_class = next(
c for c in type(self).__mro__
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
return (_picklable_subplot_class_constructor,
(axes_class,),
self.__getstate__())
def get_geometry(self):
"""Get the subplot geometry, e.g., (2, 2, 3)."""
rows, cols, num1, num2 = self.get_subplotspec().get_geometry()
return rows, cols, num1 + 1 # for compatibility
# COVERAGE NOTE: Never used internally or from examples
def change_geometry(self, numrows, numcols, num):
"""Change subplot geometry, e.g., from (1, 1, 1) to (2, 2, 3)."""
self._subplotspec = GridSpec(numrows, numcols,
figure=self.figure)[num - 1]
self.update_params()
self.set_position(self.figbox)
def get_subplotspec(self):
"""Return the `.SubplotSpec` instance associated with the subplot."""
return self._subplotspec
def set_subplotspec(self, subplotspec):
"""Set the `.SubplotSpec`. instance associated with the subplot."""
self._subplotspec = subplotspec
def get_gridspec(self):
"""Return the `.GridSpec` instance associated with the subplot."""
return self._subplotspec.get_gridspec()
def update_params(self):
"""Update the subplot position from ``self.figure.subplotpars``."""
self.figbox, _, _, self.numRows, self.numCols = \
self.get_subplotspec().get_position(self.figure,
return_all=True)
@cbook.deprecated("3.2", alternative="ax.get_subplotspec().rowspan.start")
@property
def rowNum(self):
return self.get_subplotspec().rowspan.start
@cbook.deprecated("3.2", alternative="ax.get_subplotspec().colspan.start")
@property
def colNum(self):
return self.get_subplotspec().colspan.start
def is_first_row(self):
return self.get_subplotspec().rowspan.start == 0
def is_last_row(self):
return self.get_subplotspec().rowspan.stop == self.get_gridspec().nrows
def is_first_col(self):
return self.get_subplotspec().colspan.start == 0
def is_last_col(self):
return self.get_subplotspec().colspan.stop == self.get_gridspec().ncols
def label_outer(self):
"""
Only show "outer" labels and tick labels.
x-labels are only kept for subplots on the last row; y-labels only for
subplots on the first column.
"""
lastrow = self.is_last_row()
firstcol = self.is_first_col()
if not lastrow:
for label in self.get_xticklabels(which="both"):
label.set_visible(False)
self.get_xaxis().get_offset_text().set_visible(False)
self.set_xlabel("")
if not firstcol:
for label in self.get_yticklabels(which="both"):
label.set_visible(False)
self.get_yaxis().get_offset_text().set_visible(False)
self.set_ylabel("")
def _make_twin_axes(self, *args, **kwargs):
"""Make a twinx axes of self. This is used for twinx and twiny."""
if 'sharex' in kwargs and 'sharey' in kwargs:
# The following line is added in v2.2 to avoid breaking Seaborn,
# which currently uses this internal API.
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
raise ValueError("Twinned Axes may share only one axis")
# The dance here with label is to force add_subplot() to create a new
# Axes (by passing in a label never seen before). Note that this does
# not affect plot reactivation by subplot() as twin axes can never be
# reactivated by subplot().
sentinel = str(uuid.uuid4())
real_label = kwargs.pop("label", sentinel)
twin = self.figure.add_subplot(
self.get_subplotspec(), *args, label=sentinel, **kwargs)
if real_label is not sentinel:
twin.set_label(real_label)
self.set_adjustable('datalim')
twin.set_adjustable('datalim')
if self._layoutbox is not None and twin._layoutbox is not None:
# make the layout boxes be explicitly the same
twin._layoutbox.constrain_same(self._layoutbox)
twin._poslayoutbox.constrain_same(self._poslayoutbox)
self._twinned_axes.join(self, twin)
return twin
def __repr__(self):
fields = []
if self.get_label():
fields += [f"label={self.get_label()!r}"]
titles = []
for k in ["left", "center", "right"]:
title = self.get_title(loc=k)
if title:
titles.append(f"{k!r}:{title!r}")
if titles:
fields += ["title={" + ",".join(titles) + "}"]
if self.get_xlabel():
fields += [f"xlabel={self.get_xlabel()!r}"]
if self.get_ylabel():
fields += [f"ylabel={self.get_ylabel()!r}"]
return f"<{self.__class__.__name__}:" + ", ".join(fields) + ">"
# this here to support cartopy which was using a private part of the
# API to register their Axes subclasses.
# In 3.1 this should be changed to a dict subclass that warns on use
# In 3.3 to a dict subclass that raises a useful exception on use
# In 3.4 should be removed
# The slow timeline is to give cartopy enough time to get several
# release out before we break them.
_subplot_classes = {}
@functools.lru_cache(None)
def subplot_class_factory(axes_class=None):
"""
Make a new class that inherits from `.SubplotBase` and the
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
This is perhaps a little bit roundabout to make a new class on
the fly like this, but it means that a new Subplot class does
not have to be created for every type of Axes.
"""
if axes_class is None:
cbook.warn_deprecated(
"3.3", message="Support for passing None to subplot_class_factory "
"is deprecated since %(since)s; explicitly pass the default Axes "
"class instead. This will become an error %(removal)s.")
axes_class = Axes
try:
# Avoid creating two different instances of GeoAxesSubplot...
# Only a temporary backcompat fix. This should be removed in
# 3.4
return next(cls for cls in SubplotBase.__subclasses__()
if cls.__bases__ == (SubplotBase, axes_class))
except StopIteration:
return type("%sSubplot" % axes_class.__name__,
(SubplotBase, axes_class),
{'_axes_class': axes_class})
Subplot = subplot_class_factory(Axes) # Provided for backward compatibility.
def _picklable_subplot_class_constructor(axes_class):
"""
Stub factory that returns an empty instance of the appropriate subplot
class when called with an axes class. This is purely to allow pickling of
Axes and Subplots.
"""
subplot_class = subplot_class_factory(axes_class)
return subplot_class.__new__(subplot_class)
docstring.interpd.update(Axes=martist.kwdoc(Axes))
docstring.dedent_interpd(Axes.__init__)
docstring.interpd.update(Subplot=martist.kwdoc(Axes))

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more