GraphExplorer.purs 16.9 KB
Newer Older
James Laver's avatar
James Laver committed
1 2 3 4
module Gargantext.Components.GraphExplorer where

import Gargantext.Prelude hiding (max,min)

5
import DOM.Simple.Types (Element)
6
import Data.Array as A
7
import Data.FoldableWithIndex (foldMapWithIndex)
James Laver's avatar
James Laver committed
8
import Data.Int (toNumber)
9
import Data.Map as Map
10
import Data.Maybe (Maybe(..), fromJust, maybe)
11
import Data.Nullable (null, Nullable)
James Laver's avatar
James Laver committed
12
import Data.Sequence as Seq
13 14
import Data.Set as Set
import Data.Tuple (fst, snd, Tuple(..))
15
import Data.Tuple.Nested ((/\))
James Laver's avatar
James Laver committed
16
import Effect.Aff (Aff)
17 18 19 20
import Math (log)
import Partial.Unsafe (unsafePartial)
import Reactix as R
import Reactix.DOM.HTML as RH
21
import Record as Record
22

23
import Gargantext.AsyncTasks as GAT
24 25
import Gargantext.Components.Forest (forest)
import Gargantext.Components.Graph as Graph
James Laver's avatar
James Laver committed
26
import Gargantext.Components.GraphExplorer.Controls as Controls
27
import Gargantext.Components.GraphExplorer.Sidebar as Sidebar
James Laver's avatar
James Laver committed
28
import Gargantext.Components.GraphExplorer.ToggleButton as Toggle
29
import Gargantext.Components.GraphExplorer.Types as GET
30
import Gargantext.Data.Louvain as Louvain
31
import Gargantext.Ends (Frontends, Backend)
32 33
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Hooks.Sigmax as Sigmax
34
import Gargantext.Hooks.Sigmax.Types as SigmaxT
35
import Gargantext.Routes (SessionRoute(NodeAPI), AppRoute)
36
import Gargantext.Sessions (Session, Sessions, get)
37
import Gargantext.Types as Types
38
import Gargantext.Utils.Range as Range
39
import Gargantext.Utils.Reactix as R2
40
import Gargantext.Utils.Reload as GUR
James Laver's avatar
James Laver committed
41

42
thisModule :: String
43 44
thisModule = "Gargantext.Components.GraphExplorer"

45
type LayoutProps = (
46
    asyncTasksRef :: R.Ref (Maybe GAT.Reductor)
47
  , backend       :: R.State (Maybe Backend)
48
  , currentRoute  :: AppRoute
49
  , frontends     :: Frontends
50 51 52 53 54
  , graphId       :: GET.GraphId
  , handed        :: Types.Handed
  , session       :: Session
  , sessions      :: Sessions
  , showLogin     :: R.State Boolean
55
  )
56

57 58
type Props =
  ( graph          :: SigmaxT.SGraph
59
  , graphVersion   :: GUR.ReloadS
60
  , hyperdataGraph :: GET.HyperdataGraph
61
  , mMetaData      :: Maybe GET.MetaData
62
  | LayoutProps
63
  )
James Laver's avatar
James Laver committed
64

65
--------------------------------------------------------------
66 67
explorerLayout :: Record LayoutProps -> R.Element
explorerLayout props = R.createElement explorerLayoutCpt props []
68

69
explorerLayoutCpt :: R.Component LayoutProps
70
explorerLayoutCpt = R.hooksComponentWithModule thisModule "explorerLayout" cpt
71
  where
72
    cpt props _ = do
73
      graphVersion <- GUR.new
74 75
      pure $ explorerLayoutView graphVersion props

76
explorerLayoutView :: GUR.ReloadS -> Record LayoutProps -> R.Element
77 78
explorerLayoutView graphVersion p = R.createElement el p []
  where
79
    el = R.hooksComponentWithModule thisModule "explorerLayoutView" cpt
80
    cpt props@{ graphId, session } _ = do
81
      useLoader graphId (getNodes session graphVersion) handler
82
      where
83
        handler loaded =
84 85 86
          explorer (Record.merge props { graph, graphVersion, hyperdataGraph: loaded, mMetaData })
          where
            GET.HyperdataGraph { graph: hyperdataGraph } = loaded
