GraphExplorer.purs 13.5 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)
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 24
import Gargantext.Components.Forest (forest)
import Gargantext.Components.Graph as Graph
James Laver's avatar
James Laver committed
25
import Gargantext.Components.GraphExplorer.Controls as Controls
26
import Gargantext.Components.GraphExplorer.Sidebar as Sidebar
James Laver's avatar
James Laver committed
27
import Gargantext.Components.GraphExplorer.ToggleButton as Toggle
28
import Gargantext.Components.GraphExplorer.Types as GET
29
import Gargantext.Data.Louvain as Louvain
30
import Gargantext.Ends (Frontends)
31 32
import Gargantext.Hooks.Loader (useLoader)
import Gargantext.Hooks.Sigmax as Sigmax
33
import Gargantext.Hooks.Sigmax.Types as SigmaxT
34
import Gargantext.Routes (SessionRoute(NodeAPI), AppRoute)
35
import Gargantext.Sessions (Session, Sessions, get)
36
import Gargantext.Types as Types
37
import Gargantext.Utils.Range as Range
38
import Gargantext.Utils.Reactix as R2
James Laver's avatar
James Laver committed
39

40 41
type GraphId = Int

42
type LayoutProps =
43 44
  ( frontends :: Frontends
  , graphId :: GraphId
45
  , mCurrentRoute :: AppRoute
46
  , session :: Session
47
  , sessions :: Sessions
48
  , showLogin :: R.State Boolean
49
  , treeReload :: R.State Int
50
  )
51

52
type Props = (
53
    graph :: SigmaxT.SGraph
54
  , graphVersion :: R.State Int
55 56
  , mMetaData :: Maybe GET.MetaData
  | LayoutProps
57
  )
James Laver's avatar
James Laver committed
58

59
--------------------------------------------------------------
60 61
explorerLayout :: Record LayoutProps -> R.Element
explorerLayout props = R.createElement explorerLayoutCpt props []
62

63 64
explorerLayoutCpt :: R.Component LayoutProps
explorerLayoutCpt = R.hooksComponent "G.C.GraphExplorer.explorerLayout" cpt
65
  where
66 67 68 69 70 71 72 73 74
    cpt props _ = do
      graphVersion <- R.useState' 0

      pure $ explorerLayoutView graphVersion props

explorerLayoutView :: R.State Int -> Record LayoutProps -> R.Element
explorerLayoutView graphVersion p = R.createElement el p []
  where
    el = R.hooksComponent "G.C.GE.explorerLayoutView" cpt
75
    cpt props@{graphId, session} _ = do
76
      useLoader graphId (getNodes session graphVersion) handler
77
      where
78
        handler loaded =
79
          explorer (Record.merge props { graph, graphVersion, mMetaData })
80
          where (Tuple mMetaData graph) = convert loaded
81

82
--------------------------------------------------------------
83 84
explorer :: Record Props -> R.Element
explorer props = R.createElement explorerCpt props []
James Laver's avatar
James Laver committed
85

86 87
explorerCpt :: R.Component Props
explorerCpt = R.hooksComponent "G.C.GraphExplorer.explorer" cpt
James Laver's avatar
James Laver committed
88
  where
89
    cpt props@{frontends, graph, graphId, graphVersion, mCurrentRoute, mMetaData, session, sessions, showLogin, treeReload } _ = do
90
      dataRef <- R.useRef graph
91
      graphRef <- R.useRef null
92 93
      graphVersionRef       <- R.useRef (fst graphVersion)
      controls              <- Controls.useGraphControls graph
94
      multiSelectEnabledRef <- R.useRef $ fst controls.multiSelectEnabled
95

96
      R.useEffect' $ do
97
        let readData = R.readRef dataRef
98
        let gv = R.readRef graphVersionRef
99
        if SigmaxT.eqGraph readData graph then
100 101
          pure unit
        else do
102
          -- Graph data changed, reinitialize sigma.
103 104 105
          let rSigma = R.readRef controls.sigmaRef
          Sigmax.cleanupSigma rSigma "explorerCpt"
          R.setRef dataRef graph
106
          R.setRef graphVersionRef (fst graphVersion)
107
          -- Reinitialize bunch of state as well.
108
          snd controls.removedNodeIds  $ const SigmaxT.emptyNodeIds
109
          snd controls.selectedNodeIds $ const SigmaxT.emptyNodeIds
110
          snd controls.showEdges       $ const SigmaxT.EShow
111
          snd controls.forceAtlasState $ const SigmaxT.InitialRunning
112 113
          snd controls.graphStage      $ const Graph.Init
          snd controls.showSidePanel   $ const GET.InitialClosed
114

James Laver's avatar
James Laver committed
115
      pure $
116 117
        RH.div
          { id: "graph-explorer" }
