-
Notifications
You must be signed in to change notification settings - Fork 18
New: Add tree layout #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
New: Add tree layout #107
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2ac224b
New: Add new layouts
AlexIchenskiy 43305a0
New: Add layout options
AlexIchenskiy 77a61eb
Chore: Make layout dynamically changeable
AlexIchenskiy e9c4fe7
Chore: Update docs
AlexIchenskiy 57c9ddf
Fix: Naming typo
AlexIchenskiy c6e05f2
Chore: Refactor layouts
AlexIchenskiy 5362f34
New: Enable layout node add/remove
AlexIchenskiy 50cfc5e
Chore: Improve behavior for recurrent nodes
AlexIchenskiy ca31274
Chore: Move some simulator settings to layout
AlexIchenskiy c470ad2
Chore: Refactor code quality and performance
AlexIchenskiy 237c1da
Fix: Layout behavior on change
AlexIchenskiy 9d0cf47
Chore: Add recenter on layout change
AlexIchenskiy 70d1fc4
Fix: Layout change behavior
AlexIchenskiy 159850a
Fix: Simulation behavior on data deletion
AlexIchenskiy a06c206
Chore: Add recenter on layout change
AlexIchenskiy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| <!DOCTYPE html> | ||
| <html lang='en' type="module"> | ||
| <head> | ||
| <base href="."> | ||
| <meta charset='UTF-8'> | ||
| <title>Orb | Simple hierarchical graph</title> | ||
| <script type="text/javascript" src="./orb.js"></script> | ||
| </head> | ||
| <style> | ||
| html, body { | ||
| height: 100%; | ||
| margin: 0; | ||
| } | ||
| </style> | ||
| <body> | ||
| <div style="display: flex; flex-direction: column; align-items: center; height: 100%;"> | ||
| <h1>Example 8 - Hierarchical layout</h1> | ||
| <p style="width: 70%">Renders a simple graph with hierarchical layout.</p> | ||
|
|
||
| <!-- | ||
| Make sure that your graph container has a defined width and height. | ||
| Orb will expand to any available space, but won't be visible if it's parent container is collapsed. | ||
| --> | ||
| <div id='graph' style="flex: 1; width: 100%;"></div> | ||
| </div> | ||
| <script type="text/javascript"> | ||
| const container = document.getElementById('graph'); | ||
|
|
||
| const nodes = [ | ||
| { id: 0, label: 'Root' }, | ||
| { id: 1, label: 'Child A' }, | ||
| { id: 2, label: 'Child B' }, | ||
| { id: 3, label: 'A1' }, | ||
| { id: 4, label: 'A2' }, | ||
| { id: 5, label: 'B1' }, | ||
| { id: 6, label: 'B2' }, | ||
| ]; | ||
|
|
||
| const edges = [ | ||
| { id: 0, start: 0, end: 1, label: 'Root -> A' }, | ||
| { id: 1, start: 0, end: 2, label: 'Root -> B' }, | ||
| { id: 2, start: 1, end: 3, label: 'A -> A1' }, | ||
| { id: 3, start: 1, end: 4, label: 'A -> A2' }, | ||
| { id: 4, start: 2, end: 5, label: 'B -> B1' }, | ||
| { id: 5, start: 2, end: 6, label: 'B -> B2' }, | ||
| ]; | ||
|
|
||
| const orb = new Orb.OrbView(container, { | ||
| layout: { | ||
| type: 'hierarchical', | ||
| options: { | ||
| 'orientation': 'vertical', | ||
| 'reversed': false | ||
| }, | ||
| } | ||
| }); | ||
|
|
||
| // Assign a basic style | ||
| orb.data.setDefaultStyle({ | ||
| getNodeStyle(node) { | ||
| return { | ||
| borderColor: '#1d1d1d', | ||
| borderWidth: 0.6, | ||
| color: '#DD2222', | ||
| colorHover: '#e7644e', | ||
| colorSelected: '#e7644e', | ||
| fontSize: 10, | ||
| label: node.getData().label, | ||
| size: 6, | ||
| }; | ||
| }, | ||
| getEdgeStyle(edge) { | ||
| return { | ||
| color: '#999999', | ||
| colorHover: '#1d1d1d', | ||
| colorSelected: '#1d1d1d', | ||
| fontSize: 3, | ||
| width: 1, | ||
| widthHover: 0.9, | ||
| widthSelected: 0.9, | ||
| label: edge.getData().label, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| // Initialize nodes and edges | ||
| orb.data.setup({ nodes, edges }); | ||
|
|
||
| orb.render(() => { | ||
| orb.recenter(); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,43 @@ | ||
| import { IEdgeBase } from '../../models/edge'; | ||
| import { INode, INodeBase, INodePosition } from '../../models/node'; | ||
| import { CircleLayout } from './layouts/circle'; | ||
| import { CircularLayout, ICircularLayoutOptions } from './layouts/circular'; | ||
| import { IForceLayoutOptions } from './layouts/force'; | ||
| import { GridLayout, IGridLayoutOptions } from './layouts/grid'; | ||
| import { HierarchicalLayout, IHierarchicalLayoutOptions } from './layouts/hierarchical'; | ||
|
|
||
| export enum layouts { | ||
| DEFAULT = 'default', | ||
| CIRCLE = 'circle', | ||
| export type LayoutType = 'circular' | 'force' | 'grid' | 'hierarchical'; | ||
|
|
||
| export type LayoutSettingsMap = { | ||
| circular: ICircularLayoutOptions; | ||
| force: IForceLayoutOptions; | ||
| grid: IGridLayoutOptions; | ||
| hierarchical: IHierarchicalLayoutOptions; | ||
| }; | ||
|
|
||
| export interface ILayoutSettings { | ||
| type: LayoutType; | ||
| options?: LayoutSettingsMap[LayoutType]; | ||
| } | ||
|
|
||
| export interface ILayout<N extends INodeBase, E extends IEdgeBase> { | ||
| getPositions(nodes: INode<N, E>[], width: number, height: number): INodePosition[]; | ||
| getPositions(nodes: INode<N, E>[]): INodePosition[]; | ||
| } | ||
|
|
||
| export class Layout<N extends INodeBase, E extends IEdgeBase> implements ILayout<N, E> { | ||
| private readonly _layout: ILayout<N, E> | null; | ||
|
|
||
| private layoutByLayoutName: Record<string, ILayout<N, E> | null> = { | ||
| [layouts.DEFAULT]: null, | ||
| [layouts.CIRCLE]: new CircleLayout(), | ||
| }; | ||
|
|
||
| constructor(layoutName: string) { | ||
| this._layout = this.layoutByLayoutName[layoutName]; | ||
| } | ||
|
|
||
| getPositions(nodes: INode<N, E>[], width: number, height: number): INodePosition[] { | ||
| return this._layout === null ? [] : this._layout.getPositions(nodes, width, height); | ||
| export class LayoutFactory { | ||
| static create<N extends INodeBase, E extends IEdgeBase>( | ||
| settings?: Partial<ILayoutSettings>, | ||
| ): ILayout<N, E> | undefined { | ||
| switch (settings?.type) { | ||
| case 'circular': | ||
| return new CircularLayout<N, E>(settings.options as ICircularLayoutOptions); | ||
| case 'force': | ||
| return undefined; | ||
| case 'grid': | ||
| return new GridLayout<N, E>(settings.options as IGridLayoutOptions); | ||
| case 'hierarchical': | ||
| return new HierarchicalLayout<N, E>(settings.options as IHierarchicalLayoutOptions); | ||
| default: | ||
| throw new Error('Incorrect layout type.'); | ||
| } | ||
| } | ||
| } | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { IEdgeBase } from '../../../models/edge'; | ||
| import { INode, INodeBase, INodePosition } from '../../../models/node'; | ||
| import { ILayout } from '../layout'; | ||
|
|
||
| export interface ICircularLayoutOptions { | ||
| radius?: number; | ||
| centerX?: number; | ||
| centerY?: number; | ||
| } | ||
|
|
||
| export const DEFAULT_CIRCULAR_LAYOUT_OPTIONS: Required<ICircularLayoutOptions> = { | ||
| radius: 100, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this maybe midRadius? Because if you have radius 10, but there are 1000000 nodes. The nodes will overlap. |
||
| centerX: 0, | ||
| centerY: 0, | ||
| }; | ||
|
|
||
| export class CircularLayout<N extends INodeBase, E extends IEdgeBase> implements ILayout<N, E> { | ||
| private _config: Required<ICircularLayoutOptions>; | ||
|
|
||
| constructor(options?: ICircularLayoutOptions) { | ||
| this._config = { ...DEFAULT_CIRCULAR_LAYOUT_OPTIONS, ...options }; | ||
| } | ||
|
|
||
| getPositions(nodes: INode<N, E>[]): INodePosition[] { | ||
| const angleStep = (2 * Math.PI) / nodes.length; | ||
|
|
||
| const positions = nodes.map((node, index) => { | ||
| return { | ||
| id: node.id, | ||
| x: this._config.centerX + this._config.radius * Math.cos(angleStep * index), | ||
| y: this._config.centerY + this._config.radius * Math.sin(angleStep * index), | ||
| }; | ||
| }); | ||
|
|
||
| return positions; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool stuff. Do you get an error if you add a new LayoutType that is not referenced in LayoutSettingsMap?
Also, what we talked about: Adding force layout here along with all params (gaps, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! I am now adding a force layout, and I do get an error that there is no force layout in the
LayoutSettingsMap, so it is future-proof for adding new ones :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we have force simulation parameters in
SimulatorEngineSettings(e.g. centering, link distance) - do we want to move them to layout settings?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make sense, but not required