From 7ce1d59296ed63c60c716f9a1ba41a1f4651e62e Mon Sep 17 00:00:00 2001
From: Duncan Murdoch
+
@@ -259,6 +263,9 @@
+
diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 9e49991..7e5f501 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -3421,6 +3421,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced @@ -4452,7 +4475,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ @@ -6574,7 +6599,6 @@ var DAGittyGraphView = Class.extend({ clickHandler : function(e){ // click handler can be set to emulate keypress action // using this function - console.log(" click handler called on canvas" ) this.last_click_x = this.pointerX(e)-this.getContainer().offsetLeft this.last_click_y = this.pointerY(e)-this.getContainer().offsetTop this.last_click_g_coords = this.toGraphCoordinate( this.last_click_x, this.last_click_y ) @@ -6626,6 +6650,9 @@ var DAGittyGraphView = Class.extend({ e.preventDefault() } break + case 83: //s + if(v) this.toggleVertexProperty(v,"selectedNode") + break case 85: //u if(v) this.toggleVertexProperty(v,"latentNode") break @@ -6903,6 +6930,7 @@ var DAGittyGraphView = Class.extend({ var g,i,c var g_causal = new Graph() var g_bias = new Graph() + var g_selection_bias = new Graph() var g_trr = new Graph() var g_an = GraphTransformer.ancestorGraph( @@ -6923,6 +6951,13 @@ var DAGittyGraphView = Class.extend({ case "equivalence" : g = GraphTransformer.dagToCpdag( this.getGraph() ) break + case "causalodds": + g = this.getGraph() + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ + g_causal = GraphTransformer.causalFlowGraph(g) + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + } + break default: g = this.getGraph() g_trr = GraphTransformer.transitiveReduction( g ) @@ -6961,6 +6996,8 @@ var DAGittyGraphView = Class.extend({ vertex_type = "outcome" } else if( g.isAdjustedNode(vv[i]) ){ vertex_type = "adjusted" + } else if( g.isSelectedNode(vv[i]) ){ + vertex_type = "selected" } else if( g.isLatentNode(vv[i]) ){ vertex_type = "latent" } else if( ean_ids[vv[i].id] @@ -6970,8 +7007,7 @@ var DAGittyGraphView = Class.extend({ vertex_type = "anexposure" } else if( oan_ids[vv[i].id] ){ vertex_type = "anoutcome" - } - + } this.impl.createVertexShape( vertex_type, vs ) this.vertex_shapes.set( vv[i].id, vs ) } diff --git a/gui/js/example-dags.js b/gui/js/example-dags.js index 5bc3ce1..b4f87fb 100644 --- a/gui/js/example-dags.js +++ b/gui/js/example-dags.js @@ -342,6 +342,30 @@ e:"e0 x\n"+ "PER DET\n", l: "van Kampen, 2014" -} +}, +{ + "d": ` +dag { +Age [adjusted,pos="-1.973,-0.123"] +HRT [exposure,pos="-0.536,-0.016"] +Occ [pos="-1.645,0.432"] +S [selected,pos="0.925,-0.500"] +Smo [pos="-0.879,0.441"] +TCI [outcome,pos="0.488,0.090"] +Thist [pos="-0.569,1.027"] +Age -> HRT +Age -> Occ +Age -> S [pos="-0.945,-0.571"] +Age -> TCI [pos="-0.264,-0.456"] +HRT -> TCI +Occ -> Smo +Occ -> Thist +Smo -> HRT +Smo -> TCI +TCI -> S +Thist -> TCI +}`, + l: "Didelez et al, 2010" + } ]; diff --git a/gui/js/main.js b/gui/js/main.js index 952690a..5a2e701 100644 --- a/gui/js/main.js +++ b/gui/js/main.js @@ -75,6 +75,7 @@ var GUI = { document.getElementById("variable_label").innerText = vid document.getElementById("variable_exposure").checked = Model.dag.isSource(vid) document.getElementById("variable_outcome").checked = Model.dag.isTarget(vid) + document.getElementById("variable_selected").checked = Model.dag.isSelectedNode(vid) document.getElementById("variable_adjusted").checked = Model.dag.isAdjustedNode(vid) document.getElementById("variable_unobserved").checked = Model.dag.isLatentNode( vid ) }, @@ -212,6 +213,12 @@ function setsToHTML( sets ){ } function causalEffectEstimates(){ + if( Model.dag.getSelectedNodes().length > 0 ){ + displayCausalMsg("I cannot determine causal effects for DAGs with selection nodes."); return + } + if( GraphAnalyzer.containsCycle( Model.dag ) ){ + displayCausalMsg("I cannot determine causal effects for cyclic models."); return + } switch( document.getElementById("causal_effect_kind").value ){ case "adj_total" : displayAdjustmentInfo("total"); break @@ -248,6 +255,11 @@ function msasToHtml( msas ){ } } +function displayCausalMsg( wh ){ + document.getElementById("causal_effect").innerHTML + = "
"+wh+"
"; +} + function displayAdjustmentInfo( kind ){ var adjusted_nodes = Model.dag.getAdjustedNodes(); var html_adjustment = ""; diff --git a/gui/js/styles/original.js b/gui/js/styles/original.js index 8d4c3a4..8b4c0ab 100644 --- a/gui/js/styles/original.js +++ b/gui/js/styles/original.js @@ -9,6 +9,7 @@ DAGitty.stylesheets.original = { exposurenode : { fill : "#bed403", stroke : "#000000" }, outcomenode : { fill : "#00a2e0", stroke : "#000000" }, adjustednode : { fill : "#ffffff", stroke : "#000000" }, + selectednode : { 'd' : 'M [rx], 0 L [rx],[ry] L -[rx],[ry] L -[rx],-[ry] L [rx],-[ry] Z' }, latentnode : { fill : "#dddddd", stroke: "#aaaaaa" /*"stroke-dasharray" : "5,5" */}, confoundernode : { fill : "#ff7777", stroke : "#ff7777" }, anexposurenode : { fill : "#bed403", stroke : "#bed403" }, diff --git a/jslib/graph/GraphTransformer.js b/jslib/graph/GraphTransformer.js index de2e2de..3aa4934 100644 --- a/jslib/graph/GraphTransformer.js +++ b/jslib/graph/GraphTransformer.js @@ -253,6 +253,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced diff --git a/jslib/graph/ObservedGraph.js b/jslib/graph/ObservedGraph.js index 1b03beb..9b8d853 100644 --- a/jslib/graph/ObservedGraph.js +++ b/jslib/graph/ObservedGraph.js @@ -68,7 +68,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ diff --git a/jslib/gui/GraphGUI_View.js b/jslib/gui/GraphGUI_View.js index 7068632..612c388 100644 --- a/jslib/gui/GraphGUI_View.js +++ b/jslib/gui/GraphGUI_View.js @@ -179,7 +179,6 @@ var DAGittyGraphView = Class.extend({ clickHandler : function(e){ // click handler can be set to emulate keypress action // using this function - console.log(" click handler called on canvas" ) this.last_click_x = this.pointerX(e)-this.getContainer().offsetLeft this.last_click_y = this.pointerY(e)-this.getContainer().offsetTop this.last_click_g_coords = this.toGraphCoordinate( this.last_click_x, this.last_click_y ) @@ -231,6 +230,9 @@ var DAGittyGraphView = Class.extend({ e.preventDefault() } break + case 83: //s + if(v) this.toggleVertexProperty(v,"selectedNode") + break case 85: //u if(v) this.toggleVertexProperty(v,"latentNode") break @@ -508,6 +510,7 @@ var DAGittyGraphView = Class.extend({ var g,i,c var g_causal = new Graph() var g_bias = new Graph() + var g_selection_bias = new Graph() var g_trr = new Graph() var g_an = GraphTransformer.ancestorGraph( @@ -528,6 +531,13 @@ var DAGittyGraphView = Class.extend({ case "equivalence" : g = GraphTransformer.dagToCpdag( this.getGraph() ) break + case "causalodds": + g = this.getGraph() + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ + g_causal = GraphTransformer.causalFlowGraph(g) + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + } + break default: g = this.getGraph() g_trr = GraphTransformer.transitiveReduction( g ) @@ -566,6 +576,8 @@ var DAGittyGraphView = Class.extend({ vertex_type = "outcome" } else if( g.isAdjustedNode(vv[i]) ){ vertex_type = "adjusted" + } else if( g.isSelectedNode(vv[i]) ){ + vertex_type = "selected" } else if( g.isLatentNode(vv[i]) ){ vertex_type = "latent" } else if( ean_ids[vv[i].id] @@ -575,8 +587,7 @@ var DAGittyGraphView = Class.extend({ vertex_type = "anexposure" } else if( oan_ids[vv[i].id] ){ vertex_type = "anoutcome" - } - + } this.impl.createVertexShape( vertex_type, vs ) this.vertex_shapes.set( vv[i].id, vs ) } diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 44db4cd..e1f0cef 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -3416,6 +3416,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced @@ -4447,7 +4470,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ From 2b279cfb0779151bc5b249591acff76622478dfd Mon Sep 17 00:00:00 2001 From: Johannes TextorExposure and outcome nodes are ignored for TreeID."+ + "Instead, TreeID tests identifiability of every direct effect (path coefficient) "+ + "simultaneously.
"] document.getElementById("causal_effect").innerHTML = ""+warnings[0]+"
" } var tid diff --git a/jslib/graph/GraphAnalyzer.js b/jslib/graph/GraphAnalyzer.js index 156ac32..a1ec2f0 100644 --- a/jslib/graph/GraphAnalyzer.js +++ b/jslib/graph/GraphAnalyzer.js @@ -2126,7 +2126,7 @@ var GraphAnalyzer = { } var res = {"results": IDobject} - if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. The algorithm assumes all nodes except a certain root node have exactly one parent, and ignores nodes with more parents"] + if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. TreeID assumes all nodes except one (the root) have exactly one parent, and ignores nodes with more parents."] return res } } diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 0ac3cf5..74ea336 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -3106,7 +3106,7 @@ var GraphAnalyzer = { } var res = {"results": IDobject} - if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. The algorithm assumes all nodes except a certain root node have exactly one parent, and ignores nodes with more parents"] + if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. TreeID assumes all nodes except one (the root) have exactly one parent, and ignores nodes with more parents."] return res } } From 0686b40e007919c949caa6e98706389f0f68a62a Mon Sep 17 00:00:00 2001 From: Johannes Textor+