87
            Tuple mMetaData graph = convert hyperdataGraph
88

89
--------------------------------------------------------------
90 91
explorer :: Record Props -> R.Element
explorer props = R.createElement explorerCpt props []
James Laver's avatar
James Laver committed
92

93
explorerCpt :: R.Component Props
94
explorerCpt = R.hooksComponentWithModule thisModule "explorer" cpt
James Laver's avatar
James Laver committed
95
  where
96
    cpt props@{ asyncTasksRef
97
              , backend
98
              , currentRoute
99
              , frontends
100 101 102 103
              , graph
              , graphId
              , graphVersion
              , handed
104
              , hyperdataGraph
105 106 107 108
              , mMetaData
              , session
              , sessions
              , showLogin
109
              } _ = do
110 111

      let startForceAtlas = maybe true (\(GET.MetaData { startForceAtlas }) -> startForceAtlas) mMetaData
112 113 114 115

      let forceAtlasS = if startForceAtlas
                          then SigmaxT.InitialRunning
                          else SigmaxT.InitialStopped
116

117
      dataRef <- R.useRef graph
118
      graphRef <- R.useRef null
119 120 121 122 123 124 125 126 127 128
      graphVersionRef <- R.useRef (GUR.value graphVersion)
      treeReload <- GUR.new
      treeReloadRef <- GUR.newIInitialized treeReload
      controls <- Controls.useGraphControls { forceAtlasS
                                           , graph
                                           , graphId
                                           , hyperdataGraph
                                           , session
                                           , treeReload: \_ -> GUR.bump treeReload
                                           }
129
      multiSelectEnabledRef <- R.useRef $ fst controls.multiSelectEnabled
130

131
      R.useEffect' $ do
132
        let readData = R.readRef dataRef
133
        let gv = R.readRef graphVersionRef
134
        if SigmaxT.eqGraph readData graph then
135 136
          pure unit
        else do
137
          -- Graph data changed, reinitialize sigma.
138 139 140
          let rSigma = R.readRef controls.sigmaRef
          Sigmax.cleanupSigma rSigma "explorerCpt"
          R.setRef dataRef graph
141
          R.setRef graphVersionRef (GUR.value graphVersion)
142
          -- Reinitialize bunch of state as well.
143
          snd controls.removedNodeIds  $ const SigmaxT.emptyNodeIds
144
          snd controls.selectedNodeIds $ const SigmaxT.emptyNodeIds
145
          snd controls.showEdges       $ const SigmaxT.EShow
146
          snd controls.forceAtlasState $ const forceAtlasS
147 148
          snd controls.graphStage      $ const Graph.Init
          snd controls.showSidePanel   $ const GET.InitialClosed
149

James Laver's avatar
James Laver committed
150
      pure $
151 152 153 154 155 156 157 158 159 160 161 162
        RH.div { className: "graph-meta-container" } [
          RH.div { className: "fixed-top navbar navbar-expand-lg"
                 , id: "graph-explorer" }
            [ rowToggle
                    [ col [ spaces [ Toggle.treeToggleButton controls.showTree         ]]
                    , col [ spaces [ Toggle.controlsToggleButton controls.showControls ]]
                    , col [ spaces [ Toggle.sidebarToggleButton controls.showSidePanel ]]
                    ]
            ]
        , RH.div { className: "graph-container" } [
            inner handed [
              rowControls [ Controls.controls controls ]
163
            , RH.div { className: "row graph-row" } $ mainLayout handed $
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
                tree { asyncTasksRef
                    , backend
                    , currentRoute
                    , frontends
                    , handed
                    , reload: treeReload
                    , sessions
                    , show: fst controls.showTree
                    , showLogin: snd showLogin
                    , treeReloadRef
                    }
                /\
                RH.div { ref: graphRef, id: "graph-view", className: "col-md-12" } []
                /\
                graphView { controls
                          , elRef: graphRef
                          , graphId
                          , graph
                          , hyperdataGraph
                          , mMetaData
                          , multiSelectEnabledRef
                          }
                /\
                mSidebar mMetaData { frontends
                                  , graph
                                  , graphId
                                  , graphVersion
                                  , removedNodeIds : controls.removedNodeIds
                                  , session
                                  , selectedNodeIds: controls.selectedNodeIds
                                  , showSidePanel  :   controls.showSidePanel
                                  , treeReload
                                  }
James Laver's avatar
James Laver committed
197 198
            ]
          ]
199
        ]
