"""
These functions manages matplotlib plotting of sections.
"""
"""
Features of a plot:
- Create a visualization of the section
- Show a dictionary of common propreties Ix, Sx, Zx, etc.
- Show a dictionary of results
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection, PatchCollection
from matplotlib.patches import Circle, Polygon
from .. section import SectionAbstract, SectionRectangle, SectionSteel, SteelSectionTypes, SectionCLT
from .. element import BeamColumn
from .. display import MATCOLOURS, PlotConfigCanvas, PlotConfigObject, PlotOriginPosition
# from .model import GeomModel, GeomModelRectangle, GeomModelIbeam, GeomModelIbeamRounded, GeomModelGlulam
import limitstates.objects.output.model as md
import matplotlib.patches as mpatches
import matplotlib.path as mpath
from matplotlib.axes import Axes
class SectionPlotter:
plotOffset = 0.05
def __init__(self, baseGeom:md.GeomModel, canvasProps:PlotConfigCanvas):
self.geom = baseGeom
self.canvasConfig = canvasProps
def _getPlotLimits(self, x:list[float], y:list[float]):
dx = max(x) - min(x)
dy = max(y) - min(y)
xmin = min(x) - self.plotOffset*(1+dx)
xmax = max(x) + self.plotOffset*(1+dx)
ymin = min(y) - self.plotOffset*(1+dy)
ymax = max(y) + self.plotOffset*(1+dy)
return (xmin, xmax), (ymin, ymax)
def _getPlotSize(self, xlims:list[float], ylims:list[float]):
dx = xlims[1] - xlims[0]
dy = ylims[1] - ylims[0]
self.dx = dx
self.dy = dy
dmax = max(dy, dx)
maxFigsize = self.canvasConfig.maxFigsize
return dx / dmax * maxFigsize, dy / dmax * maxFigsize
def initPlot(self, ax:Axes = None):
x, y = self.geom.getVerticies()
xlims, ylims = self._getPlotLimits(x,y)
xplot, yplot = self._getPlotSize(xlims, ylims)
# Set the dimensions / aspect ratio of the plot
if ax is not None:
ax.set_aspect(1)
fig = ax.get_figure()
else:
fig, ax = plt.subplots(figsize=(xplot, yplot), dpi = self.canvasConfig.dpi)
if self.canvasConfig.showAxis != True:
ax.axis('off')
# Set the dimensions /
ax.set_xlim(xlims)
ax.set_ylim(ylims)
return fig, ax
def _getFillColour(self, objectConfig, kwargs):
# overwrite teh colour if needed.
if 'c' in kwargs:
c = kwargs['c']
kwargs.pop('c', None)
else:
c = objectConfig.c
return c
def _getPlotLabel(infoDict):
"""
Converts the in
{'label':value, 'label2':value ...}
label =
"""
label = r'mathbf{Section Summary}$ \n'
for item in infoDict:
line = str(item) + ' = ' + str(round(infoDict[item])) + '\n'
label += line
return label
def plotDesignInfo(self, fig, ax, infoDict):
# x0 = np.average(ax.get_xlim())
# y0 = np.average(ax.get_ylim())
x0 = ax.get_xlim()[-1] + self.dx / 20
y0 = ax.get_ylim()[-1]
text = "Section Information.\n"
for item in infoDict:
text += item +": " + str(round(infoDict[item])) + "\n"
ax.text(x0, y0, text)
def plotSectionInfo():
pass
def plot(self, ax, xy, objectConfig, *args, **kwargs):
"""
plot a set of xy points on the canvas.
"""
c = self._getFillColour(objectConfig, kwargs)
objectPatch = Polygon(xy, *args, color = c, **kwargs)
ax.add_patch(objectPatch)
# Add the border line around the object
if objectConfig.showOutline and (("linewidth" not in kwargs) or ("lw" not in kwargs)):
ax.plot(xy[:,0], xy[:,1],
linewidth = objectConfig.lineWidth,
c=objectConfig.cLine)
return ax
class SectionPlotterWithHole(SectionPlotter):
def _getPatchWithHole(self, xyOutside, xyInside):
"""
See https://matplotlib.org/stable/gallery/shapes_and_collections/donut.html
"""
Nverts = len(xyOutside)
lineCode = mpath.Path.LINETO
codes = np.ones(Nverts, dtype=mpath.Path.code_type) * lineCode
codes[0] = mpath.Path.MOVETO
vertices = np.concatenate((xyOutside[::],
xyInside[::-1]))
drawingInstructions = np.concatenate((codes, codes))
# Create the Path object
path = mpath.Path(vertices, drawingInstructions)
return path
def plot(self, ax, xy, objectConfig, *args, **kwargs):
"""
plot a set of xy points on the canvas.
It's assumed that the inside and outside verticies are passed in with
one array, xy. This array will '
"""
# We assume that the two arrays have the same size
xyOut, xyIn = np.split(xy, 2)
c = self._getFillColour(objectConfig, kwargs)
path = self._getPatchWithHole(xyOut, xyIn)
patch = mpatches.PathPatch(path, *args, color = c, **kwargs)
ax.add_patch(patch)
# Add the border line around the object
if objectConfig.showOutline and (("linewidth" not in kwargs) or ("lw" not in kwargs)):
ax.plot(xyOut[:,0], xyOut[:,1],
linewidth = objectConfig.lineWidth,
c=objectConfig.cLine)
ax.plot(xyIn[:,0], xyIn[:,1],
linewidth = objectConfig.lineWidth,
c=objectConfig.cLine)
return ax
def _getPlotOrigin(option, b, d, xy0):
if option == PlotOriginPosition.centered:
return (0 + xy0[0], 0 + xy0[1])
elif option == PlotOriginPosition.bottomCenter:
return (0 + xy0[0], d/2 + xy0[1])
elif option == PlotOriginPosition.bottomLeft:
return (b/2 + xy0[0], d/2 + xy0[1])
else:
raise Exception()
"""
We can combine the following two funcitons by assigning each section a plot
enumeration, i.e. 1 = glulam, 2 = steel, 3 = CLT,
Then checking against that enumeration.
This allow for slightly faster section checking behaviour
"""
def _defaultConfigFactory(section):
if isinstance(section, SectionRectangle): # typical section
defaultProps = PlotConfigObject(c = MATCOLOURS['glulam'], originLocation= 1)
elif isinstance(section, SectionSteel): # steel section
defaultProps = PlotConfigObject(c = MATCOLOURS['steel'], originLocation= 1)
elif isinstance(section, SectionCLT): # CLT section
defaultProps = PlotConfigObject(c = MATCOLOURS['clt'], originLocation= 3)
defaultProps.cFillLines = MATCOLOURS['black']
defaultProps.cFillPatch = MATCOLOURS['cltWeak']
else:
raise Exception(f'Section of type {section} is not supported.')
return defaultProps
def _plotGeomFactory(section: SectionAbstract,
originLocation: int|PlotOriginPosition,
xy0) -> md.GeomModel:
"""
A function that returns the appropriate geometry object given a section.
A thought - why haven't we made this an object attribute?
"""
if isinstance(section, SectionRectangle):
b, d = section.b, section.d
xy = _getPlotOrigin(originLocation, b, d, xy0)
geom = md.GeomModelRectangle(b, d, *xy)
elif isinstance(section, SectionSteel):
b, d = section.bf, section.d
xy = _getPlotOrigin(originLocation, b, d, xy0)
geom = _plotFactorySteel(section, *xy)
elif isinstance(section, SectionCLT):
b, layers = section.w, section.sLayers
xy = _getPlotOrigin(originLocation, b, layers.d, xy0)
geom = md.GeomModelClt(layers, b, *xy)
else:
raise Exception(f'Section of type {section} is not supported.')
return geom
def _plotterFactory(section: SectionAbstract,
geom: md.GeomModel,
canvasConfig: PlotConfigCanvas) -> SectionPlotter:
"""
A function that returns the appropriate geometry object given a section.
A thought - why haven't we made this an object attribute?
"""
if isinstance(section, SectionSteel) and (section.typeEnum == SteelSectionTypes.hss):
return SectionPlotterWithHole(geom, canvasConfig)
else:
return SectionPlotter(geom, canvasConfig)
def _plotFactorySteel(section:SectionSteel, *args):
enum = section.typeEnum
if SteelSectionTypes.w == enum:
# If there is information about the rounded section, plot that
if hasattr(section, 'r1') and hasattr(section, 'r2'):
geom = md.GeomModelIbeamRounded(section.d,
section.tw,
section.bf,
section.tf,
section.r2,
section.r1,
*args)
# If there is information about the rounded section, plot that
else:
geom = md.GeomModelIbeam(section.d,
section.tw,
section.bf,
section.tf,
*args)
elif SteelSectionTypes.hss == enum:
ro = section.ro
ri = section.ri
geom = md.GeomModelHss(section.d, section.bf, section.t, ro, ri, *args)
return geom
def _setupSummaryDict(listIn, ):
pass
def _plotfillLines(ax, geom, objectConfig):
linex, liney = geom.getFillVerticies()
lverts = [np.column_stack((x,y)) for x, y in zip(linex, liney)]
lines = LineCollection(lverts, colors = objectConfig.cFillLines,
linewidth = 0.5)
ax.add_collection(lines)
def _plotfillPatches(ax, geom, objectConfig):
linex, liney = geom.getFillAreas()
lverts = [np.column_stack((x,y)) for x, y in zip(linex, liney)]
p = PatchCollection([Polygon(vert) for vert in lverts], color = objectConfig.cFillPatch)
ax.add_collection(p)
[docs]def plotSection(section:SectionAbstract,
xy0: list[float,float] = None,
canvasConfig: PlotConfigCanvas = None,
objectConfig: PlotConfigObject = None,
ax:Axes = None,
summarizeGeometry: bool|list[str]=False,
*args, **kwargs):
"""
Creates a plot of the section centered at xy0.
A default set of propreties will be chosen for the section depending on
it's type. Custom propreties can also be given to the canvas and object by passing in
a canvasConfig object.
The figure propreties can be set by using a custom PlotDisplayProps object.
Steel sections will be plotted with rounded corners, if information about
the corner radius exists in r1 / r2.
Where additional arguemts passed to args and kwargs confict with arguments
passed in the config object, the arg/kwargs will overwrite the config
objects.
Parameters
----------
section : SectionAbstract
The section to be plotted.
xy0 : float, optional
The x/y point to use for the orign of the plot. The default is (0,0).
Note that the object may be plotted at a different location
dispProps : PlotDisplayProps, optional
The display propreties to use. The default is None.
canvasConfig : PlotConfigCanvas, optional
The canvas configuration object to be used. The when set to none a
default object is used.
objectConfig : PlotConfigObject, optional
The object configuration object to be used. This can be set
When set to none, a default object will be chosen based on the on the
section type input.
ax : Axes, optional
An overwrite that allows plots to be created on a specific figure.
The default is None, which creates a new plot.
summarizeGeometry : bool|list[str], optional
XXX does not work currently.
A list of the input attributes to summarize. The default is False.
*args : list
Additional arguments for matplotolib's ax.fill function
**kwargs : dict
Additional arguments for matplotolib's ax.fill function
Returns
-------
fig : matplotlib figure
The output matplotlib figure.
ax : matplotlib axis
The output matplotlib axis.
"""
if not canvasConfig:
canvasConfig = PlotConfigCanvas()
if not objectConfig:
objectConfig = _defaultConfigFactory(section)
# Setup default xy0 plot center
if xy0 is None:
xy0 = [0,0]
geom = _plotGeomFactory(section, objectConfig.originLocation, xy0)
plotter = _plotterFactory(section, geom, canvasConfig)
fig, ax = plotter.initPlot(ax)
xyVerts = np.column_stack(geom.getVerticies())
plotter.plot(ax, xyVerts, objectConfig, *args, **kwargs)
if hasattr(geom, 'getFillVerticies'):
_plotfillLines(ax, geom, objectConfig)
if hasattr(geom, 'getFillAreas'):
_plotfillPatches(ax, geom, objectConfig)
ax.plot()
return fig, ax
def _getFireSectionPositonGL(burnDims):
"""
Figures out how much to offset the fire section by.
This will depend on the fire condition used.
The if statement logic for this function may be too complex.
In that case, a seperate way of tracking how much each side is burned
will be needed.
"""
dx = (burnDims[3] - burnDims[1]) / 2
dy = (burnDims[2] - burnDims[0]) / 2
return dx, dy
def _getFireSectionPositonCLT(burnDims):
"""
Figures out how much to offset the fire section by.
This will depend on the fire condition used.
The if statement logic for this function may be too complex.
In that case, a seperate way of tracking how much each side is burned
will be needed.
"""
dx = 0
# dy = (burnDims[2] - burnDims[0]) / 2
#TODO: this will need to be updated when we do walls.
dy = burnDims[0]/2
return dx, dy
def _hasFireSection(dispProps):
return (hasattr(dispProps, 'sectionFire') and dispProps.sectionFire)
def _isCLTSection(dispProps):
return isinstance(dispProps.section, SectionCLT)
def _isGlulamSection(dispProps):
return hasattr(dispProps, 'sectionFire')
# def _isGlulamSection(dispProps):
# return hasattr(dispProps, 'sectionFire')
# If the logic gets too complex here, then we should create a plotting objects
# that have a interface, "plot", and a factory for them.
def _plotFactory(dispProps, ax=None):
"""
Figures out what type of plot function to use.
"""
if _isCLTSection(dispProps):
return _plotCLT(dispProps, ax)
if _isGlulamSection(dispProps):
return _plotGlulam(dispProps, ax)
else:
return _plotBasic(dispProps, ax)
def _plotBasic(dispProps, ax = None):
"""
Plots a basic section using the canvas plot configuration and canvas object
configuration classes.
"""
cPlotConfig = dispProps.configCanvas
cObjConfig = dispProps.configObject
geom = _plotGeomFactory(dispProps.section, cObjConfig.originLocation, [0,0])
plotter = _plotterFactory(dispProps.section, geom, cPlotConfig)
fig, ax = plotter.initPlot(ax)
xy = np.column_stack(geom.getVerticies())
plotter.plot(ax, xy, cObjConfig)
return fig, ax
def _plotGlulam(dispProps, ax = None):
"""
Plots a glulam section, showing the fire section in the center if it is
present.
We also show some fill lines for the
"""
cPlotConfig = dispProps.configCanvas
section = dispProps.section
hasFireSection = _hasFireSection(dispProps)
if hasFireSection:
canvasObjConfig = dispProps.configObjectBurnt
else:
canvasObjConfig = dispProps.configObject
# Find the offset for the base section
b, d = section.b, section.d
dx0, dy0 = _getPlotOrigin(canvasObjConfig.originLocation, b, d, [0,0])
# Get the geometry and initilziet the plot for the base section
geom = md.GeomModelGlulam(b, d, dx0 = dx0, dy0 = dy0)
plotter = SectionPlotter(geom, cPlotConfig)
fig, ax = plotter.initPlot(ax)
# Plot the base object
plotter.plot(ax, np.column_stack(geom.getVerticies()), canvasObjConfig)
# Plot the fire section.
if hasFireSection:
sFire = dispProps.sectionFire
dx, dy = _getFireSectionPositonGL(dispProps.burnDimensions)
dh = dispProps.displayLamHeight
geom = md.GeomModelGlulam(sFire.b, sFire.d, dh, dx + dx0, dy + dy0)
objectConfig = dispProps.configObject
plotter.plot(ax, np.column_stack(geom.getVerticies()), objectConfig)
# Plot the internal fill lines
_plotfillLines(ax, geom, canvasObjConfig)
return fig, ax
def _plotCLT(dispProps, ax = None):
cPlotConfig = dispProps.configCanvas
section = dispProps.section
hasFireSection = _hasFireSection(dispProps)
if hasFireSection:
canvasObjConfig = dispProps.configObjectBurnt
else:
canvasObjConfig = dispProps.configObject
b, d = section.w, section.sLayers.d
dx0, dy0 = _getPlotOrigin(canvasObjConfig.originLocation, b, d, [0,0])
geom = md.GeomModelClt(section.sLayers, b, dx0 = dx0, dy0 = dy0)
plotter = SectionPlotter(geom, cPlotConfig)
fig, ax = plotter.initPlot(ax)
# Plot the base object
plotter.plot(ax, np.column_stack(geom.getVerticies()), canvasObjConfig)
if hasFireSection:
sFire = dispProps.sectionFire
# plotLayers = _getPlotLayers(sFire)
plotLayers = sFire.layers
dx, dy = _getFireSectionPositonCLT(dispProps.burnDimensions)
geom = md.GeomModelClt(plotLayers, b, dx + dx0, dy + dy0)
objectConfig = dispProps.configObject
plotter.plot(ax, np.column_stack(geom.getVerticies()), objectConfig)
_plotfillLines(ax, geom, canvasObjConfig)
_plotfillPatches(ax, geom, canvasObjConfig)
return fig, ax
def _getPlotLayers(sFire):
"""
Return the biggest between the strong/weak axis layer group
"""
layersOut = [layer for layer in sFire.sLayers]
if len(sFire.sLayers) == len(sFire.wLayers):
return sFire.wLayers
else:
return sFire.sLayers
[docs]def plotElementSection(element:BeamColumn,
ax = None,
summarizeGeometry: bool|list[str]=False):
"""
Creates a plot of the section the element is using. Only applies to
elements that have a "eleDisplayProps" set.
The figure propreties can be set by modifying or replacing the element's
eleDisplayProps object.
If the element has a plot section set, that will be used for plotting
instead of the base element.
Parameters
----------
element : BeamColumn
The sectin to be plotted.
ax : Axes, optional
An overwrite that allows plots to be created on a specific figure.
The default is None, which creates a new plot.
summarizeGeometry : bool|list[str], optional
XXX currently unused XXX
If false, dispalys nothing.
If true, tries to find a default proprety list defined in the
element.eleDisplayProps
If a list, creates a summary of the given attributes.
The default is False.
Returns
-------
fig : matplotlib figure
The output matplotlib figure.
ax : matplotlib axis
The output matplotlib axis.
"""
dispProps = element.eleDisplayProps
# Use the display section if it is set.
if not dispProps.section:
dispProps.section = element.section
return _plotFactory(dispProps, ax)