cytoscape-angular-ng17
Version:
Angular 17+ components for cytoscape charting.
821 lines (816 loc) • 91.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, ViewChild, Input, EventEmitter, Output, NgModule } from '@angular/core';
import cytoscape from 'cytoscape';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i2 from 'primeng/progressspinner';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import * as i3 from 'primeng/button';
import { ButtonModule } from 'primeng/button';
import * as i3$1 from 'primeng/api';
import * as i4$2 from 'primeng/overlaypanel';
import { OverlayPanelModule } from 'primeng/overlaypanel';
import * as i8 from 'primeng/tooltip';
import { TooltipModule } from 'primeng/tooltip';
import * as i2$1 from '@angular/forms';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import * as i4 from 'primeng/dropdown';
import { DropdownModule } from 'primeng/dropdown';
import * as i5 from 'primeng/fieldset';
import { FieldsetModule } from 'primeng/fieldset';
import * as i6 from 'primeng/inputswitch';
import { InputSwitchModule } from 'primeng/inputswitch';
import * as i7 from 'primeng/inputtext';
import { InputTextModule } from 'primeng/inputtext';
import * as i4$1 from 'primeng/autocomplete';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { ProgressBarModule } from 'primeng/progressbar';
import { SpinnerModule } from 'primeng/spinner';
/**
* The API is a little odd to provide flexibility.
* EITHER bind to cyOptions (type CytoscapeOptions), to control the options yourself
* OR this component will build a CytoscapeOptions internally by using all the other inputs.
* If cyOptions is supplied, all other inputs are ignored.
* The cyOptions container (HTML element) is always ignored and set internally.
*/
class CytoscapeGraphComponent {
cyGraph;
debug = false;
nodes;
edges;
autolock;
autoungrabify;
autounselectify;
boxSelectionEnabled;
desktopTapThreshold;
hideEdgesOnViewport;
hideLabelsOnViewport;
layoutOptions;
maxZoom;
minZoom;
motionBlur;
motionBlurOpacity;
pan;
panningEnabled;
pixelRatio;
selectionType;
style;
styleEnabled;
textureOnViewport;
touchTapThreshold;
userPanningEnabled;
userZoomingEnabled;
wheelSensitivity;
zoom;
zoomingEnabled;
showToolbar = true;
cyOptions;
cy;
loading = false;
constructor() { }
ngOnChanges(changes) {
console.log('cytoscape graph component ngOnChanges. changes:', JSON.stringify(changes));
if (changes['style']) {
console.log('changes["style"]:', JSON.stringify(changes['style']));
this.runWhileLoading(this.updateStyles.bind(this));
}
}
getCy() {
return this.cy;
}
centerElements(selector) {
if (!this.cy) {
return;
}
const elems = this.cy.$(selector);
this.cy.center(elems);
}
zoomToElement(selector, level = 3) {
let position = this.cy?.$(selector)?.position();
if (!position) {
console.warn(`Cannot zoom to ${selector}`);
}
this.cy.zoom({
level: level,
position: position,
});
}
render() {
this.runWhileLoading(this.rerender.bind(this));
}
runWhileLoading(f) {
this.loading = true;
setTimeout(() => {
f();
setTimeout(() => {
this.loading = false;
}, 30);
}, 0);
}
updateStyles() {
if (this.cy && this.style) {
this.cy.style(this.style);
}
}
rerender() {
//TODO : this takes a heavy-handed approach, refine for performance
if (!this.cyGraph) {
console.warn(`No cyGraph found`);
return;
}
const cyOptions = this.cyOptions || {
// ignored, use nodes and edges
// elements: this.elements,
autolock: this.autolock,
autoungrabify: this.autoungrabify,
autounselectify: this.autounselectify,
boxSelectionEnabled: this.boxSelectionEnabled,
container: this.cyGraph.nativeElement,
desktopTapThreshold: this.desktopTapThreshold,
hideEdgesOnViewport: this.hideEdgesOnViewport,
hideLabelsOnViewport: this.hideLabelsOnViewport,
layout: this.layoutOptions,
maxZoom: this.maxZoom,
minZoom: this.minZoom,
motionBlur: this.motionBlur,
motionBlurOpacity: this.motionBlurOpacity,
pan: this.pan,
panningEnabled: this.panningEnabled,
pixelRatio: this.pixelRatio,
selectionType: this.selectionType,
style: this.style,
styleEnabled: this.styleEnabled,
textureOnViewport: this.textureOnViewport,
touchTapThreshold: this.touchTapThreshold,
userPanningEnabled: this.userPanningEnabled,
userZoomingEnabled: this.userZoomingEnabled,
wheelSensitivity: this.wheelSensitivity,
zoomingEnabled: this.zoomingEnabled,
zoom: this.zoom,
};
// TODO do reset() instead?
if (!this.cy) {
this.cy = cytoscape(cyOptions);
}
this.cy.startBatch();
this.cy.boxSelectionEnabled(this.boxSelectionEnabled);
this.cy.nodes().remove();
this.cy.edges().remove();
if (this.nodes) {
this.cy.add(this.nodes);
}
if (this.edges) {
this.cy.add(this.edges);
}
this.cy.endBatch();
if (this.layoutOptions) {
this.cy.layout(this.layoutOptions).run();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: CytoscapeGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.3", type: CytoscapeGraphComponent, selector: "cytoscape-graph", inputs: { debug: "debug", nodes: "nodes", edges: "edges", autolock: "autolock", autoungrabify: "autoungrabify", autounselectify: "autounselectify", boxSelectionEnabled: "boxSelectionEnabled", desktopTapThreshold: "desktopTapThreshold", hideEdgesOnViewport: "hideEdgesOnViewport", hideLabelsOnViewport: "hideLabelsOnViewport", layoutOptions: "layoutOptions", maxZoom: "maxZoom", minZoom: "minZoom", motionBlur: "motionBlur", motionBlurOpacity: "motionBlurOpacity", pan: "pan", panningEnabled: "panningEnabled", pixelRatio: "pixelRatio", selectionType: "selectionType", style: "style", styleEnabled: "styleEnabled", textureOnViewport: "textureOnViewport", touchTapThreshold: "touchTapThreshold", userPanningEnabled: "userPanningEnabled", userZoomingEnabled: "userZoomingEnabled", wheelSensitivity: "wheelSensitivity", zoom: "zoom", zoomingEnabled: "zoomingEnabled", showToolbar: "showToolbar" }, viewQueries: [{ propertyName: "cyGraph", first: true, predicate: ["cyGraph"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
<p-progressSpinner
*ngIf="loading"
class="spinner"
strokeWidth="4"
fill="#EEEEEE"
animationDuration=".5s"
></p-progressSpinner>
<div #cyGraph class="graphWrapper"></div>
`, isInline: true, styles: [".spinner{position:absolute;left:\"350px\";z-index:10;width:\"250px\";height:\"250px\"}@keyframes ui-progress-spinner-color{to,0%{stroke:#d62d20}40%{stroke:#0057e7}66%{stroke:#008744}80%,90%{stroke:#ffa700}}.graphWrapper{height:100%;width:100%}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.ProgressSpinner, selector: "p-progressSpinner", inputs: ["styleClass", "style", "strokeWidth", "fill", "animationDuration", "ariaLabel"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: CytoscapeGraphComponent, decorators: [{
type: Component,
args: [{ selector: 'cytoscape-graph', template: `
<p-progressSpinner
*ngIf="loading"
class="spinner"
strokeWidth="4"
fill="#EEEEEE"
animationDuration=".5s"
></p-progressSpinner>
<div #cyGraph class="graphWrapper"></div>
`, styles: [".spinner{position:absolute;left:\"350px\";z-index:10;width:\"250px\";height:\"250px\"}@keyframes ui-progress-spinner-color{to,0%{stroke:#d62d20}40%{stroke:#0057e7}66%{stroke:#008744}80%,90%{stroke:#ffa700}}.graphWrapper{height:100%;width:100%}\n"] }]
}], ctorParameters: () => [], propDecorators: { cyGraph: [{
type: ViewChild,
args: ['cyGraph']
}], debug: [{
type: Input
}], nodes: [{
type: Input
}], edges: [{
type: Input
}], autolock: [{
type: Input
}], autoungrabify: [{
type: Input
}], autounselectify: [{
type: Input
}], boxSelectionEnabled: [{
type: Input
}], desktopTapThreshold: [{
type: Input
}], hideEdgesOnViewport: [{
type: Input
}], hideLabelsOnViewport: [{
type: Input
}], layoutOptions: [{
type: Input
}], maxZoom: [{
type: Input
}], minZoom: [{
type: Input
}], motionBlur: [{
type: Input
}], motionBlurOpacity: [{
type: Input
}], pan: [{
type: Input
}], panningEnabled: [{
type: Input
}], pixelRatio: [{
type: Input
}], selectionType: [{
type: Input
}], style: [{
type: Input
}], styleEnabled: [{
type: Input
}], textureOnViewport: [{
type: Input
}], touchTapThreshold: [{
type: Input
}], userPanningEnabled: [{
type: Input
}], userZoomingEnabled: [{
type: Input
}], wheelSensitivity: [{
type: Input
}], zoom: [{
type: Input
}], zoomingEnabled: [{
type: Input
}], showToolbar: [{
type: Input
}] } });
class BaseLayoutOptionsImpl {
ready(e) {
// tslint:disable-next-line:no-console
console.debug(`layout ready, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`); // on layoutready
}
stop(e) {
// tslint:disable-next-line:no-console
console.debug(`layout stop, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`); // on layoutstop
}
}
class NullLayoutOptionsImpl extends BaseLayoutOptionsImpl {
name = 'null';
}
class AnimateLayoutOptionsImpl extends BaseLayoutOptionsImpl {
// the zoom level to set (prob want fit = false if set)
zoom = undefined;
// the pan level to set (prob want fit = false if set)
pan = undefined;
// whether to transition the node positions
animate = false;
// duration of animation in ms if enabled
animationDuration = 500;
// easing of animation if enabled
animationEasing = undefined;
// a function that determines whether the node should be animated.
// All nodes animated by default on animate enabled. Non-animated nodes are
// positioned immediately when the layout starts
animateFilter = (node, i) => true;
}
class PresetLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
name = 'preset';
fit;
padding;
// map of (node id) => (position obj); or function(node){ return somPos; }
positions;
// transform a given node position. Useful for changing flow direction in discrete layouts
transform = (node, position) => position;
}
class ShapedLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
// whether to fit to viewport
fit = true;
// fit padding
padding = 30;
// constrain layout bounds
boundingBox = undefined;
// prevents node overlap, may overflow boundingBox if not enough space
avoidOverlap = true;
// Excludes the label when calculating node bounding boxes for the layout algorithm
nodeDimensionsIncludeLabels = false;
// Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
spacingFactor = 1.75;
// a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
sort = undefined;
// transform a given node position. Useful for changing flow direction in discrete layouts
transform = (node, position) => position;
}
class GridLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
name = 'grid';
// extra spacing around nodes when avoidOverlap: true
avoidOverlapPadding = 10;
// uses all available space on false, uses minimal space on true
condense = false;
// force num of rows in the grid
rows = undefined;
// force num of columns in the grid
cols = undefined;
// returns { row, col } for element
// (node: NodeSingular) => return { row: number; col: number; }
position = null;
}
class RandomLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
name = 'random';
fit = true;
padding = 20;
// constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
boundingBox = undefined;
// transform a given node position. Useful for changing flow direction in discrete layouts
transform = (node, position) => position;
}
class CircleLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
name = 'circle';
radius; // the radius of the circle
startAngle = (3 / 2) * Math.PI; // where nodes start in radians
sweep = undefined; // how many radians should be between the first and last node (defaults to full circle)
clockwise; // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
}
// Note: "radius" is not part of concentric, imperfect extension
class ConcentricLayoutOptionsImpl {
name = 'concentric';
// how many radians should be between the first and last node (defaults to full circle)
sweep;
// whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
clockwise;
// where nodes start in radians, e.g. 3 / 2 * Math.PI,
startAngle = (3 / 2) * Math.PI;
fit;
nodeDimensionsIncludeLabels;
equidistant; // whether levels have an equal radial distance betwen them, may cause bounding box overflow
minNodeSpacing = 10; // min spacing between outside of nodes (used for radius adjustment)
boundingBox; // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
// height of layout area (overrides container height)
height = null;
// width of layout area (overrides container width)
width = null;
// Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
spacingFactor;
concentric(node) {
return 0;
}
levelWidth(node) {
return 0;
}
}
class BreadthFirstLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
name = 'breadthfirst';
// whether the tree is directed downwards (or edges can point in any direction if false)
directed = false;
// put depths in concentric circles if true, put depths top down if false
circle = false;
// the roots of the trees
roots;
// how many times to try to position the nodes in a maximal way (i.e. no backtracking)
maximalAdjustments;
// whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)
maximal = false;
grid = false; // whether to create an even grid into which the DAG is placed (circle:false only)
nodeDimensionsIncludeLabels = false; // Excludes the label when calculating node bounding boxes for the layout algorithm
}
class CoseLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
name = 'cose';
// The layout animates only after this many milliseconds for animate:true
// (prevents flashing on fast runs)
animationThreshold = 250;
// Number of iterations between consecutive screen positions update
refresh = 20;
// Randomize the initial positions of the nodes (true) or use existing positions (false)
randomize = false;
// Extra spacing between components in non-compound graphs
componentSpacing = 40;
// Node repulsion (overlapping) multiplier
nodeOverlap = 4;
// Nesting factor (multiplier) to compute ideal edge length for nested edges
nestingFactor = 1.2;
// Gravity force (constant)
gravity = 1;
// Maximum number of iterations to perform
numIter = 1000;
// Initial temperature (maximum node displacement)
initialTemp = 1000;
// Cooling factor (how the temperature is reduced between consecutive iterations
coolingFactor = 0.99;
// Lower temperature threshold (below this point the layout will end)
minTemp = 1.0;
// Node repulsion (non overlapping) multiplier
nodeRepulsion = (node) => 2048;
// Ideal edge (non nested) length
idealEdgeLength = (edge) => 32;
// Divisor to compute edge forces
edgeElasticity = (edge) => 32;
}
class DagreLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super();
}
name = 'dagre';
nodeSep = undefined; // the separation between adjacent nodes in the same rank
edgeSep = undefined; // the separation between adjacent edges in the same rank
rankSep = undefined; // the separation between each rank in the layout
// TB for top to bottom flow, 'LR' for left to right
rankDir = 'TB';
// Type of algorithm to assign a rank to each node in the input graph.
// Possible values: 'network-simplex', 'tight-tree' or 'longest-path'
ranker = undefined;
// number of ranks to keep between the source and target of the edge
minLen = (edge) => 1;
edgeWeight = (edge) => 1; // higher weight edges are generally made shorter and straighter than lower weight edges
}
class FormInfo {
title;
fieldsets;
showSubmitButton;
submitText;
disableSubmitOnFormInvalid;
otherFieldsetTitle;
constructor(title, fieldsets, showSubmitButton = false, submitText = 'Submit', disableSubmitOnFormInvalid = false,
/* if the model has a property that isn't in a fieldset, but it in an fieldset created by the form */
otherFieldsetTitle = null) {
this.title = title;
this.fieldsets = fieldsets;
this.showSubmitButton = showSubmitButton;
this.submitText = submitText;
this.disableSubmitOnFormInvalid = disableSubmitOnFormInvalid;
this.otherFieldsetTitle = otherFieldsetTitle;
}
}
class FieldsetInfo {
legend;
fieldInfos;
displayOnlyIfProperties;
constructor(legend, fieldInfos, displayOnlyIfProperties) {
this.legend = legend;
this.fieldInfos = fieldInfos;
this.displayOnlyIfProperties = displayOnlyIfProperties;
}
showFieldsetForModel(model) {
if (!this.displayOnlyIfProperties) {
return true;
}
for (const fieldInfo of this.fieldInfos) {
for (const modelProperty of Object.keys(model)) {
if (fieldInfo.modelProperty === modelProperty) {
return true;
}
}
}
return false;
}
}
class FieldInfo {
label;
modelProperty;
type;
tooltip;
validators;
disableWhenInvalid;
hideWhenNoModelProperty;
placeholder;
inputType;
inputSize;
options;
optionArrayLabelField;
optionArrayValueField;
fieldTypes = {};
updateOn = 'submit'; // same as AbstractControlOptions
asyncValidators;
constructor(
/* label to show the user next to the field, can be a function for i18n/dynamic labels */
label,
/* The form has a model, this is the name of the property on the form's model object that this field */
modelProperty,
/* computed from the model property type by default */
type,
/* The tooltip to display on hover */
tooltip,
/* The list of Angular Form Validators for the control or a function that returns such an array */
validators,
/* disable the field if it's not valid */
disableWhenInvalid = false,
/* If true and model[modelProperty] is undefined, don't create a field.*/
hideWhenNoModelProperty = true,
/* Input only - by default the label is used as a placeholder and floats (how to downcast in a template?) */
placeholder,
/* Input only - same as HTML input (how to downcast in a template?) */
inputType = 'text',
/* Input only - same as HTML input (how to downcast in a template?) */
inputSize = 8,
/* Select only either an array of object or the name of a model property or function that is/returns an array of objects */
options,
/* In an options object, what field to display to the user (or function that returns a string
given the option object and the model) */
optionArrayLabelField,
/* In an options object, what field to return for the value of the option
(or function that returns a string given the option object and the model) */
optionArrayValueField) {
this.label = label;
this.modelProperty = modelProperty;
this.type = type;
this.tooltip = tooltip;
this.validators = validators;
this.disableWhenInvalid = disableWhenInvalid;
this.hideWhenNoModelProperty = hideWhenNoModelProperty;
this.placeholder = placeholder;
this.inputType = inputType;
this.inputSize = inputSize;
this.options = options;
this.optionArrayLabelField = optionArrayLabelField;
this.optionArrayValueField = optionArrayValueField;
}
fieldType(model) {
if (!this.modelProperty) {
return this.fieldTypes[0]; // Return first field type if no model property
}
const cached = this.fieldTypes[this.modelProperty];
if (cached) {
return cached;
}
else {
const fieldValueType = typeof model[this.modelProperty];
const result = this.type
? this.type
: this.options
? 'options'
: fieldValueType;
this.fieldTypes[this.modelProperty] = result;
return result;
}
}
setValue(newValue, model, modelChange) {
if (!this.modelProperty) {
return; // cannot set values that don't have a model property
}
model[this.modelProperty] = newValue;
modelChange.emit({ property: this.modelProperty, value: newValue });
}
}
class FluidFormComponent {
model;
modelChange = new EventEmitter();
modelProperty;
formInfo;
formGroup;
constructor() { }
ngOnInit() {
console.debug('FluidFormComponent this.formInfo:', JSON.stringify(this.formInfo));
let controls = {};
this.formInfo?.fieldsets.forEach((fieldsetInfo) => {
fieldsetInfo.fieldInfos.forEach((fieldInfo) => {
if (!this.model || !fieldInfo || !fieldInfo.modelProperty) {
return;
}
let modelValue = this.model[fieldInfo.modelProperty];
// console.log('fieldInfo.modelProperty:', fieldInfo.modelProperty, ', modelValue:', modelValue)
const validators = typeof fieldInfo.validators === 'function'
? fieldInfo.validators()
: fieldInfo.validators;
const asyncValidators = typeof fieldInfo.asyncValidators === 'function'
? fieldInfo.asyncValidators()
: fieldInfo.asyncValidators;
const { updateOn } = fieldInfo;
let formControl = new FormControl(modelValue, {
validators,
asyncValidators,
updateOn,
});
formControl.valueChanges.subscribe((change) => {
if (!this.model || !fieldInfo || !fieldInfo.modelProperty) {
return;
}
console.debug('form control change ', JSON.stringify(change), ' for prop ', fieldInfo.modelProperty, ', changing current model value ', this.model[fieldInfo.modelProperty], ' to ', change);
fieldInfo.setValue(change, this.model, this.modelChange);
});
controls[fieldInfo.modelProperty] = formControl;
});
});
this.formGroup = new FormGroup(controls);
}
ngOnChanges(changes) {
console.debug('ngOnChanges fluid-form changes:', JSON.stringify(changes));
if (changes['model']) {
const model = changes['model'].currentValue;
for (let key of Object.keys(model)) {
console.debug('ngOnChanges model key copying to form:', key);
const control = this.formGroup?.controls[key];
control
? control.setValue(model[key], { emitEvent: false })
: console.warn('no control for model key ', key);
}
}
}
ngAfterViewInit() {
// console.debug("ngAfterViewInit")
}
ngAfterViewChecked() {
// console.debug("ngAfterViewChecked")
}
onSubmit() {
console.log(`Form submitted`);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: FluidFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.3", type: FluidFormComponent, selector: "cyng-fluid-form", inputs: { model: "model", modelProperty: "modelProperty", formInfo: "formInfo" }, outputs: { modelChange: "modelChange" }, usesOnChanges: true, ngImport: i0, template: "<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", styles: [".ui-chkbox-label{padding-right:.5em}.ui-dropdown-label{align-self:center;padding-right:.5em}.field:nth-child(n+4){margin-top:1em}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "component", type: i4.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i5.Fieldset, selector: "p-fieldset", inputs: ["legend", "toggleable", "collapsed", "style", "styleClass", "transitionOptions"], outputs: ["collapsedChange", "onBeforeToggle", "onAfterToggle"] }, { kind: "component", type: i6.InputSwitch, selector: "p-inputSwitch", inputs: ["style", "styleClass", "tabindex", "inputId", "name", "disabled", "readonly", "trueValue", "falseValue", "ariaLabel", "ariaLabelledBy"], outputs: ["onChange"] }, { kind: "directive", type: i7.InputText, selector: "[pInputText]" }, { kind: "directive", type: i8.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: FluidFormComponent, decorators: [{
type: Component,
args: [{ selector: 'cyng-fluid-form', template: "<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", styles: [".ui-chkbox-label{padding-right:.5em}.ui-dropdown-label{align-self:center;padding-right:.5em}.field:nth-child(n+4){margin-top:1em}\n"] }]
}], ctorParameters: () => [], propDecorators: { model: [{
type: Input
}], modelChange: [{
type: Output
}], modelProperty: [{
type: Input
}], formInfo: [{
type: Input
}] } });
class CytoscapeLayoutToolComponent {
static LAYOUT_FORM_INFO = CytoscapeLayoutToolComponent.createLayoutFormInfo();
layoutForm;
_layoutOptions;
changed = false;
get layoutOptions() {
return this._layoutOptions;
}
set layoutOptions(value) {
console.log(`set layoutOptions: ${value?.name}`);
this._layoutOptions = value;
}
layoutOptionsChange = new EventEmitter();
layoutOptionsList = [
new BreadthFirstLayoutOptionsImpl(),
new CoseLayoutOptionsImpl(),
new DagreLayoutOptionsImpl(),
new CircleLayoutOptionsImpl(),
new ConcentricLayoutOptionsImpl(),
new GridLayoutOptionsImpl(),
new PresetLayoutOptionsImpl(),
new RandomLayoutOptionsImpl(),
new NullLayoutOptionsImpl(),
];
formInfo;
constructor() { }
ngOnInit() {
this.formInfo = CytoscapeLayoutToolComponent.createLayoutFormInfo();
let layoutOptionsSelect = this.layoutOptionsList[5];
console.log('setting the initial selected layout, default: ', layoutOptionsSelect.name);
if (this.layoutOptions) {
console.log(`setting the initial selected layout based on input/output layout ${JSON.stringify(this.layoutOptions)}`);
this.addOrReplaceInLayoutOptionsList(this.layoutOptions);
}
console.log('Initializing this.selectedLayoutInfo with layoutOptionsSelect ', JSON.stringify(layoutOptionsSelect));
}
ngOnChanges(changes) {
console.log('ngOnChanges layout changes:', JSON.stringify(changes));
if (changes['layoutOptions']) {
}
}
onLayoutModelChange() {
console.log('Layout model change: ', JSON.stringify(this.layoutOptions));
this.changed = true;
}
onFormModelChange() {
console.log('onFormModelChange');
this.changed = true;
}
onApplyLayout() {
this.changed = false;
this.layoutOptionsChange.emit(this.layoutOptions);
}
addOrReplaceInLayoutOptionsList(layoutOptions) {
let matchingOptions = this.layoutOptionsList.find((selectOption) => selectOption.name === layoutOptions.name);
if (matchingOptions) {
console.log('got matching layoutOptions: ', JSON.stringify(matchingOptions));
this.layoutOptionsList.splice(this.layoutOptionsList.indexOf(matchingOptions), 1, layoutOptions);
}
else {
console.info(`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.`);
this.layoutOptionsList.unshift(layoutOptions);
}
}
static createLayoutFormInfo() {
let fit = new FieldInfo('Fit', 'fit', 'boolean', 'Whether to fit to viewport');
let padding = new FieldInfo('Padding', 'padding', 'number', 'When fit to viewport, padding inside the viewport.');
let fitFieldset = new FieldsetInfo('Fit', [fit, padding], ['fit']);
const zoom = new FieldInfo('Zoom', 'zoom', 'number', 'the zoom level to set (likely want fit = false if set)');
const pan = new FieldInfo('Pan', 'pan', 'number', 'the pan level to set (likely want fit = false if set)');
const animate = new FieldInfo('Animate', 'animate', 'boolean', 'whether to transition the node positions');
const animationDuration = new FieldInfo('Animation Duration', 'animationDuration', 'number', 'duration of animation in ms if enabled');
const animationEasing = new FieldInfo('Animation Easing', 'animationEasing', 'number', 'easing of animation if enabled');
let animationFieldset = new FieldsetInfo('Animation', [zoom, pan, animate, animationDuration, animationEasing], ['animate']);
let avoidOverlap = new FieldInfo('Avoid Overlap', 'avoidOverlap', 'boolean', 'prevents node overlap, may overflow boundingBox if not enough space');
let spacingFactor = new FieldInfo('Spacing Factor', 'spacingFactor', 'number', 'Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up');
let nodeDimensionsIncludeLabels = new FieldInfo('Node Dimensions Include Labels', 'nodeDimensionsIncludeLabels', 'boolean', 'Excludes the label when calculating node bounding boxes for the layout algorithm');
let shapedFieldset = new FieldsetInfo('Shaped', [avoidOverlap, spacingFactor, nodeDimensionsIncludeLabels], ['avoidOverlap']);
let directed = new FieldInfo('Directed', 'breadthFirst', 'boolean', 'whether the tree is breadthFirst downwards (or edges can point in any direction if false)');
let circle = new FieldInfo('Circle', 'circle', 'boolean', 'put depths in concentric circles if true, put depths top down if false');
let maximalAdjustments = new FieldInfo('Maximal Adjustments', 'maximalAdjustments', 'number', 'how many times to try to position the nodes in a maximal way (i.e. no backtracking)');
let maximal = new FieldInfo('Maximal', 'maximal', 'boolean', 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)');
let grid = new FieldInfo('Grid', 'grid', 'boolean', 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)');
let roots = new FieldInfo('Roots', 'roots', 'string', 'the roots of the trees');
let breadthFirstFieldset = new FieldsetInfo('Breadth First', [directed, circle, maximalAdjustments, maximal, grid, roots], ['breadthFirst']);
let nodeSep = new FieldInfo('Node Separation', 'nodeSep', 'number', 'the separation between adjacent nodes in the same rank');
let edgeSep = new FieldInfo('Edge Separation', 'edgeSep', 'number', 'the separation between adjacent edges in the same rank');
let rankSep = new FieldInfo('Rank Separation', 'rankSep', 'number', 'the separation between each rank in the layout');
let ranker = new FieldInfo('Ranker', 'ranker', 'options', 'Type of algorithm to assign a rank to each node in the input graph.');
ranker.options = [
{ name: '', label: '' },
{ name: 'network-simplex', label: 'network-simplex' },
{ name: 'tight-tree', label: 'tight-tree' },
{ name: 'longest-path', label: 'longest-path' },
];
let dagreFieldset = new FieldsetInfo('Dagre', [nodeSep, edgeSep, rankSep, ranker], ['nodeSep']);
let animationThreshold = new FieldInfo('Animation Threshold', 'animationThreshold', 'number', 'The layout animates only after this many milliseconds when animate is true (prevents flashing on fast runs)');
let refresh = new FieldInfo('Refresh', 'refresh', 'number', 'Number of iterations between consecutive screen positions update');
let randomize = new FieldInfo('Randomize', 'randomize', 'boolean', 'Randomize the initial positions of the nodes (true) or use existing positions (false)');
let componentSpacing = new FieldInfo('Component Spacing', 'componentSpacing', 'number', 'Extra spacing between components in non-compound graphs');
let nodeOverlap = new FieldInfo('Node Overlap', 'nodeOverlap', 'number', 'Node repulsion (overlapping) multiplier');
let nestingFactor = new FieldInfo('Nesting Factor', 'nestingFactor', 'number', 'Nesting factor (multiplier) to compute ideal edge length for nested edges');
let gravity = new FieldInfo('Gravity', 'gravity', 'number', 'Gravity force (constant)');
let numIter = new FieldInfo('Max Iterations', 'numIter', 'number', 'Maximum number of iterations to perform');
let initialTemp = new FieldInfo('Initial Temp', 'initialTemp', 'number', 'Initial temperature (maximum node displacement)');
let coolingFactor = new FieldInfo('Cooling Factor', 'coolingFactor', 'number', 'Cooling factor (how the temperature is reduced between consecutive iterations');
let minTemp = new FieldInfo('Min. Temp', 'minTemp', 'number', 'Lower temperature threshold (below this point the layout will end)');
let coseFieldset = new FieldsetInfo('COSE', [
animationThreshold,
refresh,
randomize,
componentSpacing,
nodeOverlap,
nestingFactor,
gravity,
numIter,
initialTemp,
coolingFactor,
minTemp,
], ['coolingFactor']);
let avoidOverlapPadding = new FieldInfo('avoidOverlapPadding', 'avoidOverlapPadding', 'number', 'extra spacing around nodes when avoidOverlap: true');
let condense = new FieldInfo('condense', 'condense', 'boolean', 'uses all available space on false, uses minimal space on true');
let rows = new FieldInfo('Rows', 'rows', 'number', 'force num of rows in the grid');
let cols = new FieldInfo('Columns', 'cols', 'number', 'force num of columns in the grid');
let gridFieldset = new FieldsetInfo('Grid', [avoidOverlapPadding, condense, rows, cols], ['cols']);
let radius = new FieldInfo('Radius', 'radius', 'number', 'the radius of the circle');
let startAngle = new FieldInfo('Start Angle', 'startAngle', 'number', 'where nodes start in radians (default:3 / 2 * Math.PI)');
let sweep = new FieldInfo('Sweep', 'sweep', 'number', 'how many radians should be between the first and last node (defaults to full circle)');
let clockwise = new FieldInfo('Clockwise', 'clockwise', 'number', 'whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)');
let circularFieldSet = new FieldsetInfo('Circular', [radius, startAngle, sweep, clockwise], ['clockwise']);
let equidistant = new FieldInfo('Equidistant', 'equidistant', 'boolean', 'whether levels have an equal radial distance betwen them, may cause bounding box overflow');
let minNodeSpacing = new FieldInfo('Min. Node Spacing', 'minNodeSpacing', 'number', 'min spacing between outside of nodes (used for radius adjustment)');
let height = new FieldInfo('Height', 'height', 'number', '');
let width = new FieldInfo('Width', 'width', 'number', '');
let concentricFieldSet = new FieldsetInfo('Concentric', [equidistant, minNodeSpacing, startAngle, height, width], ['equidistant']);
//boundingBox: undefined // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
return new FormInfo('Layout', [
breadthFirstFieldset,
coseFieldset,
dagreFieldset,
gridFieldset,
circularFieldSet,
concentricFieldSet,
fitFieldset,
animationFieldset,
shapedFieldset,
], false);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.3", ngImport: i0, type: CytoscapeLayoutToolComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.3", type: CytoscapeLayoutToolComponent, selector: "cytoscape-layout-tool", inputs: { layoutOptions: "layoutOptions" }, outputs: { layoutOptionsChange: "layoutOptionsChange" }, viewQueries: [{ propertyName: "layoutForm", first: true, predicate: ["layoutForm"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
<div>
<div style="display: flex;">
<div class="layout-header">Edit Layout</div>
</div>
<p-dropdown
class="layout-dropdown"
name="selectedLayoutInfo"
[options]="layoutOptionsList"
[(ngModel)]="layoutOptions"
optionLabel="name"
(ngModelChange)="onLayoutModelChange()"
>
></p-dropdown
>
<button
class="apply-button"
pButton
label="Apply"
[disabled]="!changed"
(click)="onApplyLayout()"
></button>
</div>
<cyng-fluid-form
[model]="layoutOptions"
[formInfo]="formInfo"
(modelChange)="onFormModelChange()"
></cyng-fluid-form>
`, isInline: true, styles: [":host{width:400px;height:2em}.layout-header{width:100%;height:20px}.layout-dropdown{padding-right:10px}input:disabled{background-color:#cccccc54}\n"], dependencies: [{ kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "component", type: i4.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onB