118
          [ rowToggle
119 120 121
                  [ col [ spaces [ Toggle.treeToggleButton controls.showTree         ]]
                  , col [ spaces [ Toggle.controlsToggleButton controls.showControls ]]
                  , col [ spaces [ Toggle.sidebarToggleButton controls.showSidePanel ]]
122 123 124 125
                  ], R2.row
            [ outer
              [ inner
                [ rowControls [ Controls.controls controls ]
126
                , R2.row [
127 128 129 130 131 132
                        tree { frontends
                             , mCurrentRoute
                             , reload: props.treeReload
                             , sessions
                             , show: fst controls.showTree
                             , showLogin: snd showLogin }
133
                      , RH.div { ref: graphRef, id: "graph-view", className: "col-md-12" } []  -- graph container
134 135 136 137
                      , graphView { controls
                                  , elRef: graphRef
                                  , graphId
                                  , graph
138
                                  , multiSelectEnabledRef
139
                                  }
140 141
                      , mSidebar mMetaData { frontends
                                           , graph
142
                                           , graphId
143
                                           , graphVersion
144
                                           , removedNodeIds : controls.removedNodeIds
145 146
                                           , session
                                           , selectedNodeIds: controls.selectedNodeIds
147
                                           , showSidePanel  :   controls.showSidePanel
148
                                           , treeReload
149
                                           }
150
                      ]
151
                ]
152
              ]
James Laver's avatar
James Laver committed
153 154
            ]
          ]
155

156
    outer = RH.div { className: "col-md-12" }
James Laver's avatar
James Laver committed
157
    inner = RH.div { className: "container-fluid", style: { paddingTop: "90px" } }
158 159
    rowToggle  = RH.div { id: "toggle-container" }
    rowControls = RH.div { id: "controls-container" }
160
    col       = RH.div { className: "col-md-4" }
161
    pullLeft  = RH.div { className: "pull-left"  }
James Laver's avatar
James Laver committed
162
    pullRight = RH.div { className: "pull-right" }
163 164
    spaces    = RH.div { className: "flex-space-between" }

165

166 167 168 169 170 171
    tree :: Record TreeProps -> R.Element
    tree { show: false } = RH.div { id: "tree" } []
    tree { frontends, mCurrentRoute: route, reload, sessions, showLogin } =
      RH.div {className: "col-md-2 graph-tree"} [
        forest { frontends, reload, route, sessions, showLogin }
      ]
172

173
    mSidebar :: Maybe GET.MetaData
174
             -> Record MSidebarProps
175
             -> R.Element
176
    mSidebar  Nothing            _ = RH.div {} []
177 178 179
    mSidebar (Just metaData) props =
      Sidebar.sidebar (Record.merge props { metaData })

180 181 182 183 184 185 186 187 188 189
type TreeProps =
  (
    frontends :: Frontends
  , mCurrentRoute :: AppRoute
  , reload :: R.State Int
  , sessions :: Sessions
  , show :: Boolean
  , showLogin :: R2.Setter Boolean
  )

190
type MSidebarProps =
191 192 193 194 195 196
  ( frontends       :: Frontends
  , graph           :: SigmaxT.SGraph
  , graphId         :: GraphId
  , graphVersion    :: R.State Int
  , removedNodeIds  :: R.State SigmaxT.NodeIds
  , showSidePanel   :: R.State GET.SidePanelState
197
  , selectedNodeIds :: R.State SigmaxT.NodeIds
198 199
  , session         :: Session
  , treeReload      :: R.State Int
200
  )
201 202

type GraphProps = (
203 204 205
    controls :: Record Controls.Controls
  , elRef :: R.Ref (Nullable Element)
  , graphId :: GraphId
206
  , graph :: SigmaxT.SGraph
207
  , multiSelectEnabledRef :: R.Ref Boolean
208 209
)

210
graphView :: Record GraphProps -> R.Element
211
--graphView sigmaRef props = R.createElement (R.memo el memoCmp) props []
212 213 214 215
graphView props = R.createElement graphViewCpt props []

graphViewCpt :: R.Component GraphProps
graphViewCpt = R.hooksComponent "GraphView" cpt
216
  where
217
    cpt {controls, elRef, graphId, graph, multiSelectEnabledRef} _children = do
218
      -- TODO Cache this?
219 220 221
      let louvainGraph =
            if (fst controls.showLouvain) then
              let louvain = Louvain.louvain unit in
222 223
              let cluster = Louvain.init louvain (SigmaxT.louvainNodes graph) (SigmaxT.louvainEdges graph) in
              SigmaxT.louvainGraph graph cluster
224 225 226
            else
              graph
      let transformedGraph = transformGraph controls louvainGraph
227

228
      R.useEffect1' (fst controls.multiSelectEnabled) $ do
229 230
        R.setRef multiSelectEnabledRef $ fst controls.multiSelectEnabled

231 232 233 234
      pure $ Graph.graph {
          elRef
        , forceAtlas2Settings: Graph.forceAtlas2Settings
        , graph
235 236
        , multiSelectEnabledRef
        , selectedNodeIds: controls.selectedNodeIds
237
        , showEdges: controls.showEdges
238
        , sigmaRef: controls.sigmaRef
239
        , sigmaSettings: Graph.sigmaSettings
240
        , stage: controls.graphStage
241
        , transformedGraph
242
        }
243

244 245
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
246 247
  where
    nodes = foldMapWithIndex nodeFn r.nodes
248
    nodeFn _i (GET.Node n) =
