Introduction
Overview
This extension contains all the the components used to display causal graphs.
Getting Started
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
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.
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),
)