Skip to content

Commit

Permalink
feat: add a flag to layout on change
Browse files Browse the repository at this point in the history
The layout on change flag controll if the layout should be run when data is added or removed. This
is on by default but turned off if a node is dragged.
  • Loading branch information
stuarthendren committed Mar 12, 2023
1 parent ead751b commit aeac4f2
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 12 deletions.
2 changes: 1 addition & 1 deletion apps/docs/src/Graph.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Column } from '@committed/components'
import { cytoscapeRenderer, Graph, GraphDebugControl } from '@committed/components-graph'
import { Meta } from '@storybook/react'
import React, { useState } from 'react'
import { Graph, cytoscapeRenderer, GraphDebugControl,} from '@committed/components-graph'
import { exampleModel } from './StoryUtil'

export default {
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/GraphToolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function isFunction(
const empty = {
zoom: false,
layout: false,
relayout: false,
refit: false,
hide: false,
size: false,
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/graph/src/graph/LayoutModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ it('Validating an validated layout does nothing', () => {
const model = layoutModel.setLayout('grid').validate()
expect(model.validate()).toBe(model)
})

it('Changing the onChange does not invalidate the layout', () => {
expect(layoutModel.setOnChange(false).isDirty()).toBe(false)
expect(layoutModel.setOnChange(true).isDirty()).toBe(false)
})
20 changes: 15 additions & 5 deletions packages/graph/src/graph/LayoutModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { CustomGraphLayout, GraphLayout, PresetGraphLayout } from './types'

export class LayoutModel {
private readonly layout: GraphLayout
private readonly onChange: boolean
private readonly invalidated: boolean

static createDefault(): LayoutModel {
return new LayoutModel('force-directed', true)
return new LayoutModel('force-directed', true, true)
}

constructor(layout: GraphLayout, invalidated = true) {
constructor(layout: GraphLayout, onChange = true, invalidated = true) {
this.layout = layout
this.onChange = onChange
this.invalidated = invalidated
}

Expand All @@ -21,24 +23,32 @@ export class LayoutModel {
return this.invalidated
}

isOnChange(): boolean {
return this.onChange
}

validate(): LayoutModel {
if (!this.invalidated) {
return this
} else {
return new LayoutModel(this.layout, false)
return new LayoutModel(this.layout, this.onChange, false)
}
}

invalidate(): LayoutModel {
if (this.invalidated) {
return this
} else {
return new LayoutModel(this.layout, true)
return new LayoutModel(this.layout, this.onChange, true)
}
}

setLayout(layout: GraphLayout): LayoutModel {
return new LayoutModel(layout, true)
return new LayoutModel(layout, this.onChange, true)
}

setOnChange(onChange: boolean): LayoutModel {
return new LayoutModel(this.layout, onChange, false)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Box, Button, Flex, Select, SelectItem } from '@committed/components'
import {
Box,
Button,
Checkbox,
Flex,
Select,
SelectItem,
} from '@committed/components'
import { Generator, GraphModel, PresetGraphLayout } from '@committed/graph'
import { defaultLayouts } from '../../graph'
import React from 'react'
import { defaultLayouts } from '../../graph'

const { addRandomNode, addRandomEdge, removeRandomNode, removeRandomEdge } =
Generator
Expand Down Expand Up @@ -44,6 +51,18 @@ export const GraphDebugControl: React.FC<GraphDebugControlProps> = ({
<Button onClick={() => onChange(removeRandomEdge(model))}>
Remove Random Edge
</Button>
<Checkbox
label="Layout on change"
checked={model.getCurrentLayout().isOnChange()}
onCheckedChange={(checked: boolean) =>
onChange(
GraphModel.applyLayout(
model,
model.getCurrentLayout().setOnChange(checked)
)
)
}
/>
<Button
onClick={() =>
onChange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Graph } from '../Graph'
const empty = {
zoom: false,
layout: false,
relayout: false,
refit: false,
hide: false,
size: false,
Expand Down Expand Up @@ -108,6 +109,10 @@ export const Layout: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} layout {...props} />
)

export const Relayout: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} relayout {...props} />
)

export const SizeBy: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} size {...props} />
)
Expand Down
13 changes: 12 additions & 1 deletion packages/react/src/components/GraphToolbar/GraphToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { GraphLayoutOptions } from './GraphLayoutOptions'
import { GraphLayoutRun } from './GraphLayoutRun'
import { Hide } from './Hide'
import { Refit } from './Refit'
import { ReLayout } from './ReLayout'
import { SizeBy } from './SizeBy'
import { Zoom } from './Zoom'

Expand Down Expand Up @@ -76,6 +77,7 @@ export type GraphToolbarProps = CSSProps &
layouts?: GraphLayout[]
zoom?: boolean
layout?: boolean
relayout?: boolean
refit?: boolean
hide?: boolean
size?: boolean
Expand All @@ -96,11 +98,13 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
layouts = [],
zoom = true,
layout = true,
relayout = true,
refit = true,
hide = true,
size = true,
buttonVariant = 'tertiary',
css,
children,
...props
}) => {
const menuItems = useMemo(() => {
Expand All @@ -118,6 +122,12 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
)
}

if (relayout) {
items.push(
<ReLayout key="relayout" model={model} onModelChange={onModelChange} />
)
}

if (layout && layouts.length > 0) {
items.push(
<GraphLayoutOptions
Expand All @@ -129,7 +139,7 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
)
}
return items.filter((item) => item !== null)
}, [hide, layout, layouts, model, onModelChange, size])
}, [hide, layout, layouts, model, onModelChange, size, relayout])