James Laver's avatar
James Laver committed
249
      Seq.singleton
250 251
        { borderColor: color
        , color : color
252 253
        , equilateral: { numPoints: 3 }
        , gargType
254
        , hidden : false
255
        , id    : n.id_
James Laver's avatar
James Laver committed
256
        , label : n.label
257
        , size  : log (toNumber n.size + 1.0)
258
        , type  : modeGraphType gargType
James Laver's avatar
James Laver committed
259 260 261 262
        , x     : n.x -- cos (toNumber i)
        , y     : n.y -- sin (toNumber i)
        }
      where
263
        cDef (GET.Cluster {clustDefault}) = clustDefault
264
        color = GET.intColor (cDef n.attributes)
265
        gargType =  unsafePartial $ fromJust $ Types.modeFromString n.type_
266
    nodesMap = SigmaxT.nodesMap nodes
267
    edges = foldMapWithIndex edgeFn $ A.sortWith (\(GET.Edge {weight}) -> weight) r.edges
268 269 270 271 272 273 274 275 276 277 278 279 280 281
    edgeFn i (GET.Edge e) =
      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
        }
282
      where
283 284 285
        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
286

287 288 289 290 291 292
-- | 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
293 294


295 296
getNodes :: Session -> R.State Int -> GraphId -> Aff GET.GraphData
getNodes session (graphVersion /\ _) graphId = get session $ NodeAPI Types.Graph (Just graphId) ("?version=" <> show graphVersion)
297 298


299 300
transformGraph :: Record Controls.Controls -> SigmaxT.SGraph -> SigmaxT.SGraph
transformGraph controls graph = SigmaxT.Graph {nodes: newNodes, edges: newEdges}
301
  where
302 303
    edges = SigmaxT.graphEdges graph
    nodes = SigmaxT.graphNodes graph
304 305 306
    selectedEdgeIds =
      Set.fromFoldable
        $ Seq.map _.id
307
        $ SigmaxT.neighbouringEdges graph (fst controls.selectedNodeIds)
308 309
    hasSelection = not $ Set.isEmpty (fst controls.selectedNodeIds)

310 311 312 313 314 315 316 317 318
    --newNodes = Seq.map (nodeSizeFilter <<< nodeMarked) nodes
    --newEdges = Seq.map (edgeConfluenceFilter <<< edgeWeightFilter <<< edgeShowFilter <<< edgeMarked) edges
    newEdges' = Seq.filter edgeFilter $ Seq.map (edgeShowFilter <<< edgeMarked) edges
    newNodes = Seq.filter nodeFilter $ Seq.map (nodeMarked) nodes
    newEdges = Seq.filter (edgeInGraph $ Set.fromFoldable $ Seq.map _.id newNodes) newEdges'

    edgeFilter e = edgeConfluenceFilter e &&
                   edgeWeightFilter e
                   --edgeShowFilter e
319 320
    nodeFilter n = nodeSizeFilter n &&
                   nodeRemovedFilter n
321 322 323 324 325 326 327 328

    --nodeSizeFilter node@{ size } =
    --  if Range.within (fst controls.nodeSize) size then
    --    node
    --  else
    --    node { hidden = true }
    nodeSizeFilter node@{ size } = Range.within (fst controls.nodeSize) size

329 330
    nodeRemovedFilter node@{ id } = not $ Set.member id $ fst controls.removedNodeIds

331 332 333 334 335 336
    --edgeConfluenceFilter edge@{ confluence } =
    --  if Range.within (fst controls.edgeConfluence) confluence then
    --    edge
    --  else
    --    edge { hidden = true }
    edgeConfluenceFilter edge@{ confluence } = Range.within (fst controls.edgeConfluence) confluence
337
    edgeShowFilter edge =
338
      if (SigmaxT.edgeStateHidden $ fst controls.showEdges) then
339 340 341
        edge { hidden = true }
      else
        edge
342 343 344 345 346
    --edgeWeightFilter edge@{ weight } =
    --  if Range.within (fst controls.edgeWeight) weight then
    --    edge
    --  else
    --    edge { hidden = true }
347
    edgeWeightFilter :: Record SigmaxT.Edge -> Boolean
348
    edgeWeightFilter edge@{ weightIdx } = Range.within (fst controls.edgeWeight) $ toNumber weightIdx
349

350
    edgeInGraph :: SigmaxT.NodeIds -> Record SigmaxT.Edge -> Boolean
351
    edgeInGraph nodeIds e = (Set.member e.source nodeIds) && (Set.member e.target nodeIds)
352

353
    edgeMarked edge@{ id, sourceNode } = do
354
      let isSelected = Set.member id selectedEdgeIds
355 356
      case Tuple hasSelection isSelected of
        Tuple false true  -> edge { color = "#ff0000" }
357 358
        Tuple true  true  -> edge { color = sourceNode.color }
        Tuple true false  -> edge { color = "rgba(221, 221, 221, 0.5)" }
359
        _                 -> edge
360 361
    nodeMarked node@{ id } =
      if Set.member id (fst controls.selectedNodeIds) then
362
        node { borderColor = "#000", type = "selected" }
363 364
      else
        node