.. _plotting: ######## Plotting ######## .. testsetup:: >>> graph = cc_example_graph >>> dsm_path = str(outdir / "dsm.svg") >>> mdm_edge_kinds = str(outdir / "mdm_edge_kinds.svg") >>> dmm_example = str(outdir / "dmm_example.svg") >>> mdm_edge_labels = str(outdir / "mdm_edge_labels.svg") >>> mdm_edge_weight_labels = str(outdir / "mdm_edge_weight_labels.svg") >>> mdm_edge_weights_1 = str(outdir / "mdm_edge_weights_1.svg") >>> mdm_edge_weights_2 = str(outdir / "mdm_edge_weights_2.svg") >>> mdm_edge_weights_3 = str(outdir / "mdm_edge_weights_3.svg") >>> mdm_tweaked_sort = str(outdir / "mdm_tweaked_sort.svg") >>> mdm_custom_sort = str(outdir / "mdm_custom_sort.svg") >>> mdm_edge_categorical_field_colors = str(outdir / "mdm_edge_categorical_field_colors.svg") >>> mdm_edge_continuous_field_colors = str(outdir / "mdm_edge_continuous_field_colors.svg") >>> mdm_custom = str(outdir / "mdm_custom.svg") >>> mdm_custom_advanced = str(outdir / "mdm_custom_advanced.svg") >>> mdm_highlight = str(outdir / "mdm_highlight.svg") >>> mdm_highlight_custom = str(outdir / "mdm_highlight_custom.svg") >>> import ragraph.plot This page covers plotting the :obj:`~ragraph.graph.Graph` object in various matrix forms using the :obj:`~ragraph.plot` module. In particular we will focus on the :obj:`~ragraph.plot.mdm` function and the :obj:`~ragraph.plot.Style` object. The :obj:`~ragraph.plot.mdm` function can be used to quickly generate multi-domain-matrix (MDM) and dependency-structure-matrix (DSM) figures of your data set. The :obj:`~ragraph.plot.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 :obj:`~ragraph.datasets` module is used. ************** Basic plotting ************** To create a plot, first the :obj:`~ragraph.plot` module has to be imported: .. doctest:: >>> import ragraph.plot Next, one can use the :obj:`~ragraph.plot.mdm` function to create an MDM figure as shown below. .. doctest:: >>> fig = ragraph.plot.mdm( ... leafs=graph.leafs, ... edges=graph.edges, ... ) >>> fig.write_image(mdm_edge_kinds) # e.g.: "mdm_edge_kinds.svg" The :obj:`~ragraph.plot.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. :numref:`DSM 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:: /_static/generated/mdm_edge_kinds.svg :alt: Product DSM of the climate control system showing dependency kinds. :name: DSM 1 :align: center 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 :obj:`~ragraph.plot.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 :obj:`~ragraph.plot.dmm` function: .. doctest:: >>> 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. :numref:`DMM 1` shows the resulting figure, in which we have more columns than rows. .. figure:: /_static/generated/dmm_example.svg :alt: Mapping matrix of climate control system. :name: DMM 1 :align: center Mapping matrix of the climate control system. .. _axis_sort: ********************* 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: 1. The hierarchy they are subject to. 2. The node kind they belong to. 3. Whether or not they are a bus node. 4. How big their "width" is in terms of leaf nodes (e.g. biggest clusters first). You can tweak this behavior like so: .. doctest:: >>> 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:: /_static/generated/mdm_tweaked_sort.svg :alt: MDM with tweaked sorting. :name: MDM tweaked sort :align: center MDM figure with automatic sorting tweaked to only reflect the hierarchy and no further optimizations. Where :obj:`sort_args` are options to be passed to :obj:`ragraph.analysis.sequence.axis`. Or you can also disable this behavior altogether and plot the (leaf) nodes as given: .. doctest:: >>> 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:: /_static/generated/mdm_custom_sort.svg :alt: MDM with custom sorting. :name: MDM custom sort :align: center MDM figure with automatic sorting disabled. Sorting is fully custom as given in the leaf list. .. _filtering: ********************* Information filtering ********************* The :obj:`~ragraph.graph.Graph` may contain :obj:`~ragraph.edge.Edge` objects of various kinds to which various labels and weights are attached. By passing along a :obj:`~ragraph.plot.Style` object to the ``style`` argument of the :obj:`~ragraph.plot.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 :obj:`~ragraph.plot.Style` object is set to be equal to ``"weight labels"``. :numref:`DSM 2` shows the resulting matrix in which the labels of the different weights attached to the edges are displayed as categories within a pie chart. .. doctest:: >>> 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:: /_static/generated/mdm_edge_weight_labels.svg :alt: Product DSM of the climate control system showing edge weight labels. :name: DSM 2 :align: center 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 :numref:`DSM 3`. Here the actual numerical values of the different weights are shown rather than the weight labels. .. doctest:: >>> 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:: /_static/generated/mdm_edge_weights_1.svg :alt: Product DSM of the climate control system showing all edge weights. :name: DSM 3 :align: center 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 :numref:`DSM 4`. Here only the numerical values of the weights ``"spatial"``, ``"energy flow"``, and ``"information flow"`` are displayed. .. doctest:: >>> 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:: /_static/generated/mdm_edge_weights_2.svg :alt: Product DSM of the climate control system showing a subset of edge weights. :name: DSM 4 :align: center 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 :numref:`DSM 5`. .. doctest:: >>> 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:: /_static/generated/mdm_edge_weights_3.svg :alt: Product DSM of the climate control system showing relative wedge sizes. :name: DSM 5 :align: center Product DSM of the climate control system showing relative wedge sizes. The examples shown in this section are far from exhaustive. Check out the :obj:`~ragraph.plot.generic.PieMapStyle` documentation for all properties that one can set within the ``piemap`` dictionary. .. _highlighting: ************************ 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 :obj:`~ragraph.node.Node` instance: .. testsetup:: >>> from copy import deepcopy >>> _graph = deepcopy(cc_example_graph) .. doctest:: >>> 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:: /_static/generated/mdm_highlight.svg :alt: Product DSM of the climate control system highlighting the Accumulator. :name: DSM with highlight :align: center Product DSM of the climate control system highlighting the Accumulator. You can override certain settings via the :obj:`~ragraph.plot.generic.Style`, too: .. testsetup:: >>> graph = _graph .. doctest:: >>> 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:: /_static/generated/mdm_highlight_custom.svg :alt: Product DSM of the climate control system showcasing all highlighting options. :name: DSM with custom highlighting :align: center 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: 1. :obj:`~ragraph.plot.generic.PieMapStyle`'s row/column annotation key is considered. 2. Only if there is no style entry for that, the :obj:`~ragraph.plot.generic.Style` highlight key is considered. Secondly, the color's precedence is always: 1. Annotation key's value if it's a string. 2. :obj:`~ragraph.plot.generic.PieMapStyle` row/column override color if set. 3. :obj:`~ragraph.plot.generic.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 :obj:`~ragraph.plot.Style` object. Here one can provide a dictionary mapping ``weight labels`` to ``color hex codes``. The result of setting these colors is shown in :numref:`DSM 6`. .. doctest:: >>> 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:: /_static/generated/mdm_edge_categorical_field_colors.svg :alt: Product DSM of the climate control system showing edge weight labels with user defined colors. :name: DSM 6 :align: center 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 :obj:`~ragraph.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. :numref:`DSM 7` shows the resulting figure. .. doctest:: >>> 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:: /_static/generated/mdm_edge_continuous_field_colors.svg :alt: Product DSM of the climate control system showing edge weights with user defined color gradients. :name: DSM 7 :align: center Product DSM of the climate control system showing edge weight labels with user defined color gradients. Check out the documentation of the :obj:`~ragraph.plot.generic.Palettes` object to check all options that can be set using the ``pallettes`` argument. .. _basic_customization: ******************* 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 :obj:`~ragraph.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 :numref:`DSM 8`. 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 :obj:`~ragraph.plot.components.Tree`, :obj:`~ragraph.plot.components.Labels`, :obj:`~ragraph.plot.components.PieMap`, and :obj:`~ragraph.plot.components.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). .. doctest:: >>> 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:: /_static/generated/mdm_custom.svg :alt: Product DSM of the climate control system showing edge kinds with a custom color gradient and numbers on the diagonal and numbers. :name: DSM 8 :align: center Product DSM of the climate control system showing edge kinds with a custom color gradient and numbers on the diagonal. .. _advanced_customization: ********************** 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 :obj:`~ragraph.plot.generic.Component` class which is the basic building block for creating compound Ploty figures. Within the ``custom_mdm`` method the grid layout of :numref:`DSM 9` is defined. That is, :numref:`DSM 9` 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 :obj:`~ragraph.plot.utils.get_subplots(` and :obj:`~ragraph.plot.utils.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 :obj:`~ragraph.plot.Style.xstep`, :obj:`~ragraph.plot.Style.ystep`, and :obj:`~ragraph.plot.Style.boxsize` properties of the :obj:`~ragraph.plot.Style` object are particularly important in matching the scaling of figures. .. doctest:: >>> 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:: /_static/generated/mdm_custom_advanced.svg :alt: Product DSM of the climate control system showing edge weights labels with custom color bars around the matrix. :name: DSM 9 :align: center Product DSM of the climate control system showing edge weights labels with custom colorbars around the matrix.