200

201
    mainLayout Types.RightHanded (tree' /\ gc /\ gv /\ sdb) = [tree', gc, gv, sdb]
202
    mainLayout Types.LeftHanded  (tree' /\ gc /\ gv /\ sdb) = [sdb, gc, gv, tree']
203

204
    outer = RH.div { className: "col-md-12" }
205
    inner h = RH.div { className: "container-fluid " <> hClass }
206 207
      where
        hClass = case h of
208
          Types.LeftHanded  -> "lefthanded"
209
          Types.RightHanded -> "righthanded"
210 211
    -- rowToggle  = RH.div { id: "toggle-container" }
    rowToggle  = RH.ul { className: "navbar-nav ml-auto mr-auto" }
212
    rowControls = RH.div { id: "controls-container" }
213 214
    -- col       = RH.div { className: "col-md-4" }
    col = RH.li { className: "nav-item" }
215
    pullLeft  = RH.div { className: "pull-left"  }
James Laver's avatar
James Laver committed
216
    pullRight = RH.div { className: "pull-right" }
217 218
    -- spaces    = RH.div { className: "flex-space-between" }
    spaces = RH.a { className: "nav-link" }
219

220

221 222
    tree :: Record TreeProps -> R.Element
    tree { show: false } = RH.div { id: "tree" } []
223
    tree { asyncTasksRef, backend, frontends, handed, currentRoute, reload, sessions, showLogin, treeReloadRef } =
224
      RH.div {className: "col-md-2 graph-tree"} [
225 226 227
        forest { appReload: reload
               , asyncTasksRef
               , backend
228
               , currentRoute
229 230 231 232
               , frontends
               , handed
               , sessions
               , showLogin
233
               , treeReloadRef } []
234
      ]
235

236
    mSidebar :: Maybe GET.MetaData
237
             -> Record MSidebarProps
238
             -> R.Element
239
    mSidebar  Nothing            _ = RH.div {} []
240 241 242
    mSidebar (Just metaData) props =
      Sidebar.sidebar (Record.merge props { metaData })

243 244
type TreeProps =
  (
245
    asyncTasksRef :: R.Ref (Maybe GAT.Reductor)
246
  , backend       :: R.State (Maybe Backend)
247
  , currentRoute  :: AppRoute
248 249
  , frontends     :: Frontends
  , handed        :: Types.Handed
250
  , reload        :: GUR.ReloadS
251 252 253
  , sessions      :: Sessions
  , show          :: Boolean
  , showLogin     :: R.Setter Boolean
254
  , treeReloadRef :: GUR.ReloadWithInitializeRef
255 256
  )

257
type MSidebarProps =
258 259
  ( frontends       :: Frontends
  , graph           :: SigmaxT.SGraph
260
  , graphId         :: GET.GraphId
261
  , graphVersion    :: GUR.ReloadS
262 263
  , removedNodeIds  :: R.State SigmaxT.NodeIds
  , showSidePanel   :: R.State GET.SidePanelState
264
  , selectedNodeIds :: R.State SigmaxT.NodeIds
265
  , session         :: Session
266
  , treeReload      :: GUR.ReloadS
267
  )
268 269

type GraphProps = (
270 271 272 273 274 275
    controls              :: Record Controls.Controls
  , elRef                 :: R.Ref (Nullable Element)
  , graphId               :: GET.GraphId
  , graph                 :: SigmaxT.SGraph
  , hyperdataGraph        :: GET.HyperdataGraph
  , mMetaData             :: Maybe GET.MetaData
276
  , multiSelectEnabledRef :: R.Ref Boolean
277 278
)

279
graphView :: Record GraphProps -> R.Element
280
--graphView sigmaRef props = R.createElement (R.memo el memoCmp) props []
281 282 283
graphView props = R.createElement graphViewCpt props []

graphViewCpt :: R.Component GraphProps
284
graphViewCpt = R.hooksComponentWithModule thisModule "graphView" cpt
285
  where
