Introduction
Overview
This extension contains all the the components used to display causal graphs.
Add your first Causal Graph
Below we are going to show an example of how to add a CausalGraphViewer component.
from dara.components import CausalGraphViewer
from dara.core import ConfigurationBuilder, get_icon, ComponentInstance
from cai_causal_graph import CausalGraph
# Creates a CausalGraph
def default_causal_graph() -> CausalGraph:
# instantiate CausalGraph
cg = CausalGraph()
# Create edges
cg.add_edge('Age', 'Fraud')
cg.add_edge('Age', 'Marital Status Value=Married')
cg.add_edge('Age', 'Number of Children')
cg.add_edge('Authority Contacted', 'Fraud')
cg.add_edge('CPI', 'Salary')
cg.add_edge('Car Value', 'Fraud')
cg.add_edge('Claim Type Value=Liability', 'Total Claim')
cg.add_edge('Collision Type Value=Front', 'Claim Type Value=Liability')
cg.add_edge('Collision Type Value=Front', 'Total Claim')
cg.add_edge('Crime Rate', 'Fraud')
cg.add_edge('Education Level Value=Higher', 'Fraud')
cg.add_edge('Education Level Value=Higher', 'Occupation Value=Professional')
cg.add_edge('Gender Value=F', 'Salary')
cg.add_edge('Location Value=EC', 'Crime Rate')
cg.add_edge('Location Value=SE', 'Crime Rate')
cg.add_edge('Location Value=SW', 'Crime Rate')
cg.add_edge('Marital Status Value=Married', 'Fraud')
cg.add_edge('No-Claims Years', 'Fraud')
cg.add_edge('Number of Children', 'Fraud')
cg.add_edge('Occupation Value=Professional', 'Salary')
cg.add_edge('Previous Claims', 'Fraud')
cg.add_edge('Previous Claims Value', 'Fraud')
cg.add_edge('Salary', 'Car Value')
cg.add_edge('Salary', 'Fraud')
cg.add_edge('Total Claim', 'Fraud')
cg.add_edge('Unemployment Rate', 'Salary')
cg.add_edge('Years with License', 'Fraud')
return cg
# config
config = ConfigurationBuilder()
# adds causal graph viewer
def causal_graph_viewer_content() -> ComponentInstance:
return CausalGraphViewer(
causal_graph=default_causal_graph()
)
config.add_page(name='CausalGraph', content=causal_graph_viewer_content(), icon=get_icon('diagram-project'))
CausalGraphViewer
component
Customising Causal Graph Viewer
Rendering properties
The rendering of nodes and edges can be customised via the rendering_properties
metadata. The following properties are available:
edge.meta.rendering_properties
:
accepted: boolean
- whether edge was accepted (used by resolver component)color: string
- edge colordescription: string
- description/note displayed in side panelforced: boolean
- whether edge was forced by constraints from domain knowledgethickness: number
- edge thickness; provided values are normalized and scaled across all edge thicknesses providedtooltip: string | dict[string, string]
- extra information to display in tooltip
node.meta.rendering_properties
color: string
- node color, defaults toTheme.colors.background
for latent nodes,Theme.colors.secondary
for output nodes and toTheme.colors.blue4
for other nodeshighlight_color: string
- color used for border and selected shadow, defaults toTheme.colors.primary
label: string
- human-readable alternative label to display instead of the node namelabel_color: string
- node font colorlabel_size: string | number
- node font sizelatent: boolean
- whether the node is latent; if not provided, computed based onavailable_inputs
propertysize: number
- node radius in pixelstooltip: string | dict[string, string]
- extra information to display in tooltipx: number
- x position of nodey: number
- y position of node
The metadata can be set in the following way:
from cai_causal_graph import CausalGraph
# Creates a CausalGraph
def graph_with_meta() -> CausalGraph:
cg = CausalGraph()
cg.add_node('client_age', meta={'rendering_properties': {
'color': 'red',
'label': 'Age',
'label_color': 'pink',
'label_size': 12,
'latent': False,
'size': 10,
'tooltip': 'Age of the client'
}})
cg.add_edge('client_age', 'fraud', meta={'rendering_properties': {
'color': 'blue',
'description': 'This is a note which can also be edited via the UI',
'thickness': 1,
'tooltip': 'Connection between age and fraud'
}})
return cg
Graph Legend
By default the graph has a legend which is dependent on the current editor_mode
selected for that graph. You can gain control over these in two ways:
- You can add to the legends already present with the
additional_legends
property. This accepts an array of legends as can be seen in the example below. - Alternatively you can alter the prepopulated legends by passing
default_legends
. This accepts a dict of editor_mode to array of legends.
Legend types
There are three kinds of legends which can be added to the graph. Below you will find each kind and the properties they possess.
Legend.Spacer
Spacers can be used to create a space in between sections of your legend. For example to separate node related legends to edge related ones. What it means is that it will not display a symbol. You may still write a label for it.
label: string
- Optional label to show in the legend
Legend.Node
Displays a node symbol, this can be useful to explain what different colors of nodes might mean to an user.
label: string
- Optional label to show in the legendcolor: string
- Optional color to fill the node symbol in the legend, defaults to theme.blue4highlight_color: string
- Optional color for the node symbol rim in the legend, defaults to theme.primary
When configuring the color properties for legends, the following are supported:
Standard Colors - You can use common color formats such as named colors ('red', 'blue', etc.), hexadecimal values ('#ff0000', '#0000ff'), RGB ('rgb(255, 0, 0)'), or any other CSS-compatible color specification. This is ideal for specific, custom color requirements.
Themed Colors - These are specified with a theme.
prefix, followed by the color key from the currently active theme palette.
For example, theme.primary
, theme.secondary
, or any other predefined theme color. If the app has the light theme set these would be
the primary, secondary of the Light theme. If the app was using the dark theme, then the dark theme equivalents would have been picked up.
Or even if a custom theme was setup for the app the colors which had those names would be the ones selected by the legend.
Legend.Edge
Displays a node symbol, this can be useful to explain what different colors of nodes might mean to an user.
label: string
- Optional label to show in the legendcolor: string
- Optional color for the edge symbol in the legend, defaults to theme.grey5arrow_type: ArrowType
- One of none, normal, filled, empty, soft. It is an optional edge/arrow head to show at end of line, defaults to normalcenter_symbol: CenterSymbol
- One of none, cross, question, bidirected. It is an optional symbol to show at the center of the edge, defaults to nonedash_array: string
- Optional stroke-dasharray SVG path property - line will be dashed if specified
Legend Examples
additional_legends
Example
from dara.core.configuration import ConfigurationBuilder
from cai_causal_graph import CausalGraph
from dara.core.visual.themes.light import Light
from dara.components import CausalGraphViewer
from dara.components.graphs.definitions import Legend, ArrowType, CenterSymbol
# Create a causal graph with nodes of different colors
cg = CausalGraph()
cg.add_node(
'A', meta={'rendering_properties': {'color': Light.colors.warning, 'highlight_color': Light.colors.warningDown}}
)
cg.add_node(
'B', meta={'rendering_properties': {'color': Light.colors.success, 'highlight_color': Light.colors.successDown}}
)
cg.add_edge('A', 'B')
cg.add_edge('B', 'C')
config = ConfigurationBuilder()
config.add_page(
name='Legends',
content=CausalGraphViewer(
causal_graph=cg,
# Add additional legends to the graph
additional_legends=[
Legend.Spacer(),
# You can define the colors just as you would in any other component
Legend.Node(color=Light.colors.warning, label='Warning Node', highlight_color=Light.colors.warningDown),
# Alternative you can also directly pass a theme. to get them
Legend.Node(color='theme.success', label='Success Node', highlight_color='theme.successDown'),
Legend.Spacer(),
Legend.Edge(
arrow_type=ArrowType.FILLED, center_symbol=CenterSymbol.QUESTION, label='Directed', color='theme.plum'
),
],
),
)
default_legends
Example
Default legends is structured slightly differently. By default it takes a dict of key EditorMode and legends array value. This is so the correct default
legends appear for whichever editor_mode
is set by the user.
When updating these however you may only care for overwriting it for the editor_mode
you use. An example of this can be found below:
from dara.core.configuration import ConfigurationBuilder
from cai_causal_graph import CausalGraph
from dara.core.visual.themes.light import Light
from dara.components import CausalGraphViewer, EditorMode
from dara.components.graphs.definitions import Legend
# Create a causal graph with nodes of different colors
cg = CausalGraph()
cg.add_node(
'A', meta={'rendering_properties': {'color': Light.colors.warning, 'highlight_color': Light.colors.warningDown}}
)
cg.add_node(
'B', meta={'rendering_properties': {'color': Light.colors.success, 'highlight_color': Light.colors.successDown}}
)
cg.add_edge('A', 'B')
cg.add_edge('B', 'C')
config = ConfigurationBuilder()
config.add_page(
name='Legends',
content=CausalGraphViewer(
causal_graph=cg,
# override the default legends with your own
default_legends={
EditorMode.DEFAULT: [
# You can define the colors just as you would in any other component
Legend.Node(color=Light.colors.warning, label='Warning Node', highlight_color=Light.colors.warningDown),
# Alternative you can also directly pass a theme. to get them
Legend.Node(color='theme.success', label='Success Node', highlight_color='theme.successDown'),
]
},
),
)
Edge thickness
One of the rendering properties is thickness
. This property is used to scale the displayed strength of the edges. When set on more
than two edges, the values provided for thickness
are normalized and scaled across all edges with a thickness provided.
from cai_causal_graph import CausalGraph
# Creates a CausalGraph
def graph_with_meta() -> CausalGraph:
cg = CausalGraph()
cg.add_edge('client_age', 'fraud', meta={'rendering_properties': {'thickness': 1}})
cg.add_edge('client_age', 'marital_status', meta={'rendering_properties': {'thickness': 2}})
cg.add_edge('client_age', 'number_of_children', meta={'rendering_properties': {'thickness': 3}})
cg.add_edge('client_age', 'occupation', meta={'rendering_properties': {'thickness': 4}})
cg.add_edge('client_age', 'salary', meta={'rendering_properties': {'thickness': 5}})
cg.add_edge('client_age', 'total_claim', meta={'rendering_properties': {'thickness': 6}})
cg.add_edge('client_age', 'unemployment_rate', meta={'rendering_properties': {'thickness': 7}})
return cg
CausalGraphViewer
with edge thicknesses
Graph Layouts
The default layout you have seen in the previous examples is the FcoseLayout
. Layouts are available as classes with customizable properties.
You can read more about fCoSE
and more available layouts in the GraphLayout and the layout docstrings.
You can set the layout by providing a layout instance as the graph_layout
property of CausalGraphViewer
:
from dara.components import CausalGraphViewer
from dara.components.graph_layout import PlanarLayout
CausalGraphViewer(
causal_graph=default_causal_graph(),
graph_layout=PlanarLayout()
)
CausalGraphViewer
with PlanarLayout
In particular, you can provide your own layout by setting the x
and y
properties of each node in the rendering_properties
metadata.
Here's an example of using a custom layout using the networkx
library and pygraphviz
:
import networkx
cg = CausalGraph()
# ... add edges and nodes to cg
# Compute positions wih pygraphviz
cg_nx = cg.to_networkx()
nx_layout = networkx.nx_agraph.graphviz_layout(cg_nx)
# Update metadata to include the positions
scaling_factor = 5 # the layout needs to account for node sizes so we have to scale it
for node, positions in nx_layout.items():
x, y = positions
node = cg.get_node(node)
node.meta = {
'rendering_properties': {
'x': x * scaling_factor,
'y': y * scaling_factor
}
}
This can be used with the CustomLayout
class to provide a fixed layout which can be restored upon pressing the Recalculate Layout
button.
When used with other layouts, the positions from metatdata are used directly and the specified layout algorithm is not ran on the first render.
Further recalculations of the layout will use positions provided by the specified layout algorithm.
Note that third-party layout algorithms might not account for node sizes, so you might have to scale the positions accordingly unless they provide a way to specify the node size.
Defining tiers in graphs
The following layouts support tiers: FcoseLayout
, PlanarLayout
, SpringLayout
and MarketingLayout
.
This allows one to set which nodes should appear at the same level, your are also able to set which order the nodes appear within it and the hierarchy of each tier.
A way of setting tiers is to pass it as an array of array of nodes, the inner arrays define the order in which the tiers appear:
from dara.components import CausalGraphViewer, FcoseLayout
from cai_causal_graph import CausalGraph
cg = CausalGraph()
cg.add_edge('A', 'C')
cg.add_edge('B', 'D')
cg.add_edge('C', 'E')
cg.add_edge('D', 'E')
tiers = [['A', 'B'], ['C', 'D'], ['E']]
CausalGraphViewer(
causal_graph=cg,
graph_layout=FcoseLayout(
tiers=tiers,
orientation='vertical'
),
)
Alternatively you can pass the tiers based on some node property with the use of TiersConfig
For TiersConfig
:
- you must pass a
group
, the group defines the path to the node property which contains the name of the tier they belong to. In the below example we define this to be a property called group within the meta of the node, so the path ismeta.group
- you may also define the
rank
, this is a list of group names defining the hierarchy of the tiers. - you may pass the
order_nodes_by
, this is a path to the node property containing a number which defines the order in which the nodes should be displayed within a tier.
from dara.components import CausalGraphViewer, FcoseLayout, TiersConfig
from cai_causal_graph import CausalGraph
cg = CausalGraph()
cg.add_node(identifier='A', meta={'group': 'first', order: 2})
cg.add_node(identifier='B', meta={'group': 'first', order: 1})
cg.add_node(identifier='C', meta={'group': 'second'})
cg.add_node(identifier='D', meta={'group': 'second'})
cg.add_node(identifier='E', meta={'group': 'third'})
cg.add_edge('A', 'C')
cg.add_edge('B', 'D')
cg.add_edge('C', 'E')
cg.add_edge('D', 'E')
CausalGraphViewer(
causal_graph=cg,
graph_layout=FcoseLayout(
tiers=TiersConfig(group='meta.group', rank=['first', 'second', 'third'], order_nodes_by='meta.order'),
),
)
For the PlanarLayout
we utilize the sugyiama algorithm from d3-dag library.
This approach has currently some limitations and for some configurations it may be unable to resolve the tiers/layers.
If that happens your graph will be automatically switched to use FcoseLayout
and a warning will be displayed in your app.
Defining groups/clusters in graphs
The following layouts support grouping: FcoseLayout
, and SpringLayout
.
This allows one to set which nodes should appear in groups together, these groups can be collapsed or expanded. It is a good way to simplify large and complex graphs for an overall as well as detailed view.
Similarly to tiers, the grouping is some property of the node, and can be defined as a path to where that property exists. Below is a code example showcasing this:
from dara.components import CausalGraphViewer, FcoseLayout
from cai_causal_graph import CausalGraph
cg = CausalGraph()
cg.add_node(identifier='A', meta={'group': 'group1'})
cg.add_node(identifier='B', meta={'group': 'group1'})
cg.add_node(identifier='C', meta={'group': 'group2'})
cg.add_node(identifier='D', meta={'group': 'group2'})
cg.add_node(identifier='E', meta={'group': 'group3'})
cg.add_edge('A', 'C')
cg.add_edge('B', 'D')
cg.add_edge('C', 'E')
cg.add_edge('D', 'E')
CausalGraphViewer(
causal_graph=cg,
graph_layout=FcoseLayout(
group='meta.group',
),
)
When utilizing SpringLayout
, there is also one more choice, which is to set group_repel_strength
.
This defaults to 2000 and is the force that separates the groups/clusters. So if for example the graph groups appears too crammed
this can be increased. If on the other hand it is too spaced out, this value can be set to something lower.
CausalGraphViewer(
causal_graph=cg,
graph_layout=SpringLayout(group='meta.group', group_repel_strength=5000),
),
Editing the graph
While the CausalGraphViewer
component can be used as a view-only component, it can also be used for interactive flows by passing editable=True
.
When provided with a Variable
containing a CausalGraph
instance, it will update the graph in the Variable
whenever the user makes changes to the graph.
When provided with a DerivedVariable
, the graph will not updated it as DerivedVariable
s are read-only. You can either:
- use the
on_update
callback to get notified of changes to the graph and save the updated state to another variable
from cai_causal_graph import CausalGraph
from dara.components import CausalGraphViewer
from dara.core import Variable, DerivedVariable
def compute_cg():
return CausalGraph(...)
# read-only computed causal graph
cg_dv = DerivedVariable(compute_cg, variables=[])
# writable variable to store the updated graph
cg_copy = Variable()
CausalGraphViewer(
causal_graph=cg_dv,
editable=True,
on_update=cg_copy.sync()
)
- use the
Variable.create_from_derived
API to create a writable variable from a read-only variable. This copy will be in sync with the read-only variable until the user makes changes to the graph. You can read more about it in the interactivity documentation.
from cai_causal_graph import CausalGraph
def compute_cg():
return CausalGraph(...)
# read-only computed causal graph
cg_dv = DerivedVariable(compute_cg, variables=[])
# writable variable to store the updated graph
cg_copy = Variable.create_from_derived(cg_dv)
CausalGraphViewer(
causal_graph=cg_copy,
editable=True
)
Interactivity
In order to interact with the causal graph, you can provide on_click_node
and on_click_edge
event handlers in order to trigger actions upon clicking on an edge or a node. The following example
demonstrates how to use the on_click_node
and on_click_edge
event handlers to update a variable
with the name of the clicked node or edge.
from dara.core import Variable, py_component, action
from dara.components import CausalGraphViewer, Stack, Text
from dara.components.graphs.graph_layout import PlanarLayout
from cai_causal_graph import CausalGraph
selected_node = Variable(None)
selected_edge = Variable(None)
causal_graph = CausalGraph()
causal_graph.add_edge('A', 'B')
causal_graph.add_edge('B', 'C')
causal_graph.add_edge('A', 'C')
@action
async def resolver_on_click_node(ctx: action.Ctx):
value = ctx.input.get('identifier') if isinstance(ctx.input, dict) else None
await ctx.update(variable=selected_node, value=value)
@action
async def resolver_on_click_edge(ctx: action.Ctx):
await ctx.update(variable=selected_edge, value=f"{ctx.input.get('source')} -> {ctx.input.get('destination')}")
@py_component
def display(selected_node, selected_edge):
return Stack(
Text(f"Selected Node: {selected_node}"),
Text(f"Selected Edge: {selected_edge}"),
)
Stack(
CausalGraphViewer(
causal_graph=causal_graph,
graph_layout=PlanarLayout(),
on_click_node=resolver_on_click_node(),
on_click_edge=resolver_on_click_edge(),
),
display(selected_node, selected_edge),
)
Skeleton
You can also display the Skeleton of your CausalGraph
this is simply
a representation of a graph where all the edges are undirected.
from dara.core.configuration import ConfigurationBuilder
from cai_causal_graph import CausalGraph
from dara.components import CausalGraphViewer
# create a causal graph
cg = CausalGraph()
cg.add_edge('A', 'B')
cg.add_edge('B', 'C')
cg.add_edge('C', 'D')
cg.add_edge('D', 'E')
cg.add_edge('B', 'D')
# get its Skeleton instance
skeleton = cg.skeleton
config = ConfigurationBuilder()
config.add_page(name='Skeleton Example', content=CausalGraphViewer(causal_graph=skeleton))
TimeSeriesCausalGraph
If a TimeSeriesCausalGraph
is detected and the layout chosen supports it, then by default they will use time_lag
and variable_name
to place these
nodes within the same tier. You can override this behaviour by setting tiers to something else.
The layouts that currently support TimeSeriesCausalGraph
are: FcoseLayout
, SpringLayout
and MarketingLayout
.
from dara.core.configuration import ConfigurationBuilder
from dara.components import CausalGraphViewer, FcoseLayout
from cai_causal_graph import TimeSeriesCausalGraph
cg_time = TimeSeriesCausalGraph()
cg_time.add_edge('X1 lag(n=1)', 'X1')
cg_time.add_edge('X2 lag(n=1)', 'X2')
cg_time.add_edge('X1', 'X3')
cg_time.add_edge('X2', 'X3')
config = ConfigurationBuilder()
config.add_page(
name='Time Series Example',
content=CausalGraphViewer(causal_graph=cg_time, graph_layout=FcoseLayout(orientation='vertical')),
)