return (
<StyledToolbar css={css as any} {...props}>
Expand Down Expand Up @@ -157,6 +167,7 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
onModelChange={onModelChange}
/>
)}
{children}
{menuItems.length > 0 && (
<Menu>
<MenuTrigger>
Expand Down
24 changes: 24 additions & 0 deletions packages/react/src/components/GraphToolbar/ReLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { renderLight, screen, userEvent } from '../../test/setup'
import { Relayout } from './GraphToolbar.test'

it('Can select to size by', () => {
renderLight(<Relayout withGraph={false} />)
userEvent.tab()
userEvent.keyboard('{enter}')
expect(
screen
.getByRole('menuitemcheckbox', { name: /Layout on change/i })
.getAttribute('aria-checked')
).toBe('true')
userEvent.click(
screen.getByRole('menuitemcheckbox', { name: /Layout on change/i })
)
userEvent.tab()
userEvent.keyboard('{enter}')
expect(
screen
.getByRole('menuitemcheckbox', { name: /Layout on change/i })
.getAttribute('aria-checked')
).toBe('false')
})
50 changes: 50 additions & 0 deletions packages/react/src/components/GraphToolbar/ReLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { CSSProps, MenuCheckboxItem, VariantProps } from '@committed/components'
import { GraphModel } from '@committed/graph'
import React, { useCallback } from 'react'

export type ReLayoutProps = CSSProps &
VariantProps<typeof MenuCheckboxItem> & {
/** Declarative definition of graph state */
model: GraphModel
/** The graph model change callback */
onModelChange: (
model: GraphModel | ((model2: GraphModel) => GraphModel)
) => void
}

/**
* A GraphToolbar sub-component to relayout on graph change
*/
export const ReLayout: React.FC<ReLayoutProps> = ({
model,
onModelChange,
css,
...props
}) => {
const handleToggle = useCallback((): void => {
onModelChange(
GraphModel.applyLayout(
model,
model
.getCurrentLayout()
.setOnChange(!model.getCurrentLayout().isOnChange())
)
)
}, [model, onModelChange])

return (
<>
<MenuCheckboxItem
css={css as any}
{...props}
key="hideEdgeLabels"
checked={model.getCurrentLayout().isOnChange()}
onCheckedChange={handleToggle}
>
Layout on change
</MenuCheckboxItem>
</>
)
}
4 changes: 2 additions & 2 deletions packages/react/src/graph/renderer/CytoscapeRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ it('can be rendered', () => {
/>
)
expect(asFragment()).toBeDefined()
expect(cytoscape.addListener).toHaveBeenCalledTimes(8)
expect(cytoscape.addListener).toHaveBeenCalledTimes(10)

unmount()

expect(cytoscape.removeListener).toHaveBeenCalledTimes(8)
expect(cytoscape.removeListener).toHaveBeenCalledTimes(10)
})

it('layout command triggers layout', () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/react/src/graph/renderer/CytoscapeRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,21 @@ const Renderer: GraphRenderer<CyGraphRendererOptions>['render'] = ({
},
[updateSelection]
)
const updateLayout = useCallback(() => {
if (graphModel.getCurrentLayout().isOnChange()) {
triggerLayout(layout)
}
}, [graphModel, triggerLayout, layout])
const disableLayoutOnChange = useCallback(() => {
if (graphModel.getCurrentLayout().isOnChange()) {
onChange((model) =>
GraphModel.applyLayout(
model,
model.getCurrentLayout().setOnChange(false)
)
)
}
}, [graphModel, onChange])
const layoutStarting = useCallback(() => {
console.debug('Layout started')
layoutStart.current = Date.now()
Expand All @@ -286,6 +301,8 @@ const Renderer: GraphRenderer<CyGraphRendererOptions>['render'] = ({
useCyListener(cytoscape, unselectNode, 'unselect', 'node')
useCyListener(cytoscape, selectEdge, 'select', 'edge')
useCyListener(cytoscape, unselectEdge, 'unselect', 'edge')
useCyListener(cytoscape, updateLayout, 'add remove', '*')
useCyListener(cytoscape, disableLayoutOnChange, 'drag', 'node')
useCyListener(cytoscape, layoutStarting, 'layoutstart')
useCyListener(cytoscape, layoutStopping, 'layoutstop')
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down

0 comments on commit aeac4f2

Please sign in to comment.