286 287 288 289 290 291 292
    cpt { controls
        , elRef
        , graphId
        , graph
        , hyperdataGraph: GET.HyperdataGraph { mCamera }
        , mMetaData
        , multiSelectEnabledRef } _children = do
293
      -- TODO Cache this?
294 295 296
      let louvainGraph =
            if (fst controls.showLouvain) then
              let louvain = Louvain.louvain unit in
297 298
              let cluster = Louvain.init louvain (SigmaxT.louvainNodes graph) (SigmaxT.louvainEdges graph) in
              SigmaxT.louvainGraph graph cluster
299 300 301
            else
              graph
      let transformedGraph = transformGraph controls louvainGraph
302
      let startForceAtlas = maybe true (\(GET.MetaData { startForceAtlas }) -> startForceAtlas) mMetaData
303

304
      R.useEffect1' (fst controls.multiSelectEnabled) $ do
305 306
        R.setRef multiSelectEnabledRef $ fst controls.multiSelectEnabled

307 308 309 310 311 312 313 314 315 316 317 318 319
      pure $ Graph.graph { elRef
                         , forceAtlas2Settings: Graph.forceAtlas2Settings
                         , graph
                         , mCamera
                         , multiSelectEnabledRef
                         , selectedNodeIds: controls.selectedNodeIds
                         , showEdges: controls.showEdges
                         , sigmaRef: controls.sigmaRef
                         , sigmaSettings: Graph.sigmaSettings
                         , stage: controls.graphStage
                         , startForceAtlas
                         , transformedGraph
                         }
320

321 322
convert :: GET.GraphData -> Tuple (Maybe GET.MetaData) SigmaxT.SGraph
convert (GET.GraphData r) = Tuple r.metaData $ SigmaxT.Graph {nodes, edges}
James Laver's avatar
James Laver committed
323 324
  where
    nodes = foldMapWithIndex nodeFn r.nodes
325 326 327
    nodeFn _i nn@(GET.Node n) =
      Seq.singleton {
          borderColor: color
328
        , color : color
329 330
        , equilateral: { numPoints: 3 }
        , gargType
331
        , hidden : false
332
        , id    : n.id_
James Laver's avatar
James Laver committed
333
        , label : n.label
334
        , size  : log (toNumber n.size + 1.0)
335
        , type  : modeGraphType gargType
James Laver's avatar
James Laver committed
336 337
        , x     : n.x -- cos (toNumber i)
        , y     : n.y -- sin (toNumber i)
338
        , _original: nn
James Laver's avatar
James Laver committed
339 340
        }
      where
341
        cDef (GET.Cluster {clustDefault}) = clustDefault
342
        color = GET.intColor (cDef n.attributes)
343
        gargType =  unsafePartial $ fromJust $ Types.modeFromString n.type_
344
    nodesMap = SigmaxT.nodesMap nodes
345
    edges = foldMapWithIndex edgeFn $ A.sortWith (\(GET.Edge {weight}) -> weight) r.edges
346
    edgeFn i ee@(GET.Edge e) =
347 348 349 350 351 352 353 354 355 356 357 358
      Seq.singleton
        { id : e.id_
        , color
        , confluence : e.confluence
        , hidden : false
        , size: 1.0
        , source : e.source
        , sourceNode
        , target : e.target
        , targetNode
        , weight : e.weight
        , weightIdx: i
359
        , _original: ee
360
        }
361
      where
362 363 364
        sourceNode = unsafePartial $ fromJust $ Map.lookup e.source nodesMap
        targetNode = unsafePartial $ fromJust $ Map.lookup e.target nodesMap
        color = sourceNode.color
James Laver's avatar
James Laver committed
365

366 367 368 369 370 371
-- | See sigmajs/plugins/sigma.renderers.customShapes/shape-library.js
modeGraphType :: Types.Mode -> String
modeGraphType Types.Authors = "square"
modeGraphType Types.Institutes = "equilateral"
modeGraphType Types.Sources = "star"
modeGraphType Types.Terms = "def"
James Laver's avatar
James Laver committed
372 373


374 375
getNodes :: Session -> GUR.ReloadS -> GET.GraphId -> Aff GET.HyperdataGraph
getNodes session graphVersion graphId =
376 377
  get session $ NodeAPI Types.Graph
                        (Just graphId)
