""":mod:`~tessif.visualize.ldc` is a powerfull :mod:`tessif` line drawing utility based on
:func:`matplotlib.pyplot.plot`.
Designed to quickly draw arbitrarily complex sets of load duration curves.
Algorithm is build on a triple nested list engine which is able to visualize
plot data grouped as ``[categroy [subcategory [data]]]``
Default coloring uses :attr:`tessif.frused.themes.colors` and
:attr:`~tessif.frused.themes.cmaps`.
"""
from matplotlib import pyplot as plt
from matplotlib.ticker import MaxNLocator
import collections
import ittools
from itertools import cycle
from tessif.frused import themes
[docs]def plot(loads, category_labels=[], category_colors=[],
subcat_labels=[], subcat_colors=[],
load_label='Load in', load_unit='MW',
time_label='Cumulated time period in', time_unit='hours',
title='Load duration curves', integer_ticks=True,
**kwargs):
r"""
Draws a sophisticated bar plot for visualizing energy system load data.
Works on a simple yet powerfull nested list engine. Able to automatically
draw a simple bar chart or a full fledged energy system load analysis split
by major and minor categorization options. Minor categorization uses triple
nested lists, whereas major parameters are represented as double layer
nested lists. See parameters for details. Calls
:func:`matplotlib.pyplot.bar`.
Parameters
----------
loads : :class:`~collections.abc.Iterable`
Iterable of numbers i.e. :class:`list`, :class:`pandas.Series` or
:class:`pandas.DataFrame` etc.
To be plotted data will be transformed into a triple nested list as::
[ # Category Level
[ # Subcategory Level
[ # Data Level
]
]
]
- Upper most level:
[Sector 1, ..., Sector N]
- In between:
[Sbucategory 1, ..., Subcategory N] for each category
- Lowest level:
[Load at time step 1, .... load at time step N]
for each subcategory
Any nesting depth <= 3 can be handled and will be interpreted as
stated above
category_labels: :class:`~collections.abc.Iterable`, default=[]
Iterable container of strings used to label category level entries.
i.e.: ``['Power', 'Heat', 'Mobility']``
Beware:
- ``len(category_labels)`` !>= number of categories
- Use ``None`` to **not** draw any major labels.
- Use default to tag the n-th category by 'Category N'
category_colors: :class:`~collections.abc.Iterable`, default=[]
Iterable container of color strings used to color major categories
when drawing the major category plot.
i.e.: ``['yellow', 'red' 'blue']``
Beware:
- ``len(category_colors)`` !>= number of categories
- Use ``None`` to use **matplotlib's default coloring cycle**.
- Use ``[]`` (default) to invoke the
:attr:`tessif.frused.themes.colors` coloring.
subcat_labels: :class:`~collections.abc.Iterable`, default=[]
iterable container of strings used to tag subcategory level entries.
i.e : ``[['PV', 'Wind'], ['Gas', 'ST'], ['Diesel', 'Petrol']]``
Nest entries intuitively the way the loads are nested.
Beware:
- ``len(labels)`` !>= number of categories
- ``len(nested_labels)`` !>= number of respective subcategory
entries
- Use ``None`` to not tag any subcategory level entries
- Use ``[]`` (default) to tag the n-th subcategory entry by
'Subcategory N'
subcat_colors: :class:`~collections.abc.Iterable`, default=[]
iterable container of strings used to color subcategory level entries.
i.e:
``[['yellow' , '#123456'], ['red', 'crimson'], ['pink', 'black']]``
Nest entries intuitively the way the loads are nested.
Beware:
- ``len(colors)`` !>= number of categories
- ``len(nested_colors)`` !>= number of respective subcategories
- Use ``None`` to use matplotlib's default coloring cycle.
- Use ``[]`` (default) to invoke the
:attr:`tessif.frused.themes.cmaps` coloring.
load_label : str, default='Load in'
First part of the y label.
load_unit : str, default ='MW'
Second part of the y label
time_label : str, default='Cumulated time period in'
First part of the x label
time_unit : str, default='hours'
Second part of the x label
title : str ='Load duration curves'
Title displayed on the very top
integer_ticks : bool, default=True
Axes tick switch. If ``True`` there will only be integer ticks on both
axis.
kwargs :
kwargs are passed to :func:`matplotlib.pyplot.subplots`
Use them for sharing x and y axes for example.
Notes
-----
When using the list engine to categorize load duration curves, be aware
that the upper most load duration curve is the sum of all load duration
curves in that category.
Examples
---------
>>> import matplotlib.pyplot as plt
>>> import tessif.visualize.ldc as ldc
Creating the most simple load duration curve examplifying:
- Using :func:`plot` as simple laod duration curve plotter
- ``None`` usage to supress features
- Omitting the outer iterable on the optional parameters
- Using :paramref:`~plot.kwargs` to modify call to
:func:`matplotlib.pyplot.subplots`
>>> ldc.plot(
... loads=[1,2,3], # one load series
... subcat_labels=None, # None to supress feature
... category_labels='Power', # Omitting outer iterable
... tight_layout=True) # using **kwargs to ask for tight layout
IGNORE:
>>> plt.draw()
>>> plt.pause(.1)
IGNORE
.. image:: images/LDC_simple.png
:align: center
:alt: alternate text
Creating a ldc plot with 2 subcategories of the same category:
>>> ldc.plot(
... loads=[[1, 2, 3], [3, 2, 1]], # 1 cat of 2 subcats of 3 laods each
... subcat_labels=['PV', 'Wind'], # 2 subcats, 2 labels
... category_labels='Power', # Omitting outer iterable
... tight_layout=True) # using kwargs to ask for tight layout
IGNORE:
>>> plt.draw()
>>> plt.pause(.1)
IGNORE
.. image:: images/LDC_2subcats.png
:align: center
:alt: alternate text
Creating a ldc plot with 2 categories of 1 subcategory each:
>>> ldc.plot(
... loads=[[[1, 2, 3]], [[4, 5, 6]]], # 2 cats 1 subcat each
... # Beware of appropriate nesting when not using all levels:
... subcat_labels=[['Power'], ['Heat']],
... # Use default (empty list) to tag loads by 'Category N'
... category_labels=[],
... sharex=True, # using kwargs to share x axis
... tight_layout=True) # and ask for tight layout
IGNORE:
>>> plt.draw()
>>> plt.pause(.1)
IGNORE
.. image:: images/LDC_2cats.png
:align: center
:alt: alternate text
Creating an energy system load duration curve analysis plot with 2
categories of 3/2 subcategories:
>>> ldc.plot(
... loads=[[[1, 2, 3], [3, 2, 1], [3, 2, 2]],
... [[4, 5, 6], [6, 5, 4]]],
... # labels are nested intuitively
... subcat_labels=[['PV', 'Wind', 'Coal'], ['ST', 'Gas', ]],
... # All kinds of iterables are supported:
... category_labels=('Power', 'Heat'),
... sharex=True, # using kwargs to share x axis
... tight_layout=True) # and ask for tight layout
IGNORE:
>>> plt.draw()
>>> plt.pause(.1)
IGNORE
.. image:: images/LDC_32.png
:align: center
:alt: alternate text
Creating an energy system load duration curve analysis plot - Design Case:
>>> ldc.plot(
... # 3 cats, 3/2/4 subs
... loads=[[[1, 2, 3], [2, 2, 1], [3, 1, 1]],
... [[4, 6, 6], [6, 5, 4]],
... [[3, 4, 5], [3, 3, 3], [4, 5, 6], [4, 2, 3]]],
... # Varying subcat lengths
... subcat_labels=[['PV', 'Wind', 'Water'], ['ST', 'Gas'],
... ['Cars', 'Trucks', 'Shipping', 'Aviation']],
... # All kinds of iterables are supported:
... category_labels=('Power', 'Heat', 'Mobility'),
... sharex=True, # using kwargs to share x axis
... tight_layout=True) # and ask for tight layout
IGNORE:
>>> plt.draw()
>>> plt.pause(3)
>>> plt.close('all')
IGNORE
.. image:: images/LDC_DesignCase.png
:align: center
:alt: alternate text
"""
# # 1.) Prepare the plot data:
# # 1.1) Make sure loads is a nested list of plots to be drawn:
# if not all(isinstance(load, collections.Iterable) for load in loads):
# loads = [loads]
# 1.) Handle input:
# 1.1) Transform loads to: [Cats.., [Subcats.. [ Loads...]]]
loads = ittools.nestify(ittools.itrify(loads, list), 3, list)
# 1.2) Transform category parameters to ['Param 1', ... 'Param N']
major_attr = [category_labels, category_colors]
for pos, attribute in enumerate(major_attr):
# category parameter was stated
if attribute:
# make sure each attribute is a list as in [Attribute, ...]
major_attr[pos] = ittools.nestify(
ittools.itrify(attribute, list), 1, list)
# default was used, so utilize default themes (interfaces.themes.py)
elif ittools.is_empty(attribute):
# modify category_labels
if pos == 0:
major_attribute = ittools.Stringcrementor('Category', 1)
# modify category_colors
if pos == 1:
major_attribute = cycle(themes.colors.sector.values())
# create empty placeholder list
major_attr[pos] = []
# iterate through each load category...
for category, category_laods in enumerate(loads):
# to append major_attribute
major_attr[pos].append(next(major_attribute))
# copy changes to parameters:
category_labels, category_colors = major_attr
# 1.3) Transform subcategory parameters to
# [['Param 1',...], ..., ['Param N', ...]]
attr = [subcat_labels, subcat_colors]
for pos, attribute in enumerate(attr):
if attribute: # subcategory parameter was stated
attr[pos] = ittools.nestify(
ittools.itrify(attribute, list), 2, list)
elif ittools.is_empty(attribute):
if pos == 0: # use default themes (interfaces.themes.py)
themed = [ittools.Stringcrementor('Subcategory', 1)
for i in range(len(loads))]
if pos == 1:
themed = [clrc for clrc in themes.ccycles.sector.values()]
# create empty placeholder list
attr[pos] = []
# iterate through each load category:
for category, category_loads in enumerate(loads):
# create empty nested placeholder list:
attr[pos].append([])
# iterate through each load subcategory
for subcategory, subcat_loads in enumerate(category_loads):
attr[pos][category].append(next(themed[category]))
# copy changes to parameters:
subcat_labels, subcat_colors = attr
# 1.3) Sort load data (Y-Axis) from highest to lowest:
# iterate through each category:
for category, category_loads in enumerate(loads):
# iterate though each subcategory load:
for subcat, subcat_loads in enumerate(category_loads):
loads[category][subcat] = sorted(subcat_loads, reverse=True)
# Compute the number of plots to be drawn if a category has more than
# 1 subcategory an additional plot will be drawn for detailed analysis
subplots = 1
if len(loads) > 1:
for cat_loads in loads:
if len(cat_loads) > 1:
subplots += 1
# 2.) Create an empty canvas to draw on:
f, ax = plt.subplots(subplots, 1, **kwargs)
skipper = 0 # counter variable to skip unsubcategorized categories
for subplt, axis in enumerate(
ax if isinstance(ax, collections.Iterable) else [ax]):
# Category overview plot:
if subplt == 0 and len(loads) > 1:
for category, cat_loads in enumerate(loads):
sum_of_loads = list( # aggregate all subcat loads
sum(load_data) for load_data in zip(*cat_loads))
axis.plot(
list(range(1, len(sum_of_loads)+1)), sum_of_loads,
label=category_labels[
category] if category_labels else None,
color=category_colors[
category] if category_colors else None,
)
# 3.) Do the actual plotting:
# Subcategory plots:
else:
# skip subcats that consist of only one load series
# since they have been exhaustivley visualized in the overview plot
if len(loads[subplt-1+skipper]) <= 1:
skipper += 1
for subcategory, subcat_loads in enumerate(
loads[subplt-1+skipper]):
axis.plot(
list(range(1, len(subcat_loads)+1)), subcat_loads,
label=subcat_labels[subplt-1+skipper][
subcategory]if subcat_labels else None,
color=subcat_colors[subplt-1+skipper][
subcategory] if subcat_colors else None)
axis.set_title(category_labels[subplt-1+skipper]
if category_labels else None)
for axis in ax if isinstance(ax, collections.Iterable) else [ax]:
axis.xaxis.set_major_locator(MaxNLocator(integer=True))
# Set y label:
axis.set_ylabel('{} {}'.format(load_label, load_unit))
# make y axius start at 0
axis.set_ylim(bottom=0)
# 7.) Draw the legend
bbox = axis.get_position()
# 2.2) shrink bounding box to 80% of original width
axis.set_position([bbox.x0, bbox.y0, bbox.width*0.8, bbox.height])
handles, tags = axis.get_legend_handles_labels()
if tags:
axis.legend(handles[::-1], tags[::-1],
# labelspacing=1,
# title='This Title'
bbox_to_anchor=(1.0, 1), loc='upper left',
# borderaxespad=0
)
if integer_ticks:
axis.yaxis.set_major_locator(MaxNLocator(integer=True))
if isinstance(ax, collections.Iterable):
ax[-1].set_xlabel('{} {}'.format(time_label, time_unit))