{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}{- |
Module : Data.GraphViz.Types.Graph
Description : A graph-like representation of Dot graphs.
Copyright : (c) Ivan Lazar Miljenovic
License : 3-Clause BSD-style
Maintainer : Ivan.Miljenovic@gmail.com
It is sometimes useful to be able to manipulate a Dot graph /as/ an
actual graph. This representation lets you do so, using an
inductive approach based upon that from FGL (note that 'DotGraph'
is /not/ an instance of the FGL classes due to having the wrong
kind). Note, however, that the API is not as complete as proper
graph implementations.
For purposes of manipulation, all edges are found in the root graph
and not in a cluster; as such, having 'EdgeAttrs' in a cluster's
'GlobalAttributes' is redundant.
Printing is achieved via "Data.GraphViz.Types.Canonical" (using
'toCanonical') and parsing via "Data.GraphViz.Types.Generalised"
(so /any/ piece of Dot code can be parsed in).
This representation doesn't allow non-cluster sub-graphs. Also, all
clusters /must/ have a unique identifier. For those functions (with
the exception of 'DotRepr' methods) that take or return a \"@Maybe
GraphID@\", a value of \"@Nothing@\" refers to the root graph; \"@Just
clust@\" refers to the cluster with the identifier \"@clust@\".
You would not typically explicitly create these values, instead
converting existing Dot graphs (via 'fromDotRepr'). However, one
way of constructing the sample graph would be:
> setID (Str "G")
> . setStrictness False
> . setIsDirected True
> . setClusterAttributes (Int 0) [GraphAttrs [style filled, color LightGray, textLabel "process #1"], NodeAttrs [style filled, color White]]
> . setClusterAttributes (Int 1) [ GraphAttrs [textLabel "process #2", color Blue], NodeAttrs [style filled]]
> $ composeList [ Cntxt "a0" (Just $ Int 0) [] [("a3",[]),("start",[])] [("a1",[])]
> , Cntxt "a1" (Just $ Int 0) [] [] [("a2",[]),("b3",[])]
> , Cntxt "a2" (Just $ Int 0) [] [] [("a3",[])]
> , Cntxt "a3" (Just $ Int 0) [] [("b2",[])] [("end",[])]
> , Cntxt "b0" (Just $ Int 1) [] [("start",[])] [("b1",[])]
> , Cntxt "b1" (Just $ Int 1) [] [] [("b2",[])]
> , Cntxt "b2" (Just $ Int 1) [] [] [("b3",[])]
> , Cntxt "b3" (Just $ Int 1) [] [] [("end",[])]
> , Cntxt "end" Nothing [shape MSquare] [] []
> , Cntxt "start" Nothing [shape MDiamond] [] []]
-}moduleData.GraphViz.Types.Graph(DotGraph,GraphID(..),Context(..)-- * Conversions,toCanonical,unsafeFromCanonical,fromDotRepr-- * Graph information,isEmpty,hasClusters,isEmptyGraph,graphAttributes,parentOf,clusterAttributes,foundInCluster,attributesOf,predecessorsOf,successorsOf,adjacentTo,adjacent-- * Graph construction,mkGraph,emptyGraph,(&),composeList,addNode,DotNode(..),addDotNode,addEdge,DotEdge(..),addDotEdge,addCluster,setClusterParent,setClusterAttributes-- * Graph deconstruction,decompose,decomposeAny,decomposeList,deleteNode,deleteAllEdges,deleteEdge,deleteDotEdge,deleteCluster,removeEmptyClusters)whereimportData.GraphViz.TypesimportqualifiedData.GraphViz.Types.CanonicalasCimportqualifiedData.GraphViz.Types.GeneralisedasGimportData.GraphViz.Types.Common(partitionGlobal)importqualifiedData.GraphViz.Types.StateasStimportData.GraphViz.Attributes.SameimportData.GraphViz.Attributes.Complete(Attributes)importData.GraphViz.Util(groupSortBy,groupSortCollectBy)importData.GraphViz.Algorithms(CanonicaliseOptions(..),canonicaliseOptions)importData.GraphViz.Algorithms.ClusteringimportData.GraphViz.Printing(PrintDot(..))importData.GraphViz.Parsing(ParseDot(..))importData.List(foldl',delete,unfoldr)importqualifiedData.FoldableasFimportData.Maybe(mapMaybe,fromMaybe)importqualifiedData.MapasMimportData.Map(Map)importqualifiedData.SetasSimportqualifiedData.SequenceasSeqimportControl.Arrow((***))importControl.Monad(liftM,liftM2)importText.Read(Lexeme(Ident),lexP,parens,readPrec)importText.ParserCombinators.ReadPrec(prec)-- ------------------------------------------------------------------------------- | A Dot graph that allows graph operations on it.dataDotGraphn=DG{strictGraph::!Bool,directedGraph::!Bool,graphAttrs::!GlobAttrs,graphID::!(MaybeGraphID),clusters::!(MapGraphIDClusterInfo),values::!(NodeMapn)}deriving(Eq,Ord)-- | It should be safe to substitute 'unsafeFromCanonical' for-- 'fromCanonical' in the output of this.instance(Ordn,Shown)=>Show(DotGraphn)whereshowsPrecddg=showParen(d>10)$showString"fromCanonical ".shows(toCanonicaldg)-- | If the graph is the output from 'show', then it should be safe to-- substitute 'unsafeFromCanonical' for 'fromCanonical'.instance(Ordn,Readn)=>Read(DotGraphn)wherereadPrec=parens.prec10$doIdent"fromCanonical"<-lexPcdg<-readPrecreturn$fromCanonicalcdgdataGlobAttrs=GA{graphAs::!SAttrs,nodeAs::!SAttrs,edgeAs::!SAttrs}deriving(Eq,Ord,Show,Read)dataNodeInfon=NI{_inCluster::!(MaybeGraphID),_attributes::!Attributes,_predecessors::!(EdgeMapn),_successors::!(EdgeMapn)}deriving(Eq,Ord,Show,Read)dataClusterInfo=CI{parentCluster::!(MaybeGraphID),clusterAttrs::!GlobAttrs}deriving(Eq,Ord,Show,Read)typeNodeMapn=Mapn(NodeInfon)typeEdgeMapn=Mapn[Attributes]-- | The decomposition of a node from a dot graph. Any loops should-- be found in 'successors' rather than 'predecessors'. Note also-- that these are created/consumed as if for /directed/ graphs.dataContextn=Cntxt{node::!n-- | The cluster this node can be found in;-- @Nothing@ indicates the node can be-- found in the root graph.,inCluster::!(MaybeGraphID),attributes::!Attributes,predecessors::![(n,Attributes)],successors::![(n,Attributes)]}deriving(Eq,Ord,Show,Read)adjacent::Contextn->[DotEdgen]adjacentc=mapU(flipDotEdgen)(predecessorsc)++mapU(DotEdgen)(successorsc)wheren=nodecmapU=map.uncurryemptyGraph::DotGraphnemptyGraph=DG{strictGraph=False,directedGraph=True,graphID=Nothing,graphAttrs=emptyGA,clusters=M.empty,values=M.empty}emptyGA::GlobAttrsemptyGA=GAS.emptyS.emptyS.empty-- ------------------------------------------------------------------------------- Construction-- | Merge the 'Context' into the graph. Assumes that the specified-- node is not in the graph but that all endpoints in the-- 'successors' and 'predecessors' (with the exception of loops)-- are. If the cluster is not present in the graph, then it will be-- added with no attributes with a parent of the root graph.---- Note that @&@ and @'decompose'@ are /not/ quite inverses, as this-- function will add in the cluster if it does not yet exist in the-- graph, but 'decompose' will not delete it.(&)::(Ordn)=>Contextn->DotGraphn->DotGraphn(Cntxtnmcaspsss)&dg=withValuesmergedg'whereps'=toMappsps''=M.deletenps'ss'=toMapssss''=M.deletenss'dg'=addNodenmcasdgmerge=addSuccnps''.addPrednss''.M.adjust(\ni->ni{_predecessors=ps',_successors=ss'})ninfixr5&-- | Recursively merge the list of contexts.---- > composeList = foldr (&) emptyGraphcomposeList::(Ordn)=>[Contextn]->DotGraphncomposeList=foldr(&)emptyGraphaddSucc::(Ordn)=>n->EdgeMapn->NodeMapn->NodeMapnaddSucc=addPSniSuccaddPred::(Ordn)=>n->EdgeMapn->NodeMapn->NodeMapnaddPred=addPSniPredaddPS::(Ordn)=>((EdgeMapn->EdgeMapn)->NodeInfon->NodeInfon)->n->EdgeMapn->NodeMapn->NodeMapnaddPSfnitfasnm=t`seq`foldl'addSucc'nmfas'wherefas'=fromMapfasaddSucc'nm'(f,as)=f`seq`M.alter(addSas)fnm'addSas=Just.maybe(error"Node not in the graph!")(fni(M.insertWith(++)t[as]))-- | Add a node to the current graph. Throws an error if the node-- already exists in the graph.---- If the specified cluster does not yet exist in the graph, then it-- will be added (as a sub-graph of the overall graph and no-- attributes).addNode::(Ordn)=>n->MaybeGraphID-- ^ The cluster the node can be found in-- (@Nothing@ refers to the root graph).->Attributes->DotGraphn->DotGraphnaddNodenmcasdg|n`M.member`ns=error"Node already exists in the graph"|otherwise=addEmptyClustermc$dg{values=ns'}wherens=valuesdgns'=M.insertn(NImcasM.emptyM.empty)ns-- | A variant of 'addNode' that takes in a DotNode (not in a-- cluster).addDotNode::(Ordn)=>DotNoden->DotGraphn->DotGraphnaddDotNode(DotNodenas)=addNodenNothingas-- | Add the specified edge to the graph; assumes both node values are-- already present in the graph. If the graph is undirected then-- the order of nodes doesn't matter.addEdge::(Ordn)=>n->n->Attributes->DotGraphn->DotGraphnaddEdgeftas=withValuesmergewhere-- Add the edge assuming it's directed; let the getter functions-- be smart regarding directedness.merge=addPredt(M.singletonf[as]).addSuccf(M.singletont[as])-- | A variant of 'addEdge' that takes a 'DotEdge' value.addDotEdge::(Ordn)=>DotEdgen->DotGraphn->DotGraphnaddDotEdge(DotEdgeftas)=addEdgeftas-- | Add a new cluster to the graph; throws an error if the cluster-- already exists. Assumes that it doesn't match the identifier of-- the overall graph. If the parent cluster doesn't already exist-- in the graph then it will be added.addCluster::GraphID-- ^ The identifier for this cluster.->MaybeGraphID-- ^ The parent of this cluster-- (@Nothing@ refers to the root-- graph)->[GlobalAttributes]->DotGraphn->DotGraphnaddClustercmpgasdg|c`M.member`cs=error"Cluster already exists in the graph"|otherwise=addEmptyClustermp$dg{clusters=M.insertccics}wherecs=clustersdgci=CImp$toGlobAttrsgas-- Used to make sure that the parent cluster existsaddEmptyCluster::MaybeGraphID->DotGraphn->DotGraphnaddEmptyCluster=maybeid(withClusters.flipdontReplacedefCI)wheredontReplace=M.insertWith(constid)defCI=CINothingemptyGA-- | Specify the parent of the cluster; adds both in if not already present.setClusterParent::GraphID->MaybeGraphID->DotGraphn->DotGraphnsetClusterParentcp=withClusters(M.adjustsetPc).addCswhereaddCs=addEmptyClusterp.addEmptyCluster(Justc)setPci=ci{parentCluster=p}-- | Specify the attributes of the cluster; adds it if not already-- present.setClusterAttributes::GraphID->[GlobalAttributes]->DotGraphn->DotGraphnsetClusterAttributescgas=withClusters(M.adjustsetAsc).addEmptyCluster(Justc)wheresetAsci=ci{clusterAttrs=toGlobAttrsgas}-- | Create a graph with no clusters.mkGraph::(Ordn)=>[DotNoden]->[DotEdgen]->DotGraphnmkGraphnses=flip(foldl'(flipaddDotEdge))es$foldl'(flipaddDotNode)emptyGraphns-- | Convert this DotGraph into canonical form. All edges are found-- in the outer graph rather than in clusters.toCanonical::(Ordn)=>DotGraphn->C.DotGraphntoCanonicaldg=C.DotGraph{C.strictGraph=strictGraphdg,C.directedGraph=directedGraphdg,C.graphID=graphIDdg,C.graphStatements=stmts}wherestmts=C.DotStmts{C.attrStmts=fromGlobAttrs$graphAttrsdg,C.subGraphs=cs,C.nodeStmts=ns,C.edgeStmts=getEdgeInfoFalsedg}cls=clustersdgpM=clusterPath'dgclustAs=maybe[](fromGlobAttrs.clusterAttrs).(`M.lookup`cls)lns=map(\(n,ni)->(n,(_inClusterni,_attributesni))).M.assocs$valuesdg(cs,ns)=clustersToNodespathOfidclustAssndlnspathOf(n,(c,as))=pathFromc(n,as)pathFromcln=F.foldrC(Nln).fromMaybeSeq.empty$(`M.lookup`pM)=<<c-- ------------------------------------------------------------------------------- Deconstruction-- | A partial inverse of @'&'@, in that if a node exists in a graph-- then it will be decomposed, but will not remove the cluster that-- it was in even if it was the only node in that cluster.decompose::(Ordn)=>n->DotGraphn->Maybe(Contextn,DotGraphn)decomposendg|n`M.notMember`ns=Nothing|otherwise=Just(c,dg')wherens=valuesdg(Just(NImcaspsss),ns')=M.updateLookupWithKey(const.constNothing)nnsc=Cntxtnmcas(fromMap$n`M.delete`ps)(fromMapss)dg'=dg{values=delSuccnps.delPrednss$ns'}-- | As with 'decompose', but do not specify /which/ node to-- decompose.decomposeAny::(Ordn)=>DotGraphn->Maybe(Contextn,DotGraphn)decomposeAnydg|isEmptydg=Nothing|otherwise=decompose(fst.M.findMin$valuesdg)dg-- | Recursively decompose the Dot graph into a list of contexts such-- that if @(c:cs) = decomposeList dg@, then @dg = c & 'composeList' cs@.---- Note that all global attributes are lost, so this is /not/-- suitable for representing a Dot graph on its own.decomposeList::(Ordn)=>DotGraphn->[Contextn]decomposeList=unfoldrdecomposeAnydelSucc::(Ordn)=>n->EdgeMapn->NodeMapn->NodeMapndelSucc=delPSniSuccdelPred::(Ordn)=>n->EdgeMapn->NodeMapn->NodeMapndelPred=delPSniPred-- Only takes in EdgeMap rather than [n] to make it easier to call-- from decomposedelPS::(Ordn)=>((EdgeMapn->EdgeMapn)->NodeInfon->NodeInfon)->n->EdgeMapn->NodeMapn->NodeMapndelPSfnitfmnm=foldl'delEnm$M.keysfmwheredelEnm'f=M.adjust(fni$M.deletet)fnm'-- | Delete the specified node from the graph; returns the original-- graph if that node isn't present.deleteNode::(Ordn)=>n->DotGraphn->DotGraphndeleteNodendg=maybedgsnd$decomposendg-- | Delete all edges between the two nodes; returns the original-- graph if there are no edges.deleteAllEdges::(Ordn)=>n->n->DotGraphn->DotGraphndeleteAllEdgesn1n2=withValues(delAEn1n2.delAEn2n1)wheredelAEft=delSuccft'.delPredft'wheret'=M.singletont[]-- | Deletes the specified edge from the DotGraph (note: for unordered-- graphs both orientations are considered).deleteEdge::(Ordn)=>n->n->Attributes->DotGraphn->DotGraphndeleteEdgen1n2asdg=withValuesdelEsdgwheredelEft=M.adjust(niSucc$M.adjust(deleteas)t)f.M.adjust(niPred$M.adjust(deleteas)f)tdelEs|directedGraphdg=delEn1n2|otherwise=delEn1n2.delEn2n1-- | As with 'deleteEdge' but takes a 'DotEdge' rather than individual-- values.deleteDotEdge::(Ordn)=>DotEdgen->DotGraphn->DotGraphndeleteDotEdge(DotEdgen1n2as)=deleteEdgen1n2as-- | Delete the specified cluster, and makes any clusters or nodes-- within it be in its root cluster (or the overall graph if-- required).deleteCluster::(Ordn)=>GraphID->DotGraphn->DotGraphndeleteClustercdg=withValues(M.mapadjNode).withClusters(M.mapadjCluster.M.deletec)$dgwherep=parentCluster=<<c`M.lookup`clustersdgadjParentp'|p'==Justc=p|otherwise=p'adjNodeni=ni{_inCluster=adjParent$_inClusterni}adjClusterci=ci{parentCluster=adjParent$parentClusterci}-- | Remove clusters with no sub-clusters and no nodes within them.removeEmptyClusters::(Ordn)=>DotGraphn->DotGraphnremoveEmptyClustersdg=dg{clusters=cM'}wherecM=clustersdgcM'=(cM`M.difference`invCs)`M.difference`invNsinvCs=usedClustsIn$M.mapparentClustercMinvNs=usedClustsIn.M.map_inCluster$valuesdgusedClustsIn=M.fromAscList.map(liftM2(,)(fst.head)(mapsnd)).groupSortByfst.mapMaybe(uncurry(fmap.flip(,))).M.assocs-- ------------------------------------------------------------------------------- Information-- | Does this graph have any nodes?isEmpty::DotGraphn->BoolisEmpty=M.null.values-- | Does this graph have any clusters?hasClusters::DotGraphn->BoolhasClusters=M.null.clusters-- | Determine if this graph has nodes or clusters.isEmptyGraph::DotGraphn->BoolisEmptyGraph=liftM2(&&)isEmpty(not.hasClusters)graphAttributes::DotGraphn->[GlobalAttributes]graphAttributes=fromGlobAttrs.graphAttrs-- | Return the ID for the cluster the node is in.foundInCluster::(Ordn)=>DotGraphn->n->MaybeGraphIDfoundInClusterdgn=_inCluster$valuesdgM.!n-- | Return the attributes for the node.attributesOf::(Ordn)=>DotGraphn->n->AttributesattributesOfdgn=_attributes$valuesdgM.!n-- | Predecessor edges for the specified node. For undirected graphs-- equivalent to 'adjacentTo'.predecessorsOf::(Ordn)=>DotGraphn->n->[DotEdgen]predecessorsOfdgt|directedGraphdg=emToDE(flipDotEdget)._predecessors$valuesdgM.!t|otherwise=adjacentTodgt-- | Successor edges for the specified node. For undirected graphs-- equivalent to 'adjacentTo'.successorsOf::(Ordn)=>DotGraphn->n->[DotEdgen]successorsOfdgf|directedGraphdg=emToDE(DotEdgef)._successors$valuesdgM.!f|otherwise=adjacentTodgf-- | All edges involving this node.adjacentTo::(Ordn)=>DotGraphn->n->[DotEdgen]adjacentTodgn=sucs++predswhereni=valuesdgM.!nsucs=emToDE(DotEdgen)$_successorsnipreds=emToDE(flipDotEdgen)$n`M.delete`_predecessorsniemToDE::(Ordn)=>(n->Attributes->DotEdgen)->EdgeMapn->[DotEdgen]emToDEf=map(uncurryf).fromMap-- | Which cluster (or the root graph) is this cluster in?parentOf::DotGraphn->GraphID->MaybeGraphIDparentOfdgc=parentCluster$clustersdgM.!cclusterAttributes::DotGraphn->GraphID->[GlobalAttributes]clusterAttributesdgc=fromGlobAttrs.clusterAttrs$clustersdgM.!c-- ------------------------------------------------------------------------------- For DotRepr instanceinstance(Ordn)=>DotReprDotGraphnwherefromCanonical=fromDotReprgetID=graphIDsetIDig=g{graphID=Justi}graphIsDirected=directedGraphsetIsDirecteddg=g{directedGraph=d}graphIsStrict=strictGraphsetStrictnesssg=g{strictGraph=s}mapDotGraph=mapNsgraphStructureInformation=getGraphInfonodeInformation=getNodeInfoedgeInformation=getEdgeInfounAnonymise=id-- No anonymous clusters!instance(Ordn,PrintDotn)=>PrintDotReprDotGraphninstance(Ordn,ParseDotn)=>ParseDotReprDotGraphninstance(Ordn,PrintDotn,ParseDotn)=>PPDotReprDotGraphn-- | Uses the PrintDot instance for canonical 'C.DotGraph's.instance(Ordn,PrintDotn)=>PrintDot(DotGraphn)whereunqtDot=unqtDot.toCanonical-- | Uses the ParseDot instance for generalised 'G.DotGraph's.instance(Ordn,ParseDotn)=>ParseDot(DotGraphn)whereparseUnqt=liftMfromGDot$parseUnqtwhere-- fromGDot :: G.DotGraph n -> DotGraph nfromGDot=fromDotRepr.flipasTypeOf(undefined::G.DotGraphn)cOptions::CanonicaliseOptionscOptions=COpts{edgesInClusters=False,groupAttributes=True}-- | Convert any existing DotRepr instance to a 'DotGraph'.fromDotRepr::(DotReprdgn)=>dgn->DotGraphnfromDotRepr=unsafeFromCanonical.canonicaliseOptionscOptions.unAnonymise-- | Convert a canonical Dot graph to a graph-based one. This assumes-- that the canonical graph is the same format as returned by-- 'toCanonical'. The \"unsafeness\" is that:---- * All clusters must have a unique identifier ('unAnonymise' can-- be used to make sure all clusters /have/ an identifier, but it-- doesn't ensure uniqueness).---- * All nodes are assumed to be explicitly listed precisely once---- * Only edges found in the root graph are considered.---- If this isn't the case, use 'fromCanonical' instead.---- The 'graphToDot' and 'graphElemsToDot' functions from-- "Data.GraphViz" produces output suitable for this function-- (assuming all clusters are provided with a unique identifier).unsafeFromCanonical::(Ordn)=>C.DotGraphn->DotGraphnunsafeFromCanonicaldg=DG{strictGraph=C.strictGraphdg,directedGraph=dirGraph,graphAttrs=as,graphID=mgid,clusters=cs,values=ns}wherestmts=C.graphStatementsdgmgid=C.graphIDdgdirGraph=C.directedGraphdg(as,cs,ns)=fCStmtNothingstmtsfCStmtpstmts'=(sgAs,cs',ns')wheresgAs=toGlobAttrs$C.attrStmtsstmts'(cs',sgNs)=(M.unions***M.unions).unzip.map(fCSGp)$C.subGraphsstmts'nNs=M.fromList.map(fDNp)$C.nodeStmtsstmts'ns'=sgNs`M.union`nNsfCSGpsg=(M.insertsgidcics',ns')wheremsgid@(Justsgid)=C.subGraphIDsg(as',cs',ns')=fCStmtmsgid$C.subGraphStmtssgci=CIpas'fDNp(DotNodenas')=(n,NI{_inCluster=p,_attributes=as',_predecessors=eSelntEs,_successors=eSelnfEs})es=C.edgeStmtsstmtsfEs=toEdgeMapfromNodetoNodeestEs=delLoops$toEdgeMaptoNodefromNodeeseSelnes'=fromMaybeM.empty$n`M.lookup`es'delLoops=M.mapWithKeyM.deletetoEdgeMap::(Ordn)=>(DotEdgen->n)->(DotEdgen->n)->[DotEdgen]->Mapn(EdgeMapn)toEdgeMapft=M.mapeM.M.fromList.groupSortCollectByft'wheret'=liftM2(,)tedgeAttributeseM=M.fromList.groupSortCollectByfstsndmapNs::(Ordn,Ordn')=>(n->n')->DotGraphn->DotGraphn'mapNsf(DGstdasmidcsvs)=DGstdasmidcs$mapNMvswheremapNM=M.mapmapNI.mpMmapNI(NImcas'psss)=NImcas'(mpMps)(mpMss)mpM=M.mapKeysfgetGraphInfo::DotGraphn->(GlobalAttributes,ClusterLookup)getGraphInfodg=(gas,cl)wheretoGA=GraphAttrs.unSame(gas,cgs)=(toGA***M.maptoGA)$globAttrMapgraphAsdgpM=M.mappInit$clusterPathdgcl=M.mapWithKeyaddPath$M.mapKeysMonotonicJustcgsaddPathcas=(maybe[](:[])$c`M.lookup`pM,as)pInitp=caseSeq.viewrpof(p'Seq.:>_)->p'_->Seq.emptygetNodeInfo::(Ordn)=>Bool->DotGraphn->NodeLookupngetNodeInfowithGlobdg=M.maptoLookupnswhere(gGlob,aM)=globAttrMapnodeAsdgpM=clusterPathdgns=valuesdgtoLookupni=(pth,as')whereas=_attributesnimp=_inClusternipth=fromMaybeSeq.empty$mp`M.lookup`pMpAs=fromMaybegGlob$flipM.lookupaM=<<mpas'|withGlob=unSame$toSAttras`S.union`pAs|otherwise=asgetEdgeInfo::(Ordn)=>Bool->DotGraphn->[DotEdgen]getEdgeInfowithGlobdg=concatMap(uncurrymkDotEdges)eswheregGlob=edgeAs$graphAttrsdges=concatMap(uncurry(map.(,))).M.assocs.M.map(M.assocs._successors)$valuesdgaddGlobas|withGlob=unSame$toSAttras`S.union`gGlob|otherwise=asmkDotEdgesf(t,ass)=map(DotEdgeft.addGlob)assglobAttrMap::(GlobAttrs->SAttrs)->DotGraphn->(SAttrs,MapGraphIDSAttrs)globAttrMapafdg=(gGlob,aM)wheregGlob=af$graphAttrsdgcs=clustersdgaM=M.mapattrsForcsattrsForci=as`S.union`pAswhereas=af$clusterAttrscip=parentClustercipAs=fromMaybegGlob$flipM.lookupaM=<<pclusterPath::DotGraphn->Map(MaybeGraphID)St.PathclusterPath=M.mapKeysMonotonicJust.M.map(fmapJust).clusterPath'clusterPath'::DotGraphn->MapGraphID(Seq.SeqGraphID)clusterPath'dg=pMwherecs=clustersdgpM=M.mapWithKeypathOfcspathOfcci=pPthSeq.|>cwheremp=parentClustercipPth=fromMaybeSeq.empty$flipM.lookuppM=<<mp-- -----------------------------------------------------------------------------withValues::(Ordn)=>(NodeMapn->NodeMapn)->DotGraphn->DotGraphnwithValuesfdg=dg{values=f$valuesdg}withClusters::(MapGraphIDClusterInfo->MapGraphIDClusterInfo)->DotGraphn->DotGraphnwithClustersfdg=dg{clusters=f$clustersdg}toGlobAttrs::[GlobalAttributes]->GlobAttrstoGlobAttrs=mkGA.partitionGlobalwheremkGA(ga,na,ea)=GA(toSAttrga)(toSAttrna)(toSAttrea)fromGlobAttrs::GlobAttrs->[GlobalAttributes]fromGlobAttrs(GAganaea)=filter(not.null.attrs)[GraphAttrs$unSamega,NodeAttrs$unSamena,EdgeAttrs$unSameea]niSucc::(Ordn)=>(EdgeMapn->EdgeMapn)->NodeInfon->NodeInfonniSuccfni=ni{_successors=f$_successorsni}niPred::(Ordn)=>(EdgeMapn->EdgeMapn)->NodeInfon->NodeInfonniPredfni=ni{_predecessors=f$_predecessorsni}toMap::(Ordn)=>[(n,Attributes)]->EdgeMapntoMap=M.fromAscList.groupSortCollectByfstsndfromMap::EdgeMapn->[(n,Attributes)]fromMap=concatMap(uncurry(map.(,))).M.toList