UNPKG

cytoscape-angular-ng17

Version:
1 lines 127 kB
{"version":3,"file":"cytoscape-angular-ng17.mjs","sources":["../../../projects/cytoscape-angular/src/lib/cytoscape-graph.component.ts","../../../projects/cytoscape-angular/src/lib/layout/layout-options-impl.ts","../../../projects/cytoscape-angular/src/lib/fluid-form/form-info.ts","../../../projects/cytoscape-angular/src/lib/fluid-form/fluid-form.component.ts","../../../projects/cytoscape-angular/src/lib/fluid-form/fluid-form.component.html","../../../projects/cytoscape-angular/src/lib/cytoscape-layout-tool/cytoscape-layout-tool.component.ts","../../../projects/cytoscape-angular/src/lib/style/style.ts","../../../projects/cytoscape-angular/src/lib/cytoscape-style-tool/cytoscape-style-tool.component.ts","../../../projects/cytoscape-angular/src/lib/cytoscape-graph-toolbar/cytoscape-graph-toolbar.component.ts","../../../projects/cytoscape-angular/src/lib/cytoscape-angular.module.ts","../../../projects/cytoscape-angular/src/public-api.ts","../../../projects/cytoscape-angular/src/cytoscape-angular-ng17.ts"],"sourcesContent":["import {\n AfterViewInit,\n Component,\n ElementRef,\n Input,\n OnChanges,\n SimpleChanges,\n ViewChild,\n} from '@angular/core';\nimport * as cy from 'cytoscape';\nimport cytoscape, {\n CytoscapeOptions,\n EdgeDefinition,\n LayoutOptions,\n NodeDefinition,\n Position,\n SelectionType,\n Stylesheet,\n} from 'cytoscape';\n\n\n/**\n * The API is a little odd to provide flexibility.\n * EITHER bind to cyOptions (type CytoscapeOptions), to control the options yourself\n * OR this component will build a CytoscapeOptions internally by using all the other inputs.\n * If cyOptions is supplied, all other inputs are ignored.\n * The cyOptions container (HTML element) is always ignored and set internally.\n */\n@Component({\n selector: 'cytoscape-graph',\n template: `\n <p-progressSpinner\n *ngIf=\"loading\"\n class=\"spinner\"\n strokeWidth=\"4\"\n fill=\"#EEEEEE\"\n animationDuration=\".5s\"\n ></p-progressSpinner>\n <div #cyGraph class=\"graphWrapper\"></div>\n `,\n styles: [\n `\n .spinner {\n position: absolute;\n left: '350px';\n z-index: 10;\n width: '250px';\n height: '250px';\n }\n @keyframes ui-progress-spinner-color {\n 100%,\n 0% {\n stroke: #d62d20;\n }\n 40% {\n stroke: #0057e7;\n }\n 66% {\n stroke: #008744;\n }\n 80%,\n 90% {\n stroke: #ffa700;\n }\n }\n .graphWrapper {\n height: 100%;\n width: 100%;\n }\n `,\n ],\n})\nexport class CytoscapeGraphComponent implements OnChanges {\n @ViewChild('cyGraph')\n cyGraph: ElementRef | undefined;\n\n @Input()\n debug = false;\n\n @Input()\n nodes: NodeDefinition[] | undefined;\n @Input()\n edges: EdgeDefinition[] | undefined;\n\n @Input()\n autolock: boolean | undefined;\n @Input()\n autoungrabify: boolean | undefined;\n @Input()\n autounselectify: boolean | undefined;\n @Input()\n boxSelectionEnabled: boolean | undefined;\n @Input()\n desktopTapThreshold: number | undefined;\n @Input()\n hideEdgesOnViewport: boolean | undefined;\n @Input()\n hideLabelsOnViewport: boolean | undefined;\n @Input()\n layoutOptions: LayoutOptions | undefined;\n @Input()\n maxZoom: number | undefined;\n @Input()\n minZoom: number | undefined;\n @Input()\n motionBlur: boolean | undefined;\n @Input()\n motionBlurOpacity: number | undefined;\n @Input()\n pan: Position | undefined;\n @Input()\n panningEnabled: boolean | undefined;\n @Input()\n pixelRatio: number | 'auto' | undefined;\n @Input()\n selectionType: SelectionType | undefined;\n @Input()\n style: Stylesheet[] | undefined;\n @Input()\n styleEnabled: boolean | undefined;\n @Input()\n textureOnViewport: boolean | undefined;\n @Input()\n touchTapThreshold: number | undefined;\n @Input()\n userPanningEnabled: boolean | undefined;\n @Input()\n userZoomingEnabled: boolean | undefined;\n @Input()\n wheelSensitivity: number | undefined;\n @Input()\n zoom: 1 | undefined;\n @Input()\n zoomingEnabled: boolean | undefined;\n @Input()\n showToolbar = true;\n\n cyOptions: CytoscapeOptions | undefined;\n private cy!: cy.Core;\n loading: boolean = false;\n\n constructor() {}\n\n public ngOnChanges(changes: SimpleChanges): any {\n console.log(\n 'cytoscape graph component ngOnChanges. changes:',\n JSON.stringify(changes)\n );\n if (changes['style']) {\n console.log('changes[\"style\"]:', JSON.stringify(changes['style']));\n this.runWhileLoading(this.updateStyles.bind(this));\n }\n }\n\n public getCy() {\n return this.cy;\n }\n\n public centerElements(selector) {\n if (!this.cy) {\n return;\n }\n const elems = this.cy.$(selector);\n this.cy.center(elems);\n }\n\n public zoomToElement(selector: string, level = 3) {\n let position = this.cy?.$(selector)?.position();\n if (!position) {\n console.warn(`Cannot zoom to ${selector}`);\n }\n this.cy.zoom({\n level: level,\n position: position,\n });\n }\n\n public render() {\n this.runWhileLoading(this.rerender.bind(this));\n }\n\n public runWhileLoading(f: Function) {\n this.loading = true;\n setTimeout(() => {\n f();\n setTimeout(() => {\n this.loading = false;\n }, 30);\n }, 0);\n }\n\n private updateStyles() {\n if (this.cy && this.style) {\n this.cy.style(this.style);\n }\n }\n\n public rerender() {\n //TODO : this takes a heavy-handed approach, refine for performance\n if (!this.cyGraph) {\n console.warn(`No cyGraph found`);\n return;\n }\n\n const cyOptions = this.cyOptions || {\n // ignored, use nodes and edges\n // elements: this.elements,\n autolock: this.autolock,\n autoungrabify: this.autoungrabify,\n autounselectify: this.autounselectify,\n boxSelectionEnabled: this.boxSelectionEnabled,\n container: this.cyGraph.nativeElement,\n desktopTapThreshold: this.desktopTapThreshold,\n hideEdgesOnViewport: this.hideEdgesOnViewport,\n hideLabelsOnViewport: this.hideLabelsOnViewport,\n layout: this.layoutOptions,\n maxZoom: this.maxZoom,\n minZoom: this.minZoom,\n motionBlur: this.motionBlur,\n motionBlurOpacity: this.motionBlurOpacity,\n pan: this.pan,\n panningEnabled: this.panningEnabled,\n pixelRatio: this.pixelRatio,\n selectionType: this.selectionType,\n style: this.style,\n styleEnabled: this.styleEnabled,\n textureOnViewport: this.textureOnViewport,\n touchTapThreshold: this.touchTapThreshold,\n userPanningEnabled: this.userPanningEnabled,\n userZoomingEnabled: this.userZoomingEnabled,\n wheelSensitivity: this.wheelSensitivity,\n zoomingEnabled: this.zoomingEnabled,\n zoom: this.zoom,\n };\n // TODO do reset() instead?\n\n if (!this.cy) {\n this.cy = cytoscape(cyOptions);\n }\n this.cy.startBatch();\n this.cy.boxSelectionEnabled(this.boxSelectionEnabled);\n this.cy.nodes().remove();\n this.cy.edges().remove();\n if (this.nodes) {\n this.cy.add(this.nodes);\n }\n if (this.edges) {\n this.cy.add(this.edges);\n }\n this.cy.endBatch();\n if (this.layoutOptions) {\n this.cy.layout(this.layoutOptions).run();\n }\n }\n}\n\n/*\nGradient:\n\nbackground-gradient-stop-colors : The colours of the background gradient stops (e.g. cyan magenta yellow).\nbackground-gradient-stop-positions : The positions of the background gradient stops (e.g. 0% 50% 100%). If not specified or invalid, the stops will divide equally.\nbackground-gradient-direction : For background-fill: linear-gradient, this property defines the direction of the background gradient. The following values are accepted:\nto-bottom (default)\nto-top\nto-left\nto-right\nto-bottom-right\nto-bottom-left\nto-top-right\nto-top-left\n */\n","import {\n AnimatedLayoutOptions,\n BoundingBox12,\n BoundingBoxWH,\n SortingFunction,\n} from 'cytoscape';\n\nclass BaseLayoutOptionsImpl {\n ready(e: cytoscape.LayoutEventObject): void {\n // tslint:disable-next-line:no-console\n console.debug(\n `layout ready, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`\n ); // on layoutready\n }\n\n stop(e: cytoscape.LayoutEventObject): void {\n // tslint:disable-next-line:no-console\n console.debug(\n `layout stop, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`\n ); // on layoutstop\n }\n}\n\nexport class NullLayoutOptionsImpl extends BaseLayoutOptionsImpl {\n name = 'null';\n}\n\nexport class AnimateLayoutOptionsImpl\n extends BaseLayoutOptionsImpl\n implements AnimatedLayoutOptions\n{\n // the zoom level to set (prob want fit = false if set)\n zoom?: number = undefined;\n // the pan level to set (prob want fit = false if set)\n pan?: number = undefined;\n // whether to transition the node positions\n animate = false;\n // duration of animation in ms if enabled\n animationDuration = 500;\n // easing of animation if enabled\n animationEasing = undefined;\n // a function that determines whether the node should be animated.\n // All nodes animated by default on animate enabled. Non-animated nodes are\n // positioned immediately when the layout starts\n animateFilter = (node, i) => true;\n}\n\nexport class PresetLayoutOptionsImpl extends AnimateLayoutOptionsImpl {\n name = 'preset';\n\n fit?: boolean;\n padding?: number;\n\n // map of (node id) => (position obj); or function(node){ return somPos; }\n positions?: null;\n // transform a given node position. Useful for changing flow direction in discrete layouts\n transform = (node, position) => position;\n}\n\nexport class ShapedLayoutOptionsImpl extends AnimateLayoutOptionsImpl {\n // whether to fit to viewport\n fit = true;\n // fit padding\n padding = 30;\n // constrain layout bounds\n boundingBox?: BoundingBox12 | BoundingBoxWH | undefined = undefined;\n\n // prevents node overlap, may overflow boundingBox if not enough space\n avoidOverlap = true;\n\n // Excludes the label when calculating node bounding boxes for the layout algorithm\n nodeDimensionsIncludeLabels = false;\n // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up\n spacingFactor = 1.75;\n\n // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }\n sort?: SortingFunction = undefined;\n // transform a given node position. Useful for changing flow direction in discrete layouts\n transform = (node, position) => position;\n}\n\nexport class GridLayoutOptionsImpl extends ShapedLayoutOptionsImpl {\n name = 'grid';\n\n // extra spacing around nodes when avoidOverlap: true\n avoidOverlapPadding = 10;\n // uses all available space on false, uses minimal space on true\n condense = false;\n // force num of rows in the grid\n rows?: number | undefined = undefined;\n // force num of columns in the grid\n cols?: number | undefined = undefined;\n // returns { row, col } for element\n // (node: NodeSingular) => return { row: number; col: number; }\n position = null;\n}\n\nexport class RandomLayoutOptionsImpl extends AnimateLayoutOptionsImpl {\n name = 'random';\n\n fit = true;\n padding = 20;\n // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n boundingBox: cytoscape.BoundingBox12 | cytoscape.BoundingBoxWH | undefined =\n undefined;\n // transform a given node position. Useful for changing flow direction in discrete layouts\n transform = (node, position) => position;\n}\n\nexport class CircleLayoutOptionsImpl extends ShapedLayoutOptionsImpl {\n name = 'circle';\n\n radius?: number; // the radius of the circle\n startAngle: number = (3 / 2) * Math.PI; // where nodes start in radians\n sweep?: number = undefined; // how many radians should be between the first and last node (defaults to full circle)\n clockwise?: true; // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)\n}\n\n// Note: \"radius\" is not part of concentric, imperfect extension\nexport class ConcentricLayoutOptionsImpl {\n name = 'concentric';\n // how many radians should be between the first and last node (defaults to full circle)\n sweep?: number;\n // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)\n clockwise?: boolean;\n // where nodes start in radians, e.g. 3 / 2 * Math.PI,\n startAngle: number = (3 / 2) * Math.PI;\n fit?: boolean;\n nodeDimensionsIncludeLabels?: true;\n equidistant?: false; // whether levels have an equal radial distance betwen them, may cause bounding box overflow\n minNodeSpacing? = 10; // min spacing between outside of nodes (used for radius adjustment)\n boundingBox?: any; // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n // height of layout area (overrides container height)\n height = null;\n // width of layout area (overrides container width)\n width = null;\n // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up\n spacingFactor?: number;\n\n concentric(node: { degree(): number }): number {\n return 0;\n }\n\n levelWidth(node: { maxDegree(): number }): number {\n return 0;\n }\n}\n\nexport class BreadthFirstLayoutOptionsImpl extends ShapedLayoutOptionsImpl {\n name = 'breadthfirst';\n\n // whether the tree is directed downwards (or edges can point in any direction if false)\n directed = false;\n // put depths in concentric circles if true, put depths top down if false\n circle = false;\n // the roots of the trees\n roots?: string;\n // how many times to try to position the nodes in a maximal way (i.e. no backtracking)\n maximalAdjustments?: number;\n // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)\n maximal = false;\n grid = false; // whether to create an even grid into which the DAG is placed (circle:false only)\n override nodeDimensionsIncludeLabels = false; // Excludes the label when calculating node bounding boxes for the layout algorithm\n}\n\nexport class CoseLayoutOptionsImpl extends ShapedLayoutOptionsImpl {\n name = 'cose';\n\n // The layout animates only after this many milliseconds for animate:true\n // (prevents flashing on fast runs)\n animationThreshold = 250;\n\n // Number of iterations between consecutive screen positions update\n refresh = 20;\n\n // Randomize the initial positions of the nodes (true) or use existing positions (false)\n randomize = false;\n\n // Extra spacing between components in non-compound graphs\n componentSpacing = 40;\n\n // Node repulsion (overlapping) multiplier\n nodeOverlap = 4;\n\n // Nesting factor (multiplier) to compute ideal edge length for nested edges\n nestingFactor = 1.2;\n\n // Gravity force (constant)\n gravity = 1;\n\n // Maximum number of iterations to perform\n numIter = 1000;\n\n // Initial temperature (maximum node displacement)\n initialTemp = 1000;\n\n // Cooling factor (how the temperature is reduced between consecutive iterations\n coolingFactor = 0.99;\n\n // Lower temperature threshold (below this point the layout will end)\n minTemp = 1.0;\n\n // Node repulsion (non overlapping) multiplier\n nodeRepulsion = (node) => 2048;\n\n // Ideal edge (non nested) length\n idealEdgeLength = (edge) => 32;\n\n // Divisor to compute edge forces\n edgeElasticity = (edge) => 32;\n}\n\ntype RankDir = 'LR' | 'TB';\ntype Ranker = 'network-simplex' | 'tight-tree' | 'longest-path';\n\nexport class DagreLayoutOptionsImpl extends ShapedLayoutOptionsImpl {\n constructor() {\n super();\n }\n\n name = 'dagre';\n\n nodeSep?: number = undefined; // the separation between adjacent nodes in the same rank\n edgeSep?: number = undefined; // the separation between adjacent edges in the same rank\n rankSep?: number = undefined; // the separation between each rank in the layout\n // TB for top to bottom flow, 'LR' for left to right\n rankDir: RankDir = 'TB';\n // Type of algorithm to assign a rank to each node in the input graph.\n // Possible values: 'network-simplex', 'tight-tree' or 'longest-path'\n ranker?: Ranker = undefined;\n // number of ranks to keep between the source and target of the edge\n minLen = (edge) => 1;\n edgeWeight = (edge) => 1; // higher weight edges are generally made shorter and straighter than lower weight edges\n}\n","/* tslint:disable:ban-types */\nimport { AsyncValidatorFn, ValidatorFn } from '@angular/forms';\nimport { EventEmitter } from '@angular/core';\n\nexport type FieldType =\n | 'ShapePolygonPoints'\n | 'percent'\n | 'NodeShape'\n | 'LineStyle'\n | 'TextTransformation'\n | 'FontStyle'\n | 'FontWeight'\n | 'options'\n | 'Colour'\n | 'undefined'\n | 'object'\n | 'boolean'\n | 'number'\n | 'string'\n | 'function'\n | 'symbol'\n | 'bigint';\n\nexport class FormInfo {\n constructor(\n public title: string,\n public fieldsets: FieldsetInfo[],\n public showSubmitButton = false,\n public submitText = 'Submit',\n public disableSubmitOnFormInvalid = false,\n /* if the model has a property that isn't in a fieldset, but it in an fieldset created by the form */\n public otherFieldsetTitle = null\n ) {}\n}\n\nexport class FieldsetInfo {\n constructor(\n public legend: string,\n public fieldInfos: FieldInfo[],\n public displayOnlyIfProperties?: string[]\n ) {}\n\n showFieldsetForModel(model: object): boolean {\n if (!this.displayOnlyIfProperties) {\n return true;\n }\n for (const fieldInfo of this.fieldInfos) {\n for (const modelProperty of Object.keys(model)) {\n if (fieldInfo.modelProperty === modelProperty) {\n return true;\n }\n }\n }\n return false;\n }\n}\n\nexport class FieldInfo {\n private fieldTypes = {};\n updateOn: 'change' | 'blur' | 'submit' = 'submit'; // same as AbstractControlOptions\n asyncValidators: AsyncValidatorFn[] | Function | undefined;\n\n constructor(\n /* label to show the user next to the field, can be a function for i18n/dynamic labels */\n public label?: string | Function,\n /* The form has a model, this is the name of the property on the form's model object that this field */\n public modelProperty?: string,\n /* computed from the model property type by default */\n public type?: FieldType,\n /* The tooltip to display on hover */\n public tooltip?: string,\n /* The list of Angular Form Validators for the control or a function that returns such an array */\n public validators?: ValidatorFn[] | Function,\n /* disable the field if it's not valid */\n public disableWhenInvalid = false,\n /* If true and model[modelProperty] is undefined, don't create a field.*/\n public hideWhenNoModelProperty = true,\n /* Input only - by default the label is used as a placeholder and floats (how to downcast in a template?) */\n public placeholder?: string,\n /* Input only - same as HTML input (how to downcast in a template?) */\n public inputType: string = 'text',\n /* Input only - same as HTML input (how to downcast in a template?) */\n public inputSize: number = 8,\n /* Select only either an array of object or the name of a model property or function that is/returns an array of objects */\n public options?: object[],\n /* In an options object, what field to display to the user (or function that returns a string\n given the option object and the model) */\n public optionArrayLabelField?: string,\n /* In an options object, what field to return for the value of the option\n (or function that returns a string given the option object and the model) */\n public optionArrayValueField?: string | Function\n ) {}\n\n fieldType(model: object): FieldType {\n if (!this.modelProperty) {\n return this.fieldTypes[0]; // Return first field type if no model property\n }\n const cached = this.fieldTypes[this.modelProperty];\n if (cached) {\n return cached;\n } else {\n const fieldValueType = typeof model[this.modelProperty];\n const result = this.type\n ? this.type\n : this.options\n ? 'options'\n : fieldValueType;\n this.fieldTypes[this.modelProperty] = result;\n return result;\n }\n }\n\n setValue(newValue: any, model: object, modelChange: EventEmitter<any>) {\n if (!this.modelProperty) {\n return; // cannot set values that don't have a model property\n }\n model[this.modelProperty] = newValue;\n modelChange.emit({ property: this.modelProperty, value: newValue });\n }\n}\n","import {\n AfterViewChecked,\n AfterViewInit,\n Component,\n EventEmitter,\n Input,\n OnChanges,\n OnInit,\n Output,\n SimpleChanges,\n} from '@angular/core';\nimport {\n AsyncValidatorFn,\n FormControl,\n FormGroup,\n ValidatorFn,\n} from '@angular/forms';\nimport { FormInfo } from './form-info';\n\n@Component({\n selector: 'cyng-fluid-form',\n templateUrl: `./fluid-form.component.html`,\n styleUrls: [`./fluid-form.component.scss`],\n})\nexport class FluidFormComponent\n implements OnInit, OnChanges, AfterViewInit, AfterViewChecked\n{\n @Input()\n model: object | undefined;\n @Output()\n modelChange: EventEmitter<object> = new EventEmitter<object>();\n\n @Input()\n modelProperty: string | undefined;\n @Input()\n formInfo!: FormInfo;\n\n formGroup!: FormGroup;\n\n constructor() {}\n\n ngOnInit(): void {\n console.debug(\n 'FluidFormComponent this.formInfo:',\n JSON.stringify(this.formInfo)\n );\n let controls = {};\n this.formInfo?.fieldsets.forEach((fieldsetInfo) => {\n fieldsetInfo.fieldInfos.forEach((fieldInfo) => {\n if (!this.model || !fieldInfo || !fieldInfo.modelProperty) {\n return;\n }\n let modelValue = this.model[fieldInfo.modelProperty];\n // console.log('fieldInfo.modelProperty:', fieldInfo.modelProperty, ', modelValue:', modelValue)\n const validators: ValidatorFn[] =\n typeof fieldInfo.validators === 'function'\n ? fieldInfo.validators()\n : fieldInfo.validators;\n const asyncValidators: AsyncValidatorFn[] =\n typeof fieldInfo.asyncValidators === 'function'\n ? fieldInfo.asyncValidators()\n : fieldInfo.asyncValidators;\n const { updateOn } = fieldInfo;\n let formControl = new FormControl(modelValue, {\n validators,\n asyncValidators,\n updateOn,\n });\n formControl.valueChanges.subscribe((change) => {\n if (!this.model || !fieldInfo || !fieldInfo.modelProperty) {\n return;\n }\n console.debug(\n 'form control change ',\n JSON.stringify(change),\n ' for prop ',\n fieldInfo.modelProperty,\n ', changing current model value ',\n this.model[fieldInfo.modelProperty],\n ' to ',\n change\n );\n fieldInfo.setValue(change, this.model, this.modelChange);\n });\n controls[fieldInfo.modelProperty] = formControl;\n });\n });\n this.formGroup = new FormGroup(controls);\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n console.debug('ngOnChanges fluid-form changes:', JSON.stringify(changes));\n if (changes['model']) {\n const model = changes['model'].currentValue;\n for (let key of Object.keys(model)) {\n console.debug('ngOnChanges model key copying to form:', key);\n const control = this.formGroup?.controls[key];\n control\n ? control.setValue(model[key], { emitEvent: false })\n : console.warn('no control for model key ', key);\n }\n }\n }\n\n ngAfterViewInit(): void {\n // console.debug(\"ngAfterViewInit\")\n }\n\n ngAfterViewChecked(): void {\n // console.debug(\"ngAfterViewChecked\")\n }\n\n onSubmit() {\n console.log(`Form submitted`);\n }\n}\n","<form [formGroup]=\"formGroup\" [title]=\"formInfo?.title\" (ngSubmit)=\"onSubmit()\">\n <ng-container *ngFor=\"let fieldSetInfo of formInfo.fieldsets\">\n <p-fieldset\n *ngIf=\"model && fieldSetInfo.showFieldsetForModel(model)\"\n class=\"fieldset\"\n legend=\"{{ fieldSetInfo.legend }}\"\n >\n <div class=\"ui-g ui-fluid\">\n <div\n class=\"ui-g-12 ui-md-4 field\"\n *ngFor=\"let fieldInfo of fieldSetInfo.fieldInfos\"\n >\n <div class=\"ui-inputgroup\">\n <ng-container *ngIf=\"fieldInfo.fieldType(model) === 'boolean'\">\n <span class=\"ui-chkbox-label\">\n {{ fieldInfo.label }}\n </span>\n <p-inputSwitch\n name=\"{{ fieldInfo.modelProperty }}\"\n pTooltip=\"{{ fieldInfo.tooltip }}\"\n formControlName=\"{{ fieldInfo.modelProperty }}\"\n >\n </p-inputSwitch>\n </ng-container>\n <ng-container\n *ngIf=\"\n fieldInfo.fieldType(model) === 'string' ||\n fieldInfo.fieldType(model) === 'number'\n \"\n >\n <span class=\"ui-float-label\">\n <input\n pInputText\n id=\"{{ fieldInfo.modelProperty }}\"\n name=\"{{ fieldInfo.modelProperty }}\"\n formControlName=\"{{ fieldInfo.modelProperty }}\"\n [pTooltip]=\"fieldInfo.tooltip\"\n [type]=\"fieldInfo.inputType\"\n [size]=\"fieldInfo.inputSize\"\n />\n <label for=\"{{ fieldInfo.modelProperty }}\">{{\n fieldInfo.label\n }}</label>\n </span>\n </ng-container>\n <ng-container\n *ngIf=\"\n fieldInfo.fieldType(model) === 'options' &&\n fieldInfo.options &&\n fieldInfo.optionArrayLabelField\n \"\n >\n <span class=\"ui-float-label\">\n <p-dropdown\n formControlName=\"{{ fieldInfo.modelProperty }}\"\n [name]=\"fieldInfo.modelProperty\"\n [options]=\"fieldInfo.options\"\n [optionLabel]=\"fieldInfo.optionArrayLabelField\"\n [pTooltip]=\"fieldInfo.tooltip\"\n ></p-dropdown>\n <label for=\"{{ fieldInfo.modelProperty }}\">{{\n fieldInfo.label\n }}</label>\n </span>\n </ng-container>\n </div>\n </div>\n </div>\n </p-fieldset>\n </ng-container>\n</form>\n<button\n *ngIf=\"formInfo.showSubmitButton\"\n pButton\n [disabled]=\"formInfo.disableSubmitOnFormInvalid && !formGroup.valid\"\n (submit)=\"onSubmit()\"\n>\n {{ formInfo.submitText || \"Submit\" }}\n</button>\n","import {\n AfterViewChecked,\n AfterViewInit,\n Component,\n EventEmitter,\n HostListener,\n Input,\n OnChanges,\n OnInit,\n Output,\n SimpleChanges,\n ViewChild,\n} from '@angular/core';\nimport { LayoutOptions } from 'cytoscape';\nimport {\n BreadthFirstLayoutOptionsImpl,\n CircleLayoutOptionsImpl,\n ConcentricLayoutOptionsImpl,\n CoseLayoutOptionsImpl,\n DagreLayoutOptionsImpl,\n GridLayoutOptionsImpl,\n NullLayoutOptionsImpl,\n PresetLayoutOptionsImpl,\n RandomLayoutOptionsImpl,\n} from '../layout/layout-options-impl';\nimport { FieldInfo, FieldsetInfo, FormInfo } from '../fluid-form/form-info';\n\n@Component({\n selector: 'cytoscape-layout-tool',\n styles: [\n `\n :host {\n width: 400px;\n height: 2em;\n }\n\n .layout-header {\n width: 100%;\n height: 20px;\n }\n\n .layout-dropdown {\n padding-right: 10px;\n }\n\n input:disabled {\n background-color: rgba(204, 204, 204, 0.33);\n }\n `,\n ],\n template: `\n <div>\n <div style=\"display: flex;\">\n <div class=\"layout-header\">Edit Layout</div>\n </div>\n <p-dropdown\n class=\"layout-dropdown\"\n name=\"selectedLayoutInfo\"\n [options]=\"layoutOptionsList\"\n [(ngModel)]=\"layoutOptions\"\n optionLabel=\"name\"\n (ngModelChange)=\"onLayoutModelChange()\"\n >\n ></p-dropdown\n >\n <button\n class=\"apply-button\"\n pButton\n label=\"Apply\"\n [disabled]=\"!changed\"\n (click)=\"onApplyLayout()\"\n ></button>\n </div>\n <cyng-fluid-form\n [model]=\"layoutOptions\"\n [formInfo]=\"formInfo\"\n (modelChange)=\"onFormModelChange()\"\n ></cyng-fluid-form>\n `,\n})\nexport class CytoscapeLayoutToolComponent implements OnInit, OnChanges {\n private static LAYOUT_FORM_INFO: FormInfo =\n CytoscapeLayoutToolComponent.createLayoutFormInfo();\n\n @ViewChild('layoutForm') layoutForm;\n\n _layoutOptions: any;\n changed = false;\n\n @Input()\n get layoutOptions(): any {\n return this._layoutOptions;\n }\n set layoutOptions(value) {\n console.log(`set layoutOptions: ${value?.name}`);\n this._layoutOptions = value;\n }\n @Output() layoutOptionsChange: EventEmitter<LayoutOptions> =\n new EventEmitter<LayoutOptions>();\n\n public layoutOptionsList: LayoutOptions[] = [\n new BreadthFirstLayoutOptionsImpl(),\n new CoseLayoutOptionsImpl(),\n new DagreLayoutOptionsImpl(),\n new CircleLayoutOptionsImpl(),\n new ConcentricLayoutOptionsImpl(),\n new GridLayoutOptionsImpl(),\n new PresetLayoutOptionsImpl(),\n new RandomLayoutOptionsImpl(),\n new NullLayoutOptionsImpl(),\n ];\n\n formInfo!: FormInfo;\n\n constructor() {}\n\n ngOnInit(): void {\n this.formInfo = CytoscapeLayoutToolComponent.createLayoutFormInfo();\n let layoutOptionsSelect = this.layoutOptionsList[5];\n console.log(\n 'setting the initial selected layout, default: ',\n layoutOptionsSelect.name\n );\n if (this.layoutOptions) {\n console.log(\n `setting the initial selected layout based on input/output layout ${JSON.stringify(\n this.layoutOptions\n )}`\n );\n this.addOrReplaceInLayoutOptionsList(this.layoutOptions);\n }\n console.log(\n 'Initializing this.selectedLayoutInfo with layoutOptionsSelect ',\n JSON.stringify(layoutOptionsSelect)\n );\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n console.log('ngOnChanges layout changes:', JSON.stringify(changes));\n if (changes['layoutOptions']) {\n }\n }\n\n onLayoutModelChange() {\n console.log('Layout model change: ', JSON.stringify(this.layoutOptions));\n this.changed = true;\n }\n\n onFormModelChange() {\n console.log('onFormModelChange');\n this.changed = true;\n }\n\n onApplyLayout() {\n this.changed = false;\n this.layoutOptionsChange.emit(this.layoutOptions);\n }\n\n private addOrReplaceInLayoutOptionsList(layoutOptions: LayoutOptions): void {\n let matchingOptions = this.layoutOptionsList.find(\n (selectOption) => selectOption.name === layoutOptions.name\n );\n if (matchingOptions) {\n console.log(\n 'got matching layoutOptions: ',\n JSON.stringify(matchingOptions)\n );\n this.layoutOptionsList.splice(\n this.layoutOptionsList.indexOf(matchingOptions),\n 1,\n layoutOptions\n );\n } else {\n console.info(\n `Did you pass a new kind of layout? The layout name ${name} was not found, adding a new one to the top of the list.`\n );\n this.layoutOptionsList.unshift(layoutOptions);\n }\n }\n\n private static createLayoutFormInfo(): FormInfo {\n let fit = new FieldInfo(\n 'Fit',\n 'fit',\n 'boolean',\n 'Whether to fit to viewport'\n );\n let padding = new FieldInfo(\n 'Padding',\n 'padding',\n 'number',\n 'When fit to viewport, padding inside the viewport.'\n );\n\n let fitFieldset = new FieldsetInfo('Fit', [fit, padding], ['fit']);\n\n const zoom = new FieldInfo(\n 'Zoom',\n 'zoom',\n 'number',\n 'the zoom level to set (likely want fit = false if set)'\n );\n const pan = new FieldInfo(\n 'Pan',\n 'pan',\n 'number',\n 'the pan level to set (likely want fit = false if set)'\n );\n const animate = new FieldInfo(\n 'Animate',\n 'animate',\n 'boolean',\n 'whether to transition the node positions'\n );\n const animationDuration = new FieldInfo(\n 'Animation Duration',\n 'animationDuration',\n 'number',\n 'duration of animation in ms if enabled'\n );\n const animationEasing = new FieldInfo(\n 'Animation Easing',\n 'animationEasing',\n 'number',\n 'easing of animation if enabled'\n );\n let animationFieldset = new FieldsetInfo(\n 'Animation',\n [zoom, pan, animate, animationDuration, animationEasing],\n ['animate']\n );\n\n let avoidOverlap = new FieldInfo(\n 'Avoid Overlap',\n 'avoidOverlap',\n 'boolean',\n 'prevents node overlap, may overflow boundingBox if not enough space'\n );\n let spacingFactor = new FieldInfo(\n 'Spacing Factor',\n 'spacingFactor',\n 'number',\n 'Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up'\n );\n let nodeDimensionsIncludeLabels = new FieldInfo(\n 'Node Dimensions Include Labels',\n 'nodeDimensionsIncludeLabels',\n 'boolean',\n 'Excludes the label when calculating node bounding boxes for the layout algorithm'\n );\n let shapedFieldset = new FieldsetInfo(\n 'Shaped',\n [avoidOverlap, spacingFactor, nodeDimensionsIncludeLabels],\n ['avoidOverlap']\n );\n\n let directed = new FieldInfo(\n 'Directed',\n 'breadthFirst',\n 'boolean',\n 'whether the tree is breadthFirst downwards (or edges can point in any direction if false)'\n );\n let circle = new FieldInfo(\n 'Circle',\n 'circle',\n 'boolean',\n 'put depths in concentric circles if true, put depths top down if false'\n );\n let maximalAdjustments = new FieldInfo(\n 'Maximal Adjustments',\n 'maximalAdjustments',\n 'number',\n 'how many times to try to position the nodes in a maximal way (i.e. no backtracking)'\n );\n let maximal = new FieldInfo(\n 'Maximal',\n 'maximal',\n 'boolean',\n 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)'\n );\n let grid = new FieldInfo(\n 'Grid',\n 'grid',\n 'boolean',\n 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)'\n );\n let roots = new FieldInfo(\n 'Roots',\n 'roots',\n 'string',\n 'the roots of the trees'\n );\n let breadthFirstFieldset = new FieldsetInfo(\n 'Breadth First',\n [directed, circle, maximalAdjustments, maximal, grid, roots],\n ['breadthFirst']\n );\n\n let nodeSep = new FieldInfo(\n 'Node Separation',\n 'nodeSep',\n 'number',\n 'the separation between adjacent nodes in the same rank'\n );\n let edgeSep = new FieldInfo(\n 'Edge Separation',\n 'edgeSep',\n 'number',\n 'the separation between adjacent edges in the same rank'\n );\n let rankSep = new FieldInfo(\n 'Rank Separation',\n 'rankSep',\n 'number',\n 'the separation between each rank in the layout'\n );\n let ranker = new FieldInfo(\n 'Ranker',\n 'ranker',\n 'options',\n 'Type of algorithm to assign a rank to each node in the input graph.'\n );\n ranker.options = [\n { name: '', label: '' },\n { name: 'network-simplex', label: 'network-simplex' },\n { name: 'tight-tree', label: 'tight-tree' },\n { name: 'longest-path', label: 'longest-path' },\n ];\n\n let dagreFieldset = new FieldsetInfo(\n 'Dagre',\n [nodeSep, edgeSep, rankSep, ranker],\n ['nodeSep']\n );\n\n let animationThreshold = new FieldInfo(\n 'Animation Threshold',\n 'animationThreshold',\n 'number',\n 'The layout animates only after this many milliseconds when animate is true (prevents flashing on fast runs)'\n );\n let refresh = new FieldInfo(\n 'Refresh',\n 'refresh',\n 'number',\n 'Number of iterations between consecutive screen positions update'\n );\n let randomize = new FieldInfo(\n 'Randomize',\n 'randomize',\n 'boolean',\n 'Randomize the initial positions of the nodes (true) or use existing positions (false)'\n );\n let componentSpacing = new FieldInfo(\n 'Component Spacing',\n 'componentSpacing',\n 'number',\n 'Extra spacing between components in non-compound graphs'\n );\n let nodeOverlap = new FieldInfo(\n 'Node Overlap',\n 'nodeOverlap',\n 'number',\n 'Node repulsion (overlapping) multiplier'\n );\n let nestingFactor = new FieldInfo(\n 'Nesting Factor',\n 'nestingFactor',\n 'number',\n 'Nesting factor (multiplier) to compute ideal edge length for nested edges'\n );\n let gravity = new FieldInfo(\n 'Gravity',\n 'gravity',\n 'number',\n 'Gravity force (constant)'\n );\n let numIter = new FieldInfo(\n 'Max Iterations',\n 'numIter',\n 'number',\n 'Maximum number of iterations to perform'\n );\n let initialTemp = new FieldInfo(\n 'Initial Temp',\n 'initialTemp',\n 'number',\n 'Initial temperature (maximum node displacement)'\n );\n let coolingFactor = new FieldInfo(\n 'Cooling Factor',\n 'coolingFactor',\n 'number',\n 'Cooling factor (how the temperature is reduced between consecutive iterations'\n );\n let minTemp = new FieldInfo(\n 'Min. Temp',\n 'minTemp',\n 'number',\n 'Lower temperature threshold (below this point the layout will end)'\n );\n let coseFieldset = new FieldsetInfo(\n 'COSE',\n [\n animationThreshold,\n refresh,\n randomize,\n componentSpacing,\n nodeOverlap,\n nestingFactor,\n gravity,\n numIter,\n initialTemp,\n coolingFactor,\n minTemp,\n ],\n ['coolingFactor']\n );\n\n let avoidOverlapPadding = new FieldInfo(\n 'avoidOverlapPadding',\n 'avoidOverlapPadding',\n 'number',\n 'extra spacing around nodes when avoidOverlap: true'\n );\n let condense = new FieldInfo(\n 'condense',\n 'condense',\n 'boolean',\n 'uses all available space on false, uses minimal space on true'\n );\n let rows = new FieldInfo(\n 'Rows',\n 'rows',\n 'number',\n 'force num of rows in the grid'\n );\n let cols = new FieldInfo(\n 'Columns',\n 'cols',\n 'number',\n 'force num of columns in the grid'\n );\n\n let gridFieldset = new FieldsetInfo(\n 'Grid',\n [avoidOverlapPadding, condense, rows, cols],\n ['cols']\n );\n\n let radius = new FieldInfo(\n 'Radius',\n 'radius',\n 'number',\n 'the radius of the circle'\n );\n let startAngle = new FieldInfo(\n 'Start Angle',\n 'startAngle',\n 'number',\n 'where nodes start in radians (default:3 / 2 * Math.PI)'\n );\n let sweep = new FieldInfo(\n 'Sweep',\n 'sweep',\n 'number',\n 'how many radians should be between the first and last node (defaults to full circle)'\n );\n let clockwise = new FieldInfo(\n 'Clockwise',\n 'clockwise',\n 'number',\n 'whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)'\n );\n let circularFieldSet = new FieldsetInfo(\n 'Circular',\n [radius, startAngle, sweep, clockwise],\n ['clockwise']\n );\n\n let equidistant = new FieldInfo(\n 'Equidistant',\n 'equidistant',\n 'boolean',\n 'whether levels have an equal radial distance betwen them, may cause bounding box overflow'\n );\n let minNodeSpacing = new FieldInfo(\n 'Min. Node Spacing',\n 'minNodeSpacing',\n 'number',\n 'min spacing between outside of nodes (used for radius adjustment)'\n );\n let height = new FieldInfo('Height', 'height', 'number', '');\n let width = new FieldInfo('Width', 'width', 'number', '');\n let concentricFieldSet = new FieldsetInfo(\n 'Concentric',\n [equidistant, minNodeSpacing, startAngle, height, width],\n ['equidistant']\n );\n\n //boundingBox: undefined // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n\n return new FormInfo(\n 'Layout',\n [\n breadthFirstFieldset,\n coseFieldset,\n dagreFieldset,\n gridFieldset,\n circularFieldSet,\n concentricFieldSet,\n fitFieldset,\n animationFieldset,\n shapedFieldset,\n ],\n false\n );\n }\n}\n/*\n <!--\n\n Dagre\n // Type of algorithm to assign a rank to each node in the input graph.\n // Possible values:\n c = undefined\n // number of ranks to keep between the source and target of the edge\n minLen = ( edge ) => { return 1 }\n edgeWeight = ( edge ) => { return 1 } // higher weight edges are generally made shorter and straighter than lower weight edges\n\n AnimateLayoutOptionsImpl\n // a function that determines whether the node should be animated.\n // All nodes animated by default on animate enabled. Non-animated nodes are\n // positioned immediately when the layout starts\n animateFilter = ( node, i ) => true\n\n Preset\n // map of (node id) => (position obj); or function(node){ return somPos; }\n positions: undefined\n\n Shaped\n // constrain layout bounds\n boundingBox?: BoundingBox12 | BoundingBoxWH | undefined = undefined\n\n // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }\n sort?: SortingFunction = undefined\n // transform a given node position. Useful for changing flow direction in discrete layouts\n transform = (node, position ) => position\n\n //Grid\n // returns { row, col } for element\n // (node: NodeSingular) => return { row: number; col: number; }\n position = undefined\n\n //Random\n // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n boundingBox: cytoscape.BoundingBox12 | cytoscape.BoundingBoxWH | undefined = undefined\n // transform a given node position. Useful for changing flow direction in discrete layouts\n transform = (node, position ) => position\n\n //Circular\n //Conecntric\n concentric(node: { degree(): number }): number {\n eturn 0\n }\n\n levelWidth(node: { maxDegree(): number }): number {\n return 0\n }\n\n Breadth First\n\n CoseLayoutOptionsImpl\n\n // Node repulsion (non overlapping) multiplier\n nodeRepulsion = function( node ){ return 2048 }\n\n // Ideal edge (non nested) length\n idealEdgeLength = ( edge ) => { return 32 }\n\n // Divisor to compute edge forces\n edgeElasticity = ( edge ) => 32\n\n -->\n */\n","import { StylesheetStyle } from 'cytoscape';\nimport {\n FieldInfo,\n FieldsetInfo,\n FieldType,\n FormInfo,\n} from '../fluid-form/form-info';\n\nclass StyleFieldInfo extends FieldInfo {\n constructor(\n styleName: string,\n type: FieldType,\n hint: string,\n options?: object[]\n ) {\n super(styleName, styleName, type, hint);\n this.options = options;\n }\n\n override fieldType(model: object): FieldType {\n switch (this.type) {\n case 'percent':\n return 'number';\n case 'ShapePolygonPoints':\n case 'NodeShape':\n case 'LineStyle':\n case 'TextTransformation':\n case 'FontStyle':\n case 'FontWeight':\n case 'Colour':\n return 'string';\n default:\n return super.fieldType(model);\n }\n }\n}\n\nexport function createStyleCoreFormInfo() {\n return new FormInfo(\n 'Core Styles',\n [\n new FieldsetInfo('Background', [\n new StyleFieldInfo(\n 'active-bg-color',\n 'Colour',\n 'The colour of the indicator shown when the background is grabbed by the user.'\n ),\n new StyleFieldInfo(\n 'active-bg-opacity',\n 'number',\n 'The opacity of the active background indicator.'\n ),\n new StyleFieldInfo(\n 'active-bg-size',\n 'number',\n 'The size of the active background indicator..'\n ),\n ]),\n\n new FieldsetInfo('Selection Box', [\n new StyleFieldInfo(\n 'selection-box-color',\n 'Colour',\n 'The background colour of the selection box used for drag selection.'\n ),\n new StyleFieldInfo(\n 'selection-box-border-color',\n 'Colour',\n 'The colour of the border of the selection box used for drag selection.'\n ),\n new StyleFieldInfo(\n 'selection-box-border-width',\n 'number',\n 'The size of the border on the selection box.'\n ),\n new StyleFieldInfo(\n 'selection-box-opacity',\n 'number',\n 'The opacity of the selection box.'\n ),\n new StyleFieldInfo('', 'number', ''),\n ]),\n new FieldsetInfo('Texture During Viewport Gestures', [\n new StyleFieldInfo(\n 'outside-texture-bg-color',\n 'Colour',\n 'The colour of the area outside the viewport texture when initOptions.textureOnViewport === true.'\n ),\n new StyleFieldInfo(\n 'outside-texture-bg-opacity',\n 'number',\n 'The opacity of the area outside the viewport texture.'\n ),\n ]),\n ],\n false\n );\n}\n\nfunction fieldSorter(field1: FieldInfo, field2: FieldInfo): number {\n if (!field1) {\n return field2 ? 1 : 0;\n } else if (!field2) {\n return -1;\n } else {\n const label1 =\n typeof field1.label === 'function' ? field1.label() : field1.label;\n const label2 =\n typeof field2.label === 'function' ? field2.label() : field2.label;\n return label1.localeCompare(label2);\n }\n}\n\nexport function createStyleEdgeFieldSets() {\n const fieldsetInfos: FieldsetInfo[] = [];\n return fieldsetInfos;\n}\n\nexport function createStyleNodeFieldSets() {\n const fieldsetInfos: FieldsetInfo[] = [];\n\n let nodeFieldInfos: FieldInfo[] = [];\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'label',\n 'string',\n 'The text to display for an element’s label.'\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'source-label',\n 'string',\n 'The text to display for a node’s source label.'\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'target-label',\n 'string',\n 'The text to display for a node’s target label.'\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo('color', 'Colour', 'The colour of the element’s label.')\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'font-family',\n 'string',\n 'A comma-separated list of font names to use on the label text.'\n )\n );\n /**\n * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family\n */\n nodeFieldInfos.push(\n new StyleFieldInfo('font-size', 'string', 'The size of the label text.')\n );\n /**\n * https://developer.mozilla.org/en-US/docs/Web/CSS/font-style\n */\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'font-style',\n 'FontStyle',\n 'A CSS font style to be applied to the label text.'\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'font-weight',\n 'FontWeight',\n 'A CSS font weight to be applied to the label text.'\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'text-max-width',\n 'string',\n 'The maximum width for wrapped text, applied when \"text-wrap\" is set to wrap. For only manual newlines (i.e.\\\\n), set a very large value like 1000px such that only your newline characters would apply.'\n )\n );\n let textWrapStyleFieldInfo = new StyleFieldInfo(\n 'text-wrap',\n 'options',\n 'A wrapping style to apply to the label text; may be \"none\" for no wrapping (including manual newlines ) or \"wrap\" for manual and/ or autowrapping.'\n );\n textWrapStyleFieldInfo.options = [\n { label: 'none', value: 'none' },\n { label: 'wrap', value: 'wrap' },\n { label: 'ellipsis', value: 'ellipsis' },\n ];\n nodeFieldInfos.push(textWrapStyleFieldInfo);\n /**\n * Node label alignment:\n */\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'text-halign',\n 'options',\n 'The vertical alignment of a node’s label.',\n [\n { name: '', label: '' },\n { label: 'left', value: 'left' },\n { label: 'center', value: 'center' },\n { labe: 'right', value: 'right' },\n ]\n )\n );\n nodeFieldInfos.push(\n new StyleFieldInfo(\n 'text-valign',\n 'options',\n 'The vertical alignment of a node’s label.',\n [\n { name: '', label: '' },\n { label: 'top', value: 'top' },\n { label: 'center', value: 'center' },\n { la