Plotting#
This page covers plotting the Graph
object in various matrix forms
using the plot
module. In particular we will focus on the
mdm
function and the Style
object. The
mdm
function can be used to quickly generate multi-domain-matrix
(MDM) and dependency-structure-matrix (DSM) figures of your data set. The
Style
object allows one to filter the information to be displayed
and set the appearance of the figure.
To illustrate the various plot options the climate control dataset from the
datasets
module is used.
Basic plotting#
To create a plot, first the plot
module has to be imported:
>>> import ragraph.plot
Next, one can use the mdm
function to create an MDM figure as shown
below.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... )
>>> fig.write_image(mdm_edge_kinds) # e.g.: "mdm_edge_kinds.svg"
The mdm
function requires two arguments: leafs
, which is the
list of nodes to be displayed on the rows and columns of the matrix, and edges
,
which is the list of edges (dependencies) to be displayed within the matrix. The figure
can be saved to file using the fig.write_image(filename)
function.
Figure 1 shows the resulting matrix. By default, the different edge kinds are
displayed within the matrix. In this case, the dataset only contains the edge kind
edge
, as shown within the legend at the bottom right of the figure.
Figure 1 Product DSM of the climate control system showing dependency kinds.#
Note that the leafs
are automatically sorted following the hierarchical structure
found within the graph. If one would like to display the leafs
within the order as
provided, one can pass along the argument sort=False
to the mdm
function.
In case one wants to create a non-square matrix or a matrix with different nodes on the
rows than on the columns. One can use the dmm
function:
>>> rows = graph.leafs[0:6]
>>> cols = graph.leafs
>>> fig = ragraph.plot.dmm(
... rows=rows,
... cols=cols,
... edges=[e for e in graph.edges_between_all(sources=cols, targets=rows)],
... sort=False,
... )
>>> fig.write_image(dmm_example) # e.g.: "dmm_example.svg"
Where ‘dmm’ is an abbreviation of domain-mapping-matrix. Figure 2 shows the resulting figure, in which we have more columns than rows.
Figure 2 Mapping matrix of the climate control system.#
Tuning the axis nodes#
A big part of your figures content is the ordering of leaf nodes on the axis. By default the leaf nodes you provide will be sorted according to:
The hierarchy they are subject to.
The node kind they belong to.
Whether or not they are a bus node.
How big their “width” is in terms of leaf nodes (e.g. biggest clusters first).
You can tweak this behavior like so:
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... node_kinds=["node"], # You can order and filter node kinds here.
... sort_args=dict(
... sort_by_bus=False, # Don't actively put bus nodes first.
... sort_by_width=False, # Don't put wider (larger) nodes first.
... ),
... )
>>> fig.write_image(mdm_tweaked_sort) # e.g.: "mdm_tweaked_sort.svg"
Figure 3 MDM figure with automatic sorting tweaked to only reflect the hierarchy and no further optimizations.#
Where sort_args
are options to be passed to
ragraph.analysis.sequence.axis
.
Or you can also disable this behavior altogether and plot the (leaf) nodes as given:
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs, # Custom ordering here.
... edges=graph.edges,
... sort=False,
... )
>>> fig.write_image(mdm_custom_sort) # e.g.: "mdm_custom_sort.svg"
Figure 4 MDM figure with automatic sorting disabled. Sorting is fully custom as given in the leaf list.#
Information filtering#
The Graph
may contain Edge
objects of various
kinds to which various labels and weights are attached. By passing along a
Style
object to the style
argument of the
mdm
function one can set the information that is displayed within
the matrix.
In the listing below, the value of the display
key of the piemap
property of
the Style
object is set to be equal to "weight labels"
.
Figure 5 shows the resulting matrix in which the labels of the different weights attached to the edges are displayed as categories within a pie chart.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(piemap=dict(display="weight labels")),
... )
>>> fig.write_image(mdm_edge_weight_labels) # e.g.: "mdm_edge_weight_labels.svg"
Figure 5 Product DSM of the climate control system showing all edge weight labels.#
By changing the value of display
to "weights"
the matrix changes from
a categorical plot to a numerical plot as shown in Figure 6. Here the actual
numerical values of the different weights are shown rather than the weight labels.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(
... display="weights",
... )
... ),
... )
>>>
>>> fig.write_image(mdm_edge_weights_1) # e.g.: "mdm_edge_weights_1.svg"
Figure 6 Product DSM of the climate control system showing edge weights.#
By default, all elements that belong to the display
category are shown. One can
change this by setting the fields
key of the piemap
dictionary as shown in the
following listing, which yields Figure 7. Here only the numerical values of the
weights "spatial"
, "energy flow"
, and "information flow"
are displayed.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(
... display="weights",
... fields=[
... "spatial",
... "energy flow",
... "information flow",
... ],
... )
... ),
... )
>>>
>>> fig.write_image(mdm_edge_weights_2) # e.g.: "mdm_edge_weights_2.svg"
Figure 7 Product DSM of the climate control system showing a subset of edge weights.#
In all figures shown so far, the wedges displayed within the pie charts are of equal
size. One can change this by setting the value of the mode
key of the piemap
dictionary to "relative"
. As a result, the size of the wedges is scaled following
the numerical value attached to it as shown in Figure 8.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(
... display="weights",
... fields=[
... "spatial",
... "energy flow",
... "information flow",
... ],
... mode="relative",
... )
... ),
... )
>>>
>>> fig.write_image(mdm_edge_weights_3) # e.g.: "mdm_edge_weights_3.svg"
Figure 8 Product DSM of the climate control system showing relative wedge sizes.#
The examples shown in this section are far from exhaustive. Check out the
PieMapStyle
documentation for all properties that one can
set within the piemap
dictionary.
Information highlighting#
Highlighting certain rows and columns is possible as well. By default, you can highlight
rows and columns by setting the highlight
annotation to True
on a
Node
instance:
>>> graph["Accumulator"].annotations.highlight = True # Highlight with default color.
>>> graph["Compressor"].annotations.highlight = "rgba(255,0,0,0.25)" # Override color.
>>> fig = ragraph.plot.mdm(leafs=graph.leafs, edges=graph.edges)
>>> fig.write_image(mdm_highlight) # e.g.: "mdm_highlight.svg"
Figure 9 Product DSM of the climate control system highlighting the Accumulator.#
You can override certain settings via the Style
, too:
>>> graph["Accumulator"].annotations.row = "rgba(0,0,255,0.25)" # Specific highlight value will take precedence.
>>> graph["Command Distribution"].annotations.row = True # Row highlight which will get row color.
>>> graph["Compressor"].annotations.highlight = True # Default highlight annotation.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... highlight_annotation="highlight", # Default highlight annotation.
... highlight_color="rgba(0,255,0,0.5)", # Default highlight color.
... piemap=dict(
... highlight_row_annotation="row", # Override the row annotation key.
... highlight_row_color="rgba(255,0,0,0.5)", # Override the default row color.
... highlight_col_annotation=None, # None means no override.
... highlight_col_color=None, # None means no override.
... )
... )
... )
>>> fig.write_image(mdm_highlight_custom) # e.g.: "mdm_highlight_custom.svg"
Figure 10 Product DSM of the climate control system showcasing all highlighting options.#
That’s a lot of overrides fighting for precedence! In short, when you override the
highlight_row_annotation
or highlight_col_annotation
keys in the piemap specific
options, the PieMap’s highlighting of rows or columns will only look for those
annotation keys in row or column Nodes (e.g. the default one is completely ignored). If
these override are unset or None, it will look for the global highlight annotation
key. So:
PieMapStyle
’s row/column annotation key is considered.Only if there is no style entry for that, the
Style
highlight key is considered.
Secondly, the color’s precedence is always:
Annotation key’s value if it’s a string.
PieMapStyle
row/column override color if set.Style
default highlight color.
Matrix styling#
The colors of the fields displayed within the matrix are automatically generated. However, for easy comparison of multiple plots one may want to explicitly set the color, or color map of a field.
In the listing below the the color of the displayed weight labels are explicitly set by
setting the value of the fields
key within the palettes
property of the
Style
object. Here one can provide a dictionary mapping weight
labels
to color hex codes
. The result of setting these colors is shown in
Figure 11.
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(
... display="weight labels",
... fields=["spatial", "energy flow", "information flow", "material flow"],
... ),
... palettes=dict(
... fields={
... "spatial": {"categorical": "#a64747"},
... "energy flow": {"categorical": "#de9c38"},
... "information flow": {"categorical": "#148a8e"},
... "material flow": {"categorical": "#b8bd6c"},
... }
... ),
... ),
... )
>>>
>>> fig.write_image(mdm_edge_categorical_field_colors)
Figure 11 Product DSM of the climate control system showing edge weight labels with user defined colors.#
Similarly, one can set the colors for numerical values. Here one has to provide a list
of colors (a color map). The colors
module provides several functions
for generating such color maps.
In the example below, the functions get_diverging_redblue
,
get_diverging_orangecyan
, and get_diverging_purplegreen
are used to set a
diverging color map for the displayed weights. Figure 12 shows the resulting
figure.
>>> from ragraph.colors import (
... get_diverging_redblue,
... get_diverging_orangecyan,
... get_diverging_purplegreen,
... )
>>>
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(
... display="weights",
... fields=[
... "spatial",
... "energy flow",
... "information flow",
... ],
... ),
... palettes=dict(
... fields={
... "spatial": {"continuous": get_diverging_redblue()},
... "energy flow": {"continuous": get_diverging_orangecyan()},
... "information flow": {"continuous": get_diverging_purplegreen()},
... }
... ),
... ),
... )
>>>
>>> fig.write_image(mdm_edge_continuous_field_colors)
Figure 12 Product DSM of the climate control system showing edge weight labels with user defined color gradients.#
Check out the documentation of the Palettes
object to
check all options that can be set using the pallettes
argument.
Basic customization#
The DSM literature presents a create variety of matrix based visualizations. These
visualizations often differ with respect to the information displayed within the matrix.
The plot
module is build upon the open source Plotly library. As such, one customize the generated figures.
In the listing below numbers and squares are added to the diagonal of the matrix as
shown in Figure 13. The numbers are added by adding a scatter trace to the
figure using the add_trace
method.
Note the row
and col
arguments of the add_trace
method. The plot consists of
six components which are instances of the Tree
,
Labels
, PieMap
, and
Legend
objects. These components are placed within a
grid containing two rows and five columns. By setting the row
and col
arguments
one can add the trace to one of the six plot components.
The squares are added by using the update_layout
method to update the shapes
property of the figure. In creating the figure shapes, the arguments xref
and
yref
are set to x9
and y9
, respectively. This ensures that the shapes are
positioned with respect to x-axis nine and y-axis nine. In this case, being the axes of
the matrix (piemap).
>>> import plotly.graph_objs as go
>>> fig = ragraph.plot.mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(piemap=dict(display="kinds")),
... )
>>>
>>> # Coordinates and colors for custom traces and shapes.
>>> n_nodes = len(graph.leafs)
>>> x = [idx + 0.5 for idx in range(n_nodes)]
>>> y = [n_nodes - 0.5 - idy for idy in range(n_nodes)]
>>> colors = ragraph.colors.get_green(n_nodes)
>>>
>>> # Create and add trace for numbers on diagonal.
>>> text_trace = go.Scatter(
... x=x,
... y=y,
... text=[str(idx + 1) for idx in range(n_nodes)],
... mode="text",
... showlegend=False,
... )
>>> fig = fig.add_trace(text_trace, row=2, col=4)
>>>
>>> # Create and add shapes for colored squares on diagonal.
>>> shapes = []
>>> for idx, color in enumerate(colors):
... shapes.append(
... go.layout.Shape(
... type="rect",
... x0=x[idx] - 0.5,
... y0=y[idx] + 0.5,
... x1=x[idx] + 0.5,
... y1=y[idx] - 0.5,
... fillcolor=color,
... xref="x9",
... yref="y9",
... layer="below",
... )
... )
...
>>> fig = fig.update_layout(shapes=list(fig.layout.shapes) + shapes)
>>>
>>> fig.write_image(mdm_custom)
Figure 13 Product DSM of the climate control system showing edge kinds with a custom color gradient and numbers on the diagonal.#
Advanced customization#
One can take customization one step further. As the plot components are placed within a grid, one can expand the grid with additional rows and columns filled with custom components.
The listing below shows a custom plot component class ColorBar
, which inherits
its properties from the Component
class which is
the basic building block for creating compound Ploty figures.
Within the custom_mdm
method the grid layout of Figure 14 is defined. That is,
Figure 14 is composed of three rows and six columns. The first row contains a
ColorBar
component within the fourth column. All other positions are empty within.
The second row contains six plot components: a Tree
, Label
, ColorBar
,
PieMap
, ColorBar
, and Legend
component. The third row only contains a
ColorBar
components.
With use of the functions Ragraph get_subplots(
and
process_fig
the plot components are joined into a single
Plotly figure object.
Note
Matching the scaling of all figures within a grid plot can be quite tricky. The axis
of neighboring plots are automatically linked. Hence one should take care when
setting the range of the axis of custom plot components.
The xstep
, ystep
, and
boxsize
properties of the Style
object are particularly important in matching the scaling of figures.
>>> from ragraph.plot.generic import Component, Style
>>> from typing import List, Optional
>>> from ragraph.node import Node
>>> from ragraph.edge import Edge
>>> from ragraph.plot import utils, components
>>> from copy import deepcopy
>>> from ragraph.colors import get_red, get_green, get_orange, get_blue
>>> from ragraph.analysis.heuristics import markov_gamma
>>>
>>> class ColorBar(Component):
... """Color bar plot component.
... Arguments:
... leafs: The list of nodes on the axis of the matrix.
... colors: The list of colors to be used in the color bar
... (len(colors >= len(leafs))).
... orientation: One of "horizontal" or "vertical". Defaults to vertical
... Style: Style object of the plot.
... """
... def __init__(
... self,
... leafs: List[Node],
... colors: List[str],
... orientation: str = "vertical",
... style: Style = Style(),
... ):
... # Calculating shape coordinates
... shapes = []
... for idx in range(len(leafs)):
... if orientation == "vertical":
... x0 = 0
... x1 = style.xstep
... y0 = idx * style.ystep
... y1 = (idx + 1) * style.ystep
... elif orientation == "horizontal":
... x0 = idx * style.xstep
... x1 = (idx + 1) * style.xstep
... y0 = 0
... y1 = style.ystep
... shapes.append(
... go.layout.Shape(
... type="rect",
... x0=x0,
... y0=y0,
... x1=x1,
... y1=y1,
... fillcolor=colors[idx],
... )
... )
... # Calculating geometric boundaries.
... if orientation == "vertical":
... width = (style.xstep) * style.boxsize
... height = len(leafs) * style.ystep * style.boxsize
... elif orientation == "horizontal":
... width = len(leafs) * style.xstep * style.boxsize
... height = (style.ystep) * style.boxsize
... xaxis = go.layout.XAxis(
... automargin=False,
... scaleanchor="y",
... autorange=False,
... scaleratio=1.0,
... # showgrid=True,
... # showline=True,
... showticklabels=False,
... # zeroline=True,
... range=(0, width / style.boxsize),
... )
... yaxis = go.layout.YAxis(
... automargin=False,
... autorange=False,
... showgrid=True,
... # showline=True,
... showticklabels=False,
... # zeroline=True,
... range=(0, height / style.boxsize),
... )
... super().__init__(
... width=width,
... height=height,
... traces=[],
... shapes=shapes,
... xaxis=xaxis,
... yaxis=yaxis,
... )
...
>>>
>>>
>>> def custom_mdm(
... leafs: List[Node],
... edges: List[Edge],
... style: Style = Style(),
... sort: Optional[bool] = True,
... node_kinds: Optional[List[str]] = None,
... show: Optional[bool] = False,
... ) -> go.Figure:
... """Get a custom plot of a Graph object."""
... if sort:
... leafs = utils.get_axis_sequence(leafs, kinds=node_kinds)
... first_row = [
... None,
... None,
... None,
... ColorBar(
... leafs=leafs,
... colors=get_red(len(leafs)),
... orientation="horizontal",
... style=style,
... ),
... None,
... None,
... ]
... second_row = [
... components.Tree(leafs, style=style),
... components.Labels(leafs, style=style),
... ColorBar(
... leafs=leafs,
... colors=get_green(len(leafs)),
... orientation="vertical",
... style=style,
... ),
... components.PieMap(rows=leafs, cols=leafs, edges=edges, style=style),
... ColorBar(
... leafs=leafs,
... colors=get_orange(len(leafs)),
... orientation="vertical",
... style=style,
... ),
... components.Legend(edges, style=style),
... ]
... third_row = [
... None,
... None,
... None,
... ColorBar(
... leafs=leafs,
... colors=get_blue(len(leafs)),
... orientation="horizontal",
... style=style,
... ),
... None,
... None,
... ]
... fig = utils.get_subplots([first_row, second_row, third_row])
... return utils.process_fig(fig=fig, show=show, style=style)
...
>>> fig = custom_mdm(
... leafs=graph.leafs,
... edges=graph.edges,
... style=ragraph.plot.Style(
... piemap=dict(display="weight labels"),
... ),
... )
>>>
>>> fig.write_image(mdm_custom_advanced)
Figure 14 Product DSM of the climate control system showing edge weights labels with custom colorbars around the matrix.#