"""This could be legendary, boom, boom!
It occurs to me that legends are tables too.
Or tables are legends, if you prefer.
Both arrange text and patches, in some sort of grid.
Rows and columns, some text and a patch of colour. Or color?
The legend, has magic capabilities, including being able to inspect
a plot and figure out what to use for the various labels and patches.
The patches for the legend are just `handles` for which there are a
number of magic functions that turn the handle into an `Artist` to
represent that handle in the legend.
The legend uses objects from `matplotlib.offsetbox` to do all the drawing.
These look to be the pieces that the table ought to be using.
If you think as legends as meta data regarding a plot, the row and
column headings of a table, if you like, with their associated
timelines, then the association with tables is stronger still.
The legend probes around in the plot data to uncover meta-data.
The legend code has some specific restrictions::
* each column is a pair (patch/text) or (text/patch).
* you can specify the number of columns
* rows calculated from the data
* some columns may be short.
The objects in `matplotlib.offsetbox` are more general.
notes
=====
`matplotlib.subplot_mosaic` introduces an interesting ways of
specifying table layouts.
After more digging around in matplotlib inards I discovered the
LayoutGrid class.
`_layoutgrid`
This one uses constraints and a solver to deal with layouts.
There's a lot of offsetbox code that would not be needed.
The key thing about an `offsetbox.Artist` that it knows the fontsize
and should aim to ensure that it's size is proportional to the
fontsize. This is achieved by making things such as padding a
multiple of the fontsize.
This is desirable for packing tables with text and provides a way to
scale the whole image by adjusting a single fontsize variable.
It can save a lot of work over the current table, constantly measuring
text.
Presumably we can add the fontsize into the whole thing as a
constraint of some sort?
In short, I think I have another module to take a look at.
Update: the subplot_mosaic code has some great ideas.
It is very much focussed on axes.
Nested mosaic's of axes open up a lot of interesting opportunities,
here's hoping we can transform these mosaics and keep track of all the
axes.
`subplot_mosaic` gives us a figure and a dictionary of axes.
"""
import traceback
from matplotlib import offsetbox, pyplot, artist, transforms, figure
from matplotlib import _layoutgrid as layoutgrid
from matplotlib.offsetbox import TextArea, HPacker, VPacker, DrawingArea
#offsetbox.DEBUG = True
from blume import magic
from blume.table import Cell
[docs]
class LegendArray(magic.Ball):
""" Draw a table from a dictionary of ...
dictionaries of artists?
dictionaries of dictionaries
list of dictionaries with lists of dictionaries as values.
and so on, ad infinitum.
I think the answer will be to make this all recursive.
So hang on and lets see what goes boom!
"""
def __init__(self, data):
self.inner = HPacker
self.outer = VPacker
self.grid = Grid(data)
pass
[docs]
class Cell(offsetbox.OffsetBox):
pass
[docs]
class Grid(offsetbox.AnchoredOffsetbox):
""" A grid of cells.
What I really need here is just create a grid of
nested [HV]Packers.
But you cannot create the Packers until you have it's children.
The way forward is less clear.
For now, just create something, so we can explore the mode
"""
def __init__(self,
data,
inner=None,
outer=None,
align=None,
mode=None,
transpose=False,
bbox=None,
loc=None,
prop=None):
if inner is None: inner = HPacker
if outer is None: outer = VPacker
if transpose:
inner, outer = outer, inner
align = align or 'baseline'
mode = mode or 'equal'
loc = loc or 1
hboxes = []
textprops = None
if prop:
textprops = prop.copy()
for row in data:
#textprops = dict(horizontalalignment='right')
#textprops = {}
children = [TextArea(item, textprops=textprops) for item in row]
hboxes.append(inner(pad=0, sep=0, mode=mode, align=align,
children=children))
vbox = outer(pad=0, sep=0, align=align, mode=mode,
children=hboxes)
super().__init__(loc=loc,
#bbox_to_anchor=(0, 0, 1, 1),
child=vbox,
prop=prop)
def scale(self, factor):
for child in self._children:
for gchild in child._children:
t = gchild._text
t.set_size(t.get_size() * factor)
[docs]
def get_window_extent(self, renderer):
"""Return the bounding box of the table in window coords."""
boxes = []
for child in self.get_children():
boxes += [x.get_window_extent(renderer) for x in child.get_children()]
return transforms.Bbox.union(boxes)
def xdraw(self, renderer):
self.facecolor = COLORS[0]
COLORS.rotate()
print(f'drawing grid {id(self)} color {self.facecolor}')
print(f'drawing grid {id(self)} axes {self.axes}')
#traceback.print_stack()
from matplotlib.patches import bbox_artist
props= dict(pad=20)
bbox_artist(self, renderer, props=props, fill=True)
#print('drawn', self.get_window_extent(renderer))
super().draw(renderer)
from collections import deque
import random
COLORS = deque(['skyblue', 'green', 'yellow', 'pink', 'orange',
[random.random()/2,
random.random()/2,
random.random()/2]])
[docs]
class LayoutGrid(layoutgrid.LayoutGrid):
""" A grid of cells.
What I really need here is just create a grid of
nested [HV]Packers.
But you cannot create the Packers until you have it's children.
The way forward is less clear.
For now, just create something, so we can explore the mode
"""
def __init__(self,
data,
inner=None,
outer=None,
align=None,
mode=None,
transpose=False,
bbox=None,
loc=None):
if inner is None: inner = HPacker
if outer is None: outer = VPacker
if transpose:
inner, outer = outer, inner
align = align or 'baseline'
mode = mode or 'equal'
loc = loc or 1
hboxes = []
super().__init__(nrows=len(data), ncols=len(data[0]))
for rix, row in enumerate(data):
for cix, col in enumerate(row):
self.add_child(TextArea(col), rix, cix)
def draw(self, renderer):
x0, y0, x1, y1 = self.get_extent(renderer)
bbox = transforms.Bbox(((x0,y0), (x1,y1)))
print(bbox.p0, bbox.p1)
print(renderer.dpi)
print(f'{bbox}')
super().draw(renderer)
[docs]
class Carpet:
""" A figure to manage a bunch of axes in a mosaic.
"""
def __init__(self):
self.fig = pyplot.figure()
self.gs = self.fig.add_gridspec(1,1)
self.axes = {}
[docs]
def set_mosaic(self, mosaic, axes=None):
"""Set the figures mosaic
Aim to do this in a way we can keep track of the axes.
Returns a dictionary of newly added axes and the (updated)
existing dictionary of all axes.
"""
fig = self.fig
# delete what is there
for ax in fig.axes:
fig.delaxes(ax)
fig._gridspecs = []
new_axes = fig.subplot_mosaic(mosaic)
newones = {}
for key, nax in new_axes.items():
ax = self.axes.get(key)
if ax:
#print('switching old to new spec', key)
ax.set_subplotspec(nax.get_subplotspec())
fig.delaxes(nax)
fig.add_subplot(ax)
else:
self.axes[key] = nax
newones[key] = nax
return newones, self.axes