{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-name-shadowing #-} {-# OPTIONS_HADDOCK not-home #-} -- | Abstract Grammar for the dot language. -- http://www.graphviz.org/doc/info/lang.html module DotParse.Types ( DotConfig (..), defaultDotConfig, DotParse (..), testDotParser, runDotParser, Error (..), prettyError, Graph (..), gattL, attL, defaultGraph, processDotWith, processDot, processGraph, processGraphWith, -- * components Strict (..), defStrict, Directed (..), defDirected, ID (..), label, Compass (..), Port (..), AttributeType (..), AttributeStatement (..), NodeStatement (..), EdgeID (..), EdgeOp (..), fromDirected, EdgeStatement (..), edgeID, edgeIDs, edgeIDsNamed, Statement (..), addStatement, addStatements, SubGraphStatement (..), GlobalAttributeStatement (..), -- * Graph Extraction bbL, nodesPortL, nodesL, edgesL, nodesA, edgesA, nodePos, nodeWidth, edgeSpline, edgeWidth, NodeInfo (..), nodeInfo, EdgeInfo (..), edgeInfo, splinePath, -- * Conversion graphToChartWith, graphToChart, ChartConfig (..), defaultChartConfig, toStatements, toDotGraph, toDotGraphWith, ) where import Algebra.Graph qualified as G import Chart import Control.Monad import Data.Bool import Data.ByteString hiding (any, empty, filter, head, length, map, zip, zipWith) import Data.ByteString.Char8 qualified as B import Data.List.NonEmpty hiding (filter, head, length, map, zip, zipWith, (!!)) import Data.Map.Merge.Strict import Data.Map.Strict qualified as Map import Data.Maybe import Data.Monoid import Data.Proxy import Data.Text (Text) import Data.Text qualified as Text import Data.These import DotParse.FlatParse import FlatParse.Basic hiding (cut) import GHC.Generics import Optics.Core import System.Exit import System.Process.ByteString import Prelude hiding (replicate) -- $setup -- >>> import DotParse -- >>> import qualified Data.Map as Map -- >>> import qualified FlatParse.Basic as FP -- >>> import qualified Data.ByteString as BS -- >>> import FlatParse.Basic (runParser, Result) -- >>> :set -XOverloadedStrings -- | printing options, for separators. data DotConfig = DotConfig { topLevelSep :: ByteString, statementSep :: ByteString, attSep :: ByteString, subGraphSep :: ByteString } deriving (Eq, Show, Generic) -- | default separators defaultDotConfig :: DotConfig defaultDotConfig = DotConfig " " "\n " ";" ";" -- | A parser & printer class for a graphviz graph and components of its dot language class DotParse a where dotPrint :: DotConfig -> a -> ByteString dotParse :: Parser Error a -- | dotParse and then dotPrint: -- -- - pretty printing error on failure. -- -- - This is not an exact parser/printer, so the test re-parses the dotPrint, which should be idempotent testDotParser :: forall a. (DotParse a) => Proxy a -> DotConfig -> ByteString -> IO () testDotParser _ cfg b = case runParser dotParse b :: Result Error a of Err e -> B.putStrLn $ prettyError b e OK a left -> do when (left /= "") (B.putStrLn $ "parsed with leftovers: " <> left) case runParser dotParse (dotPrint cfg a) :: Result Error a of Err e -> B.putStrLn $ "round trip error: " <> prettyError (dotPrint cfg a) e Fail -> B.putStrLn "uncaught round trip parse error" OK _ left' -> do when (left' /= "") (B.putStrLn $ "round trip parse with left overs" <> left) Fail -> B.putStrLn "uncaught parse error" -- | run a dotParse erroring on leftovers, Fail or Err runDotParser :: (DotParse a) => ByteString -> a runDotParser = runParser_ dotParse -- | Representation of a full graphviz graph, as per the dot language specification data Graph = Graph { strict :: Last Strict, directed :: Last Directed, graphid :: Last ID, nodeAttributes :: Map.Map ID ID, graphAttributes :: Map.Map ID ID, edgeAttributes :: Map.Map ID ID, globalAttributes :: Map.Map ID ID, nodes :: [NodeStatement], edges :: [EdgeStatement], subgraphs :: [SubGraphStatement] } deriving (Eq, Show, Generic) instance Semigroup Graph where (Graph m d i na ga ea gs ns es ss) <> (Graph m' d' i' na' ga' ea' gs' ns' es' ss') = Graph (m <> m') (d <> d') (i <> i') (na <> na') (ga <> ga') (ea <> ea') (gs <> gs') (ns <> ns') (es <> es') (ss <> ss') instance Monoid Graph where mempty = Graph mempty mempty mempty mempty mempty mempty mempty mempty mempty mempty -- | 'Directed' graph of size 1. -- -- >>> BS.putStr $ dotPrint defaultDotConfig defaultGraph <> "\n" -- digraph { -- node [height=0.5;shape=circle] -- graph [overlap=false;size="1!";splines=spline] -- edge [arrowsize=0.5] -- rankdir="TB" -- } defaultGraph :: Graph defaultGraph = mempty & set (attL NodeType (ID "height")) (Just $ IDDouble 0.5) & set (attL NodeType (ID "shape")) (Just $ ID "circle") & set (attL GraphType (ID "overlap")) (Just $ ID "false") & set (attL GraphType (ID "size")) (Just $ IDQuoted "1!") & set (attL GraphType (ID "splines")) (Just $ ID "spline") & set (attL EdgeType (ID "arrowsize")) (Just $ IDDouble 0.5) & set #directed (Last (Just Directed)) & set (gattL (ID "rankdir")) (Just (IDQuoted "TB")) -- | global attributes lens gattL :: ID -> Lens' Graph (Maybe ID) gattL k = #globalAttributes % at k -- | attributes lens attL :: AttributeType -> ID -> Lens' Graph (Maybe ID) attL GraphType k = #graphAttributes % at k attL NodeType k = #nodeAttributes % at k attL EdgeType k = #edgeAttributes % at k outercalate :: ByteString -> [ByteString] -> ByteString outercalate _ [] = mempty outercalate a xs = a <> intercalate a xs <> a instance DotParse Graph where dotPrint cfg (Graph me d i na ga ea gs ns es ss) = intercalate (cfg ^. #topLevelSep) $ bool [] ["strict"] (me == Last (Just MergeEdges)) <> bool ["digraph"] ["graph"] (d == Last (Just UnDirected)) <> maybe [] ((: []) . dotPrint cfg) (getLast i) <> [ wrapCurlyPrint $ outercalate (cfg ^. #statementSep) ( [dotPrint cfg (AttributeStatement NodeType na)] <> [dotPrint cfg (AttributeStatement GraphType ga)] <> [dotPrint cfg (AttributeStatement EdgeType ea)] <> (dotPrint cfg . GlobalAttributeStatement <$> Map.toList gs) <> (dotPrint cfg <$> ns) <> (dotPrint cfg <$> es) <> (dotPrint cfg <$> ss) ) ] dotParse = token $ do me <- dotParse d <- dotParse i <- optional dotParse ss <- wrapCurlyP (many dotParse) let g = (mempty :: Graph) & #strict .~ Last (Just me) & #directed .~ Last (Just d) & #graphid .~ Last i pure $ addStatements ss g -- * Dot Grammar -- | MergeEdges (strict) data Strict = MergeEdges | NoMergeEdges deriving (Eq, Show, Generic) instance DotParse Strict where dotPrint _ MergeEdges = "strict" dotPrint _ NoMergeEdges = "" dotParse = token $ withOption ($(keyword "strict")) (const $ pure MergeEdges) (pure NoMergeEdges) -- | Default Strict is NoMergeEdges defStrict :: Last Strict -> Strict defStrict (Last Nothing) = NoMergeEdges defStrict (Last (Just x)) = x -- | Directed (digraph | graph) data Directed = Directed | UnDirected deriving (Eq, Show, Generic) instance DotParse Directed where dotPrint _ Directed = "digraph" dotPrint _ UnDirected = "graph" dotParse = token $ (Directed <$ $(keyword "digraph")) <|> (UnDirected <$ $(keyword "graph")) -- | Default Directed is Directed defDirected :: Last Directed -> Directed defDirected (Last Nothing) = Directed defDirected (Last (Just x)) = x -- | A dot statement as per the dot language specification. data Statement = StatementNode NodeStatement | StatementEdge EdgeStatement | StatementGlobalAttribute GlobalAttributeStatement | StatementAttribute AttributeStatement | StatementSubGraph SubGraphStatement deriving (Eq, Show, Generic) instance DotParse Statement where dotPrint cfg (StatementNode x) = dotPrint cfg x dotPrint cfg (StatementEdge x) = dotPrint cfg x dotPrint cfg (StatementAttribute x) = dotPrint cfg x dotPrint cfg (StatementGlobalAttribute x) = dotPrint cfg x dotPrint cfg (StatementSubGraph x) = dotPrint cfg x dotParse = token $ -- Order is important (StatementEdge <$> dotParse) <|> (StatementAttribute <$> dotParse) <|> (StatementGlobalAttribute <$> dotParse) <|> (StatementSubGraph <$> dotParse) <|> (StatementNode <$> dotParse) -- | Identifier as per the dot language specifications. -- -- >>> runDotParser "0" :: ID -- IDInt 0 -- -- >>> runDotParser "-.123" :: ID -- IDDouble (-0.123) -- -- >>> runParser dotParse "apple_1'" :: Result Error ID -- OK (ID "apple_1") "'" -- -- >>> :set -XQuasiQuotes -- >>> runParser dotParse "\"hello\"" :: Result Error ID -- OK (IDQuoted "hello") "" -- -- >>> runDotParser "foo,
the bar and
the baz>" :: ID -- IDHtml "foo,
the bar and
the baz>" -- -- >>> runDotParser "shape=diamond" :: (ID,ID) -- (ID "shape",ID "diamond") -- -- >>> runDotParser "fontname=\"Arial\"" :: (ID,ID) -- (ID "fontname",IDQuoted "Arial") -- -- >>> runDotParser "[shape=diamond; color=blue] [label=label]" :: Map.Map ID ID -- fromList [(ID "color",ID "blue"),(ID "label",ID "label"),(ID "shape",ID "diamond")] data ID = ID ByteString | IDInt Int | IDDouble Double | IDQuoted ByteString | IDHtml ByteString deriving (Eq, Show, Generic, Ord) instance DotParse ID where dotPrint _ (ID s) = s dotPrint _ (IDInt i) = strToUtf8 (show i) dotPrint _ (IDDouble x) = strToUtf8 (show x) dotPrint _ (IDQuoted x) = wrapQuotePrint x dotPrint _ (IDHtml s) = s -- order matters dotParse = (ID <$> ident) <|> (IDInt <$> (signed int `notFollowedBy` $(char '.'))) <|> (IDDouble <$> signed double) <|> (IDQuoted . strToUtf8 <$> quoted) <|> (IDHtml . strToUtf8 <$> htmlLike) -- | ID as the equivalent plain String -- -- note that the dot language identifier equivalence law is: -- -- > x == y if label x == label y label :: ID -> String label (ID s) = utf8ToStr s label (IDInt i) = show i label (IDDouble d) = show d label (IDQuoted q) = utf8ToStr q label (IDHtml h) = utf8ToStr h -- | Attribute key-value pair of identifiers instance DotParse (ID, ID) where dotPrint cfg (x0, x1) = dotPrint cfg x0 <> "=" <> dotPrint cfg x1 dotParse = token $ do x0 <- token dotParse _ <- token $(symbol "=") x1 <- dotParse pure (x0, x1) -- | Attribute collections -- -- A given entity can have multiple attribute lists. For simplicity, these are mconcat'ed on parsing. instance DotParse (Map.Map ID ID) where dotPrint cfg as = bool (wrapSquarePrint (intercalate (cfg ^. #attSep) $ dotPrint cfg <$> Map.toList as)) mempty (as == Map.empty) dotParse = Map.fromList . mconcat . fmap toList <$> token (many (wrapSquareP (nonEmptyP dotParse sepP)) <|> ([] <$ wrapSquareP ws)) -- | Compass instructions which are optionally associated with an identifier data Compass = CompassN | CompassNE | CompassE | CompassSE | CompassS | CompassSW | CompassW | CompassNW | CompassC | Compass_ deriving (Eq, Show, Generic) instance DotParse Compass where dotPrint _ CompassN = "n" dotPrint _ CompassNE = "ne" dotPrint _ CompassE = "e" dotPrint _ CompassSE = "se" dotPrint _ CompassS = "s" dotPrint _ CompassSW = "sw" dotPrint _ CompassW = "w" dotPrint _ CompassNW = "nw" dotPrint _ CompassC = "c" dotPrint _ Compass_ = "_" dotParse = token $( switch [| case _ of "n" -> pure CompassN "ne" -> pure CompassNE "e" -> pure CompassE "se" -> pure CompassSE "s" -> pure CompassS "sw" -> pure CompassSW "w" -> pure CompassW "nw" -> pure CompassNW "c" -> pure CompassC "_" -> pure Compass_ |] ) -- | Port instructions which are optionally associated with an identifier newtype Port = Port {portID :: These ID Compass} deriving (Eq, Show, Generic) instance DotParse Port where dotPrint cfg (Port (This i)) = ": " <> dotPrint cfg i dotPrint cfg (Port (That c)) = ": " <> dotPrint cfg c dotPrint cfg (Port (These i c)) = ": " <> dotPrint cfg i <> " : " <> dotPrint cfg c dotParse = token $ ((\x0 x1 -> Port (These x0 x1)) <$> ($(symbol ":") *> dotParse) <*> ($(symbol ":") *> dotParse)) <|> (Port . This <$> ($(symbol ":") *> dotParse)) <|> (Port . That <$> ($(symbol ":") *> dotParse)) -- | A top-level attribute -- -- >>> runDotParser "rankdir=\"BT\"" :: Statement -- StatementGlobalAttribute (GlobalAttributeStatement {globalAttributeStatement = (ID "rankdir",IDQuoted "BT")}) newtype GlobalAttributeStatement = GlobalAttributeStatement {globalAttributeStatement :: (ID, ID)} deriving (Eq, Show, Generic) instance DotParse GlobalAttributeStatement where dotPrint cfg (GlobalAttributeStatement s) = dotPrint cfg s dotParse = GlobalAttributeStatement <$> dotParse -- | Category of attribute data AttributeType = GraphType | NodeType | EdgeType deriving (Eq, Show, Generic) instance DotParse AttributeType where dotPrint _ GraphType = "graph" dotPrint _ NodeType = "node" dotPrint _ EdgeType = "edge" dotParse = token (GraphType <$ $(keyword "graph")) <|> (NodeType <$ $(keyword "node")) <|> (EdgeType <$ $(keyword "edge")) -- | Top-level attribute statement -- -- >>> runDotParser "graph [overlap=false, splines=spline, size=\"1!\"];" :: Statement -- StatementAttribute (AttributeStatement {attributeType = GraphType, attributes = fromList [(ID "overlap",ID "false"),(ID "size",IDQuoted "1!"),(ID "splines",ID "spline")]}) data AttributeStatement = AttributeStatement {attributeType :: AttributeType, attributes :: Map.Map ID ID} deriving (Eq, Show, Generic) instance DotParse AttributeStatement where dotPrint cfg (AttributeStatement t as) = bool ( intercalate " " [dotPrint cfg t, dotPrint cfg as] ) mempty (mempty == as) dotParse = AttributeStatement <$> dotParse <*> dotParse -- | Node statement -- -- >>> runDotParser "A [shape=diamond; color=blue]" :: Statement -- StatementNode (NodeStatement {nodeID = ID "A", port = Nothing, nodeAttrs = fromList [(ID "color",ID "blue"),(ID "shape",ID "diamond")]}) data NodeStatement = NodeStatement {nodeID :: ID, port :: Maybe Port, nodeAttrs :: Map.Map ID ID} deriving (Eq, Show, Generic) instance DotParse NodeStatement where dotPrint cfg (NodeStatement i p as) = intercalate " " $ [dotPrint cfg i] <> (dotPrint cfg <$> maybeToList p) <> [dotPrint cfg as] dotParse = NodeStatement <$> dotParse <*> optional dotParse <*> dotParse -- | An edge can be specified in as a NodeID or as a SubGraph data EdgeID = EdgeID ID (Maybe Port) | EdgeSubGraph SubGraphStatement deriving (Eq, Show, Generic) instance DotParse EdgeID where dotPrint cfg (EdgeID e p) = mconcat $ [dotPrint cfg e] <> (dotPrint cfg <$> maybeToList p) dotPrint cfg (EdgeSubGraph s) = dotPrint cfg s dotParse = (EdgeID <$> dotParse <*> optional dotParse) <|> (EdgeSubGraph <$> dotParse) -- | An edgeop is -> in directed graphs and -- in undirected graphs. data EdgeOp = EdgeDirected | EdgeUndirected deriving (Eq, Show, Generic) instance DotParse EdgeOp where dotPrint _ EdgeDirected = "->" dotPrint _ EdgeUndirected = "--" dotParse = token $( switch [| case _ of "->" -> pure EdgeDirected "--" -> pure EdgeUndirected |] ) -- | generate an EdgeOp given the type of graph. fromDirected :: Directed -> EdgeOp fromDirected Directed = EdgeDirected fromDirected UnDirected = EdgeUndirected -- | Edge statement -- -- >>> runDotParser "A -> B [style=dashed, color=grey]" :: Statement -- StatementEdge (EdgeStatement {edgeOp = EdgeDirected, leftEdge = EdgeID (ID "A") Nothing, rightEdges = EdgeID (ID "B") Nothing :| [], edgeAttrs = fromList [(ID "color",ID "grey"),(ID "style",ID "dashed")]}) data EdgeStatement = EdgeStatement {edgeOp :: EdgeOp, leftEdge :: EdgeID, rightEdges :: NonEmpty EdgeID, edgeAttrs :: Map.Map ID ID} deriving (Eq, Show, Generic) instance DotParse EdgeStatement where dotPrint cfg (EdgeStatement l rs xs as) = intercalate " " ( [intercalate (" " <> dotPrint cfg l <> " ") (dotPrint cfg <$> (rs : toList xs))] <> [dotPrint cfg as] ) dotParse = token $ do l <- dotParse o0 <- dotParse r0 <- dotParse ors <- many ((,) <$> dotParse <*> dotParse) as <- dotParse bool (pure (EdgeStatement o0 l (r0 :| (snd <$> ors)) as)) empty (any ((/= o0) . fst) ors) -- | The edge ID or subgraph ID (if any) edgeID :: EdgeID -> Maybe ID edgeID (EdgeID i _) = Just i edgeID (EdgeSubGraph (SubGraphStatement i _)) = i -- | edge IDs edgeIDsNamed :: EdgeStatement -> [(ID, ID)] edgeIDsNamed e = [(x, y) | (Just x, Just y) <- edgeIDs e] -- | list of edges in a given EdgeStatement, including anonymous SugGraphs edgeIDs :: EdgeStatement -> [(Maybe ID, Maybe ID)] edgeIDs e = zip (id0 : id1) id1 where id0 = edgeID (view #leftEdge e) id1 = edgeID <$> toList (view #rightEdges e) -- | A subgraph statement. -- -- Note: each subgraph must have a unique name -- -- >>> runDotParser "subgraph A {A, B, C}" :: Statement -- StatementSubGraph (SubGraphStatement {subgraphID = Just (ID "A"), subgraphStatements = [StatementNode (NodeStatement {nodeID = ID "A", port = Nothing, nodeAttrs = fromList []}),StatementNode (NodeStatement {nodeID = ID "B", port = Nothing, nodeAttrs = fromList []}),StatementNode (NodeStatement {nodeID = ID "C", port = Nothing, nodeAttrs = fromList []})]}) data SubGraphStatement = SubGraphStatement {subgraphID :: Maybe ID, subgraphStatements :: [Statement]} deriving (Eq, Show, Generic) instance DotParse SubGraphStatement where dotPrint cfg (SubGraphStatement x xs) = intercalate " " $ maybe [] (\x' -> [intercalate " " ["subgraph", dotPrint cfg x']]) x <> (: []) (wrapCurlyPrint (intercalate (cfg ^. #subGraphSep) $ dotPrint cfg <$> xs)) dotParse = token $ do x <- optional ($(keyword "subgraph") *> dotParse) pure (SubGraphStatement x) <*> wrapCurlyP (many (optional sepP *> dotParse)) -- | add a graphviz statement to a 'Graph' addStatement :: Statement -> Graph -> Graph addStatement (StatementNode n) g = g & #nodes %~ (<> [n]) addStatement (StatementEdge e) g = g & #edges %~ (<> [e]) addStatement (StatementSubGraph s) g = g & #subgraphs %~ (<> [s]) addStatement (StatementAttribute (AttributeStatement GraphType as)) g = g & #graphAttributes %~ (<> as) addStatement (StatementAttribute (AttributeStatement NodeType as)) g = g & #nodeAttributes %~ (<> as) addStatement (StatementAttribute (AttributeStatement EdgeType as)) g = g & #edgeAttributes %~ (<> as) addStatement (StatementGlobalAttribute (GlobalAttributeStatement s)) g = g & #globalAttributes %~ (<> Map.fromList [s]) -- | add a list of graphviz statements to a 'Graph' addStatements :: [Statement] -> Graph -> Graph addStatements ss g = Prelude.foldr addStatement g ss -- | run a dot string through graphviz, supplying arguments and collecting stdout processDotWith :: Directed -> [String] -> ByteString -> IO ByteString processDotWith d args i = do let cmd = case d of Directed -> "dot" UnDirected -> "neato" (r, input, e) <- readProcessWithExitCode cmd args i bool (error $ utf8ToStr e) (pure input) (r == ExitSuccess) -- | run a dot string through graphviz, collecting the augmented dot string output processDot :: Directed -> ByteString -> IO ByteString processDot d = processDotWith d ["-Tdot"] -- | Augment a Graph via the graphviz process processGraphWith :: DotConfig -> Graph -> IO Graph processGraphWith cfg g = runDotParser <$> processDot (defDirected $ view #directed g) (dotPrint cfg g) -- | Augment a Graph via the graphviz process processGraph :: Graph -> IO Graph processGraph g = runDotParser <$> processDot (defDirected $ view #directed g) (dotPrint defaultDotConfig g) instance DotParse (Point Double) where dotPrint _ (Point x y) = intercalate "," $ strToUtf8 . show <$> [x, y] dotParse = token pointP pointI :: Iso' ID (Point Double) pointI = iso (runParser_ pointP . strToUtf8 . label) (IDQuoted . dotPrint defaultDotConfig) instance DotParse (Rect Double) where dotPrint _ (Rect x z y w) = intercalate "," $ strToUtf8 . show <$> [x, y, z, w] dotParse = token rectP rectI :: Iso' ID (Rect Double) rectI = iso (runParser_ rectP . strToUtf8 . label) (IDQuoted . dotPrint defaultDotConfig) -- | Bounding box ID lens bb_ :: Lens' Graph (Maybe ID) bb_ = #graphAttributes % at (ID "bb") -- | Bounding Box lens as a 'Rect' bbL :: Lens' Graph (Maybe (Rect Double)) bbL = lens (preview (bb_ % _Just % rectI)) (\g r -> set bb_ (review rectI <$> r) g) -- | nodes lens nodesPortL :: Lens' Graph (Map.Map ID (Maybe Port, Map.Map ID ID)) nodesPortL = lens ( \g -> g & view #nodes & fmap (\x -> (view #nodeID x, (view #port x, view #nodeAttrs x))) & Map.fromList ) (\g m -> g & #nodes .~ ((\(n, (p, a)) -> NodeStatement n p a) <$> Map.toList m)) -- | nodes lens ignoring/forgetting port information nodesL :: Lens' Graph (Map.Map ID (Map.Map ID ID)) nodesL = lens ( \g -> g & view #nodes & fmap (\x -> (view #nodeID x, view #nodeAttrs x)) & Map.fromList ) (\g m -> g & #nodes .~ ((\(n, a) -> NodeStatement n Nothing a) <$> Map.toList m)) -- | edges lens ignoring/forgetting port information edgesL :: Lens' Graph (Map.Map (ID, ID) (Map.Map ID ID)) edgesL = lens getEdges_ setEdges_ -- | edge & attribute map -- ignores anonymous subgraphs getEdges_ :: Graph -> Map.Map (ID, ID) (Map.Map ID ID) getEdges_ g = Map.fromList $ mconcat $ fmap (\(xs, a) -> (,a) <$> xs) [(edgeIDsNamed e, view #edgeAttrs e) | e <- view #edges g] setEdges_ :: Graph -> Map.Map (ID, ID) (Map.Map ID ID) -> Graph setEdges_ g m = g & #edges .~ ( ( \((x0, x1), as) -> EdgeStatement (fromDirected (defDirected $ view #directed g)) (EdgeID x0 Nothing) (EdgeID x1 Nothing :| []) as ) <$> Map.toList m ) -- | A specific attribute for all nodes in a graph nodesA :: ID -> Graph -> Map.Map ID (Maybe ID) nodesA a g = fmap (Map.lookup a) (view nodesL g) -- | node position (as a Point) nodePos :: Graph -> Map.Map ID (Maybe (Point Double)) nodePos = fmap (fmap (view pointI)) . nodesA (ID "pos") -- -- | Specific attribute for all edges edgesA :: Graph -> ID -> Map.Map (ID, ID) (Maybe ID) edgesA g a = fmap (Map.lookup a) (view edgesL g) -- | node width attributes nodeWidth :: Graph -> Map.Map ID (Maybe Double) nodeWidth g = fmap ( \case Just (IDDouble x') -> Just x' _ -> Nothing ) $ nodesA (ID "width") g -- | edge width attributes edgeWidth :: Graph -> Map.Map (ID, ID) (Maybe Double) edgeWidth g = fmap ( \case Just (IDDouble x') -> Just x' _ -> Nothing ) $ edgesA g (ID "width") -- | edge path attributes edgeSpline :: Graph -> Map.Map (ID, ID) (Maybe Spline) edgeSpline g = fmap ( \case Just (IDQuoted x') -> Just (runParser_ splineP x') _ -> Nothing ) $ edgesA g (ID "pos") -- | typical node information after processing a dot bytestring. data NodeInfo = NodeInfo {nlabel :: ID, nwidth :: Double, pos :: Point Double} deriving (Eq, Show, Generic) -- | Create a list of NodeInfo from a graph. nodeInfo :: Graph -> Double -> [NodeInfo] nodeInfo g w = [NodeInfo x (fromMaybe w (join w')) p | (x, (Just p, w')) <- xs] where xs = Map.toList $ merge (mapMissing (\_ v -> (v, Nothing))) dropMissing (zipWithMatched (\_ x y -> (x, Just y))) (nodePos g) (nodeWidth g) -- | typical edge information after processing a dot bytestring. data EdgeInfo = EdgeInfo {elabel :: (ID, ID), ewidth :: Double, curve :: [PathData Double]} deriving (Eq, Show, Generic) -- | Create a list of EdgeInfo from a graph edgeInfo :: Graph -> Double -> [EdgeInfo] edgeInfo g w = [EdgeInfo (x, y) (fromMaybe w (join w')) (splinePath p) | ((x, y), (Just p, w')) <- xs] where xs = Map.toList $ merge (mapMissing (\_ v -> (v, Nothing))) dropMissing (zipWithMatched (\_ x y -> (x, Just y))) (edgeSpline g) (edgeWidth g) -- | -- -- https://graphviz.org/docs/attr-types/splineType/ -- format of the example is end point point and then triples (5,8,11 lengths are 1, 2 and 3 cubics) splinePath :: Spline -> [PathData Double] splinePath sp = s' <> p1' <> cs <> e' where s' = maybe [] (\s -> [StartP s, LineP $ view #splineP1 sp]) (view #splineStart sp) e' = maybe [] (\e -> [LineP e]) (view #splineEnd sp) p1' = [StartP (view #splineP1 sp)] cs = (\(x, y, z) -> CubicP x y z) <$> view #splineTriples sp -- | create Statements from a (no edge label) algebraic graph toStatements :: Directed -> G.Graph ByteString -> [Statement] toStatements d g = ((\x -> StatementNode $ NodeStatement (IDQuoted x) Nothing Map.empty) <$> G.vertexList g) <> ( ( \(x, y) -> StatementEdge $ EdgeStatement (fromDirected d) (EdgeID (IDQuoted x) Nothing) (fromList [EdgeID (IDQuoted y) Nothing]) Map.empty ) <$> G.edgeList g ) -- | Various configutaion parameters for the chart-svg Chart data ChartConfig = ChartConfig { chartHeight :: Double, chartScale :: Double, edgeSize :: Double, nodeBorderSize :: Double, chartColor :: Colour, chartBackgroundColor :: Colour, backupNodeHeight :: Double, backupNodeWidth :: Double, chartVshift :: Double, textSize :: Double, escapeText :: EscapeText } deriving (Generic, Show, Eq) -- | default parameters defaultChartConfig :: ChartConfig defaultChartConfig = ChartConfig 500 72 0.5 1 (grey 0.4 0.8) (grey 0.5 0.2) 0.5 0.5 (-6) 16 NoEscapeText -- | convert a 'Graph' processed via the graphviz commands to a 'ChartOptions' graphToChartWith :: ChartConfig -> (ID -> Text) -> Graph -> ChartOptions graphToChartWith cfg labelf g = mempty & set #chartTree (named "edges" ps <> named "shapes" c0 <> named "labels" [ts]) & set (#chartTree % charts' % each % #chartStyle % #scaleP) ScalePArea & #markupOptions % #markupHeight .~ (Just $ cfg ^. #chartHeight) & #markupOptions % #chartAspect .~ ChartAspect & #hudOptions .~ mempty where glyphs w = case view (attL NodeType (ID "shape")) g of Just (ID "circle") -> defaultGlyphStyle & #glyphShape .~ CircleGlyph & #size .~ (cfg ^. #chartScale) * w & #borderSize .~ (cfg ^. #edgeSize) & #borderColor .~ (cfg ^. #chartColor) & #color .~ (cfg ^. #chartBackgroundColor) Just (ID "box") -> defaultGlyphStyle & #glyphShape .~ RectSharpGlyph (h / w) & #size .~ (cfg ^. #chartScale) * w & #borderSize .~ view #nodeBorderSize cfg & #borderColor .~ (cfg ^. #chartColor) & #color .~ (cfg ^. #chartBackgroundColor) -- defaults to circle _ -> defaultGlyphStyle & #glyphShape .~ CircleGlyph & #size .~ (cfg ^. #chartScale) * w & #borderSize .~ view #nodeBorderSize cfg & #borderColor .~ (cfg ^. #chartColor) & #color .~ (cfg ^. #chartBackgroundColor) h = maybe (cfg ^. #backupNodeHeight) (runParser_ double . strToUtf8 . label) (view (attL NodeType (ID "height")) g) vshift' = cfg ^. #chartVshift -- node information ns = nodeInfo g (cfg ^. #backupNodeWidth) -- edge information es = edgeInfo g (cfg ^. #edgeSize) -- paths ps = fmap (\(EdgeInfo _ w p) -> PathChart (defaultPathStyle & #borderSize .~ (2 * w) & #borderColor .~ (cfg ^. #chartColor) & #color .~ transparent) p) es -- circles c0 = fmap (\(NodeInfo _ w p) -> GlyphChart (glyphs w) [p]) ns -- labels ts = TextChart (defaultTextStyle & #escapeText .~ (cfg ^. #escapeText) & #size .~ (cfg ^. #textSize) & #color .~ (cfg ^. #chartColor)) ((\(NodeInfo l _ (Point x y)) -> (labelf l, Point x (vshift' + y))) <$> ns) -- | convert a 'Graph' processed via the graphviz commands to a 'ChartOptions' using the default ChartConfig. graphToChart :: Graph -> ChartOptions graphToChart = graphToChartWith defaultChartConfig (Text.pack . label) -- | Convert an algebraic graph to a dotparse graph. toDotGraphWith :: Directed -> Graph -> G.Graph ByteString -> Graph toDotGraphWith d g gg = g & #directed .~ Last (Just d) & addStatements (toStatements d gg) -- | Convert an algebraic graph to a dotparse graph, starting with the 'defaultGraph'. toDotGraph :: G.Graph ByteString -> Graph toDotGraph = toDotGraphWith Directed defaultGraph