378
                        ("?version=" <> (show $ GUR.value graphVersion))
379 380


381 382
transformGraph :: Record Controls.Controls -> SigmaxT.SGraph -> SigmaxT.SGraph
transformGraph controls graph = SigmaxT.Graph {nodes: newNodes, edges: newEdges}
383
  where
384 385
    edges = SigmaxT.graphEdges graph
    nodes = SigmaxT.graphNodes graph
386 387 388
    selectedEdgeIds =
      Set.fromFoldable
        $ Seq.map _.id
389
        $ SigmaxT.neighbouringEdges graph (fst controls.selectedNodeIds)
390 391
    hasSelection = not $ Set.isEmpty (fst controls.selectedNodeIds)

392 393 394 395
    newEdges' = Seq.filter edgeFilter $ Seq.map (
      edgeHideWeight <<< edgeHideConfluence <<< edgeShowFilter <<< edgeMarked
      ) edges
    newNodes  = Seq.filter nodeFilter $ Seq.map (nodeMarked <<< nodeHideSize) nodes
396
    newEdges  = Seq.filter (edgeInGraph $ Set.fromFoldable $ Seq.map _.id newNodes) newEdges'
397

398 399 400 401
    edgeFilter e = true
    nodeFilter n = nodeRemovedFilter n

    nodeSizeFilter :: Record SigmaxT.Node -> Boolean
402 403
    nodeSizeFilter node@{ size } = Range.within (fst controls.nodeSize) size

404 405
    nodeRemovedFilter node@{ id } = not $ Set.member id $ fst controls.removedNodeIds

406
    edgeConfluenceFilter :: Record SigmaxT.Edge -> Boolean
407
    edgeConfluenceFilter edge@{ confluence } = Range.within (fst controls.edgeConfluence) confluence
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
    edgeWeightFilter :: Record SigmaxT.Edge -> Boolean
    edgeWeightFilter edge@{ weightIdx } = Range.within (fst controls.edgeWeight) $ toNumber weightIdx

    edgeHideConfluence :: Record SigmaxT.Edge -> Record SigmaxT.Edge
    edgeHideConfluence edge@{ confluence } =
      if Range.within (fst controls.edgeConfluence) confluence then
        edge
      else
        edge { hidden = true }

    edgeHideWeight :: Record SigmaxT.Edge -> Record SigmaxT.Edge
    edgeHideWeight edge@{ weightIdx } =
      if Range.within (fst controls.edgeWeight) $ toNumber weightIdx then
        edge
      else
        edge { hidden = true }

    edgeShowFilter :: Record SigmaxT.Edge -> Record SigmaxT.Edge
426
    edgeShowFilter edge =
427
      if (SigmaxT.edgeStateHidden $ fst controls.showEdges) then
428 429 430
        edge { hidden = true }
      else
        edge
431

432
    edgeInGraph :: SigmaxT.NodeIds -> Record SigmaxT.Edge -> Boolean
433
    edgeInGraph nodeIds e = (Set.member e.source nodeIds) && (Set.member e.target nodeIds)
434

435
    edgeMarked :: Record SigmaxT.Edge -> Record SigmaxT.Edge
436
    edgeMarked edge@{ id, sourceNode } = do
437
      let isSelected = Set.member id selectedEdgeIds
438 439
      case Tuple hasSelection isSelected of
        Tuple false true  -> edge { color = "#ff0000" }
440 441
        Tuple true  true  -> edge { color = sourceNode.color }
        Tuple true false  -> edge { color = "rgba(221, 221, 221, 0.5)" }
442
        _                 -> edge
443 444

    nodeMarked :: Record SigmaxT.Node -> Record SigmaxT.Node
445 446
    nodeMarked node@{ id } =
      if Set.member id (fst controls.selectedNodeIds) then
447
        node { borderColor = "#000", type = "selected" }
448 449
      else
        node
450 451 452 453 454 455 456

    nodeHideSize :: Record SigmaxT.Node -> Record SigmaxT.Node
    nodeHideSize node@{ size } =
      if Range.within (fst controls.nodeSize) size then
        node
      else
        node { hidden = true }