Skip to content
This repository has been archived by the owner on Feb 2, 2024. It is now read-only.

Commit

Permalink
Adding force-simulation layout algorithm to draw nodes (#119)
Browse files Browse the repository at this point in the history
* Adding 4 layout drawing algorithms

* Tuning fcose layout

* Jumping behavior fix and reset button

* Grid layout as default

* Removing unpromising layouts

* Adding klay layout, expanding button click area

* Removing unuse cola, dagre layouts

* Show uppercase Layout names, show Play on FCoSE

* Re-arrange name, avoid the first reload animation
  • Loading branch information
henrypalacios authored Jun 22, 2022
1 parent 2a59063 commit 6c22611
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 84 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
"bn.js": "^4.11.9",
"combine-reducers": "^1.0.0",
"cytoscape": "^3.21.0",
"cytoscape-fcose": "^2.1.0",
"cytoscape-klay": "^3.1.4",
"cytoscape-no-overlap": "^1.0.1",
"cytoscape-popper": "^2.0.0",
"date-fns": "^2.9.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,38 +75,22 @@ export default class ElementsBuilder {
}

build(customLayoutNodes?: CustomLayoutNodes): ElementDefinition[] {
let edges = addClassWithMoreThanOneBidirectional(this._edges, this._countEdgeDirection)
edges = addClassWithKind(edges)
if (!customLayoutNodes) {
return this._buildCoseLayout()
} else {
const edges = addClassWithMoreThanOneBidirectional(this._edges, this._countEdgeDirection)
const { center, nodes } = customLayoutNodes
return [center, ...nodes, ...addClassWithKind(edges)]
return this._buildLayout(edges)
}

const { center, nodes } = customLayoutNodes
return [center, ...nodes, ...edges]
}

_buildCoseLayout(): ElementDefinition[] {
_buildLayout(edges: ElementDefinition[]): ElementDefinition[] {
if (!this._center) {
throw new Error('Center node is required')
}
const center = {
...this._center,
position: { x: 0, y: 0 },
}
const nTypes = this._countNodeTypes.size

const r = this._SIZE / nTypes - 100 // get radio

const nodes = this._nodes.map((node: ElementDefinition, index: number) => {
return {
...node,
position: {
x: r * Math.cos((nTypes * Math.PI * index) / this._nodes.length),
y: r * Math.sin((nTypes * Math.PI * index) / this._nodes.length),
},
}
})

return [center, ...nodes, ...this._edges]
return [this._center, ...this._nodes, ...edges]
}

getById(id: string): ElementDefinition | undefined {
Expand Down Expand Up @@ -222,7 +206,7 @@ function addClassWithMoreThanOneBidirectional(
function addClassWithKind(edges: ElementDefinition[]): ElementDefinition[] {
return edges.map((_edge) => {
const CLASS_NAME = _edge.data.kind
_edge.classes = _edge.classes ? `${_edge.classes},${CLASS_NAME}` : CLASS_NAME
_edge.classes = _edge.classes ? `${_edge.classes} ${CLASS_NAME}` : CLASS_NAME
return _edge
})
}
120 changes: 83 additions & 37 deletions src/apps/explorer/components/TransanctionBatchGraph/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import Cytoscape, {
ElementDefinition,
NodeSingular,
NodeDataDefinition,
EdgeDataDefinition,
EventObject,
} from 'cytoscape'
import Cytoscape, { ElementDefinition, NodeDataDefinition, EdgeDataDefinition, EventObject } from 'cytoscape'
import popper from 'cytoscape-popper'
import noOverlap from 'cytoscape-no-overlap'
import fcose from 'cytoscape-fcose'
import klay from 'cytoscape-klay'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import CytoscapeComponent from 'react-cytoscapejs'
import styled, { useTheme } from 'styled-components'
import BigNumber from 'bignumber.js'
import { OrderKind } from '@gnosis.pm/gp-v2-contracts'
import { faRedo } from '@fortawesome/free-solid-svg-icons'
import {
faRedo,
faDiceOne,
faDiceTwo,
faDiceThree,
faDiceFour,
faDiceFive,
IconDefinition,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

import { GetTxBatchTradesResult as TxBatchData, Settlement as TxSettlement } from 'hooks/useTxBatchTrades'
Expand All @@ -23,15 +27,19 @@ import ElementsBuilder, { buildGridLayout } from 'apps/explorer/components/Trans
import { TypeEdgeOnTx, TypeNodeOnTx } from './types'
import { APP_NAME } from 'const'
import { HEIGHT_HEADER_FOOTER, TOKEN_SYMBOL_UNKNOWN } from 'apps/explorer/const'
import { STYLESHEET, ResetButton } from './styled'
import { STYLESHEET, ResetButton, LayoutButton, DropdownWrapper, FloatingWrapper } from './styled'
import { abbreviateString, FormatAmountPrecision, formattingAmountPrecision } from 'utils'
import CowLoading from 'components/common/CowLoading'
import { media } from 'theme/styles/media'
import { EmptyItemWrapper } from 'components/common/StyledUserDetailsTable'
import useWindowSizes from 'hooks/useWindowSizes'
import { layouts, LayoutNames } from './layouts'
import { DropdownOption, DropdownPosition } from 'apps/explorer/components/common/Dropdown'

Cytoscape.use(popper)
Cytoscape.use(noOverlap)
Cytoscape.use(fcose)
Cytoscape.use(klay)

const PROTOCOL_NAME = APP_NAME
const WrapperCytoscape = styled(CytoscapeComponent)`
Expand All @@ -43,6 +51,7 @@ const WrapperCytoscape = styled(CytoscapeComponent)`
margin: 1.6rem 0;
}
`
const iconDice = [faDiceOne, faDiceTwo, faDiceThree, faDiceFour, faDiceFive]

function getTypeNode(account: Account): TypeNodeOnTx {
let type = TypeNodeOnTx.Dex
Expand Down Expand Up @@ -76,7 +85,12 @@ function getNetworkParentNode(account: Account, networkName: string): string | u
return account.alias !== ALIAS_TRADER_NAME ? networkName : undefined
}

function getNodes(txSettlement: TxSettlement, networkId: Network, heightSize: number): ElementDefinition[] {
function getNodes(
txSettlement: TxSettlement,
networkId: Network,
heightSize: number,
layout: string,
): ElementDefinition[] {
if (!txSettlement.accounts) return []

const networkName = networkOptions.find((network) => network.id === networkId)?.name
Expand Down Expand Up @@ -122,7 +136,9 @@ function getNodes(txSettlement: TxSettlement, networkId: Network, heightSize: nu
})

return builder.build(
buildGridLayout(builder._countNodeTypes as Map<TypeNodeOnTx, number>, builder._center, builder._nodes),
layout === 'grid'
? buildGridLayout(builder._countNodeTypes as Map<TypeNodeOnTx, number>, builder._center, builder._nodes)
: undefined,
)
}

Expand Down Expand Up @@ -211,18 +227,27 @@ interface GraphBatchTxParams {
networkId: Network | undefined
}

function getLayout(): Cytoscape.LayoutOptions {
return {
name: 'grid',
position: function (node: NodeSingular): { row: number; col: number } {
return { row: node.data('row'), col: node.data('col') }
},
fit: true, // whether to fit the viewport to the graph
padding: 10, // padding used on fit
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false,
}
function DropdownButtonContent({
layout,
icon,
open,
}: {
layout: string
icon: IconDefinition
open?: boolean
}): JSX.Element {
return (
<>
<FontAwesomeIcon icon={icon} />
<span>Layout: {layout}</span>
<span className={`arrow ${open && 'open'}`} />
</>
)
}

const updateLayout = (cy: Cytoscape.Core, layoutName: string, noAnimation = false): void => {
cy.layout(noAnimation ? { ...layouts[layoutName], animate: false } : layouts[layoutName]).run()
cy.fit()
}

function TransanctionBatchGraph({
Expand All @@ -233,32 +258,34 @@ function TransanctionBatchGraph({
const cytoscapeRef = useRef<Cytoscape.Core | null>(null)
const cyPopperRef = useRef<PopperInstance | null>(null)
const [resetZoom, setResetZoom] = useState<boolean | null>(null)
const [layout, setLayout] = useState(layouts.grid)
const theme = useTheme()
const { innerHeight } = useWindowSizes()
const heightSize = innerHeight && innerHeight - HEIGHT_HEADER_FOOTER
const currentLayoutIndex = Object.keys(LayoutNames).findIndex((nameLayout) => nameLayout === layout.name)

const setCytoscape = useCallback(
(ref: Cytoscape.Core) => {
cytoscapeRef.current = ref
const updateLayout = (): void => {
ref.layout(getLayout()).run()
ref.fit()
}
ref.removeListener('resize')
ref.on('resize', () => {
updateLayout()
updateLayout(ref, layout.name, true)
})
updateLayout()
},
[cytoscapeRef],
[layout.name],
)

useEffect(() => {
const cy = cytoscapeRef.current
setElements([])
if (error || isLoading || !networkId || !heightSize) return
if (error || isLoading || !networkId || !heightSize || !cy) return

setElements(getNodes(txSettlement, networkId, heightSize))
setElements(getNodes(txSettlement, networkId, heightSize, layout.name))
if (resetZoom) {
updateLayout(cy, layout.name)
}
setResetZoom(null)
}, [error, isLoading, txSettlement, networkId, heightSize, resetZoom])
}, [error, isLoading, txSettlement, networkId, heightSize, resetZoom, layout.name])

useEffect(() => {
const cy = cytoscapeRef.current
Expand Down Expand Up @@ -292,7 +319,7 @@ function TransanctionBatchGraph({
<>
<WrapperCytoscape
elements={elements}
layout={getLayout()}
layout={layout}
style={{ width: '100%', height: heightSize }}
stylesheet={STYLESHEET(theme)}
cy={setCytoscape}
Expand All @@ -302,9 +329,28 @@ function TransanctionBatchGraph({
minZoom={0.1}
zoom={1}
/>
<ResetButton type="button" onClick={(): void => setResetZoom(!resetZoom)}>
<FontAwesomeIcon icon={faRedo} /> <span>Reset</span>
</ResetButton>
<FloatingWrapper>
<ResetButton type="button" onClick={(): void => setResetZoom(!resetZoom)}>
<FontAwesomeIcon icon={faRedo} /> <span>{layout.name === 'fcose' ? 'Re-arrange' : 'Reset'}</span>
</ResetButton>
<LayoutButton>
<DropdownWrapper
currentItem={currentLayoutIndex}
dropdownButtonContent={
<DropdownButtonContent icon={iconDice[currentLayoutIndex]} layout={LayoutNames[layout.name]} />
}
dropdownButtonContentOpened={
<DropdownButtonContent icon={iconDice[currentLayoutIndex]} layout={LayoutNames[layout.name]} open />
}
items={Object.values(LayoutNames).map((layoutName) => (
<DropdownOption key={layoutName} onClick={(): void => setLayout(layouts[layoutName.toLowerCase()])}>
{layoutName}
</DropdownOption>
))}
dropdownPosition={DropdownPosition.center}
/>
</LayoutButton>
</FloatingWrapper>
</>
)
}
Expand Down
103 changes: 103 additions & 0 deletions src/apps/explorer/components/TransanctionBatchGraph/layouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { LayoutOptions, NodeSingular } from 'cytoscape'

export type CytoscapeLayouts = 'grid' | 'klay' | 'fcose'

type CustomLayoutOptions = LayoutOptions & {
[key: string]: unknown
}

const defaultValues = {
padding: 10, // padding used on fit
animate: true,
fit: true, // whether to fit the viewport to the graph
}
export const layouts: Record<CytoscapeLayouts, CustomLayoutOptions> = {
grid: {
...defaultValues,
name: 'grid',
position: (node: NodeSingular): { row: number; col: number } => ({ row: node.data('row'), col: node.data('col') }),
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false,
condense: false,
},
klay: {
...defaultValues,
name: 'klay',
klay: {
addUnnecessaryBendpoints: true, // Adds bend points even if an edge does not change direction.
aspectRatio: 1.6, // The aimed aspect ratio of the drawing, that is the quotient of width by height
borderSpacing: 20, // Minimal amount of space to be left to the border
compactComponents: false, // Tries to further compact components (disconnected sub-graphs).
edgeRouting: 'SPLINES',
edgeSpacingFactor: 2,
spacing: 20,
},
},
fcose: {
...defaultValues,
name: 'fcose',
quality: 'proof',
randomize: true,
animationDuration: 1000,
animationEasing: undefined,
nodeDimensionsIncludeLabels: false,
uniformNodeDimensions: false,
packComponents: true,
step: 'all',
/* spectral layout options */
// False for random, true for greedy sampling
samplingType: true,
// Sample size to construct distance matrix
sampleSize: 25,
// Separation amount between nodes
nodeSeparation: 75,
// Power iteration tolerance
piTol: 0.0000001,

/* incremental layout options */
// Node repulsion (non overlapping) multiplier
nodeRepulsion: (): number => 4500,
// Ideal edge (non nested) length
idealEdgeLength: (): number => 300,
// Divisor to compute edge forces
edgeElasticity: (): number => 0.01,
// Nesting factor (multiplier) to compute ideal edge length for nested edges
nestingFactor: 0.9,
// Maximum number of iterations to perform - this is a suggested value and might be adjusted by the algorithm as required
numIter: 2500,
// For enabling tiling
tile: true,
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingVertical: 10,
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingHorizontal: 10,
// Gravity force (constant)
gravity: 0.8,
// Gravity range (constant) for compounds
gravityRangeCompound: 1.5,
// Gravity force (constant) for compounds
gravityCompound: 1.0,
// Gravity range (constant)
gravityRange: 0.5,
// Initial cooling factor for incremental layout
initialEnergyOnIncremental: 0.3,

/* constraint options */
// Fix desired nodes to predefined positions
// [{nodeId: 'n1', position: {x: 100, y: 200}}, {...}]
fixedNodeConstraint: undefined,
// Align desired nodes in vertical/horizontal direction
// {vertical: [['n1', 'n2'], [...]], horizontal: [['n2', 'n4'], [...]]}
alignmentConstraint: undefined,
// Place two nodes relatively in vertical/horizontal direction
// [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}, {...}]
relativePlacementConstraint: undefined,
},
}

export enum LayoutNames {
grid = 'Grid',
klay = 'KLay',
fcose = 'FCoSE',
}
Loading

0 comments on commit 6c22611

Please sign in to comment.