GoJS provides a powerful data binding system that automatically synchronizes model data with visual properties, plus comprehensive animation support for smooth transitions and interactive effects.
Connects model data properties to GraphObject visual properties with automatic updates and optional value conversion.
class Binding {
/**
* Creates a new Binding.
* @param targetProperty - GraphObject property to bind to
* @param sourceProperty - Model data property to bind from (defaults to targetProperty)
*/
constructor(targetProperty: string, sourceProperty?: string);
// Properties
name: string; // Source property name in model data
property: string; // Target property name in GraphObject
converter?: (val: any, obj: GraphObject) => any; // Value conversion function
mode: BindingMode; // OneWay or TwoWay binding
// Fluent API Methods
transform(converter: (val: any, obj: GraphObject) => any): Binding;
makeTwoWay(): Binding;
// Static Creation Methods
static ofModel(): Binding;
static ofObject(objname?: string): Binding;
}
enum BindingMode {
OneWay = 'OneWay',
TwoWay = 'TwoWay'
}Usage Examples:
// Basic property binding
const nodeTemplate = new go.Node('Auto')
.add(
new go.Shape('RoundedRectangle')
.bind('fill', 'color'), // bind fill to data.color
new go.TextBlock()
.bind('text', 'name') // bind text to data.name
);
// Binding with value conversion
const statusNode = new go.Node('Auto')
.add(
new go.Shape('RoundedRectangle')
.bind('fill', 'status', (status) => {
switch (status) {
case 'active': return 'lightgreen';
case 'inactive': return 'lightgray';
case 'error': return 'lightcoral';
default: return 'white';
}
}),
new go.TextBlock()
.bind('text', 'status', (status) => status.toUpperCase())
);
// Two-way binding for editable elements
const editableNode = new go.Node('Auto')
.add(
new go.Shape('Rectangle', { fill: 'white', stroke: 'black' }),
new go.TextBlock({
editable: true,
margin: 4
})
.bindTwoWay('text', 'name') // changes update the model
);
// Complex binding with multiple data properties
const complexNode = new go.Node('Auto')
.add(
new go.Shape('RoundedRectangle')
.bind('fill', '', (data) => {
// Use entire data object for complex logic
if (data.priority === 'high') return 'red';
if (data.completed) return 'lightgreen';
return 'lightblue';
}),
new go.TextBlock()
.bind('text', '', (data) => `${data.name} (${data.priority})`)
);Theme-aware binding that responds to theme changes for consistent visual appearance.
class ThemeBinding extends Binding {
/**
* Creates a new ThemeBinding.
* @param targetProperty - GraphObject property to bind to
* @param theme - Theme name or theme object
* @param sourceProperty - Theme property to bind from
*/
constructor(targetProperty: string, theme: string | object, sourceProperty?: string);
theme: string | object;
}Usage Examples:
// Theme-aware styling
const themedNode = new go.Node('Auto')
.add(
new go.Shape('RoundedRectangle')
.bind('fill', new go.ThemeBinding('fill', 'light', 'primaryColor'))
.bind('stroke', new go.ThemeBinding('stroke', 'light', 'borderColor')),
new go.TextBlock()
.bind('stroke', new go.ThemeBinding('stroke', 'light', 'textColor'))
);Controls diagram animations and transitions with timing and lifecycle management.
class AnimationManager {
// State
isEnabled: boolean;
isAnimating: boolean;
initialAnimationStyle: AnimationStyle;
defaultAnimation: Animation;
// Control Methods
start(): void;
stop(): void;
stopAnimation(animation: Animation): void;
// Animation Registration
canStart(reason: string): boolean;
}
enum AnimationStyle {
None = 'None',
Basic = 'Basic',
UseDefault = 'UseDefault'
}Defines animated property changes over time with easing and trigger support.
class Animation {
/**
* Creates a new Animation.
* @param init - Optional initialization properties
*/
constructor(init?: Partial<Animation>);
// Timing
duration: number; // Animation duration in milliseconds
easing: AnimationEasing; // Easing function
// Trigger
trigger: AnimationTrigger; // When to start animation
// State
isViewportUnconstrained: boolean;
runCount: number;
// Animation Targets
add(obj: GraphObject, property: string, from?: any, to?: any): Animation;
// Control
start(reason?: string): boolean;
stop(): void;
suspend(): void;
resume(): void;
reverse(): Animation;
// Events
finished: ((animation: Animation) => void) | null;
}
enum AnimationEasing {
Linear = 'Linear',
EaseInQuad = 'EaseInQuad',
EaseOutQuad = 'EaseOutQuad',
EaseInOutQuad = 'EaseInOutQuad',
EaseInCubic = 'EaseInCubic',
EaseOutCubic = 'EaseOutCubic',
EaseInOutCubic = 'EaseInOutCubic',
EaseInQuart = 'EaseInQuart',
EaseOutQuart = 'EaseOutQuart',
EaseInOutQuart = 'EaseInOutQuart',
EaseInQuint = 'EaseInQuint',
EaseOutQuint = 'EaseOutQuint',
EaseInOutQuint = 'EaseInOutQuint',
EaseInSine = 'EaseInSine',
EaseOutSine = 'EaseOutSine',
EaseInOutSine = 'EaseInOutSine',
EaseInExpo = 'EaseInExpo',
EaseOutExpo = 'EaseOutExpo',
EaseInOutExpo = 'EaseInOutExpo',
EaseInCirc = 'EaseInCirc',
EaseOutCirc = 'EaseOutCirc',
EaseInOutCirc = 'EaseInOutCirc',
EaseInElastic = 'EaseInElastic',
EaseOutElastic = 'EaseOutElastic',
EaseInOutElastic = 'EaseInOutElastic',
EaseInBack = 'EaseInBack',
EaseOutBack = 'EaseOutBack',
EaseInOutBack = 'EaseInOutBack',
EaseInBounce = 'EaseInBounce',
EaseOutBounce = 'EaseOutBounce',
EaseInOutBounce = 'EaseInOutBounce'
}Defines when animations should start based on diagram events or conditions.
class AnimationTrigger {
/**
* Creates a new AnimationTrigger.
* @param init - Optional initialization properties
*/
constructor(init?: Partial<AnimationTrigger>);
// Trigger Conditions
startCondition: TriggerStart;
// Methods
checkTrigger(reason: string): boolean;
}
enum TriggerStart {
Immediate = 'Immediate',
LayoutCompleted = 'LayoutCompleted',
SubGraphExpanded = 'SubGraphExpanded',
SubGraphCollapsed = 'SubGraphCollapsed'
}// Simple position animation
const moveAnimation = new go.Animation({
duration: 1000,
easing: go.AnimationEasing.EaseInOutQuad
});
moveAnimation.add(myNode, 'position', myNode.position, new go.Point(200, 100));
moveAnimation.start();
// Opacity fade animation
const fadeAnimation = new go.Animation({
duration: 500,
easing: go.AnimationEasing.EaseOutQuad
});
fadeAnimation.add(myNode, 'opacity', 1, 0);
fadeAnimation.finished = (anim) => {
diagram.remove(myNode); // Remove after fade
};
fadeAnimation.start();
// Multi-property animation
const transformAnimation = new go.Animation({
duration: 2000,
easing: go.AnimationEasing.EaseInOutCubic
});
transformAnimation.add(myNode, 'position', myNode.position, new go.Point(300, 200));
transformAnimation.add(myNode, 'angle', 0, 45);
transformAnimation.add(myNode, 'scale', 1, 1.5);
transformAnimation.start();// Animate layout changes
function animateLayoutChange(newLayout: go.Layout) {
// Store current positions
const oldPositions = new go.Map<go.Part, go.Point>();
diagram.nodes.each(node => {
oldPositions.add(node, node.position.copy());
});
// Apply new layout without animation
diagram.animationManager.isEnabled = false;
diagram.layout = newLayout;
diagram.layoutDiagram(true);
diagram.animationManager.isEnabled = true;
// Create animation to new positions
const layoutAnimation = new go.Animation({
duration: 1500,
easing: go.AnimationEasing.EaseInOutQuad
});
diagram.nodes.each(node => {
const oldPos = oldPositions.getValue(node);
const newPos = node.position;
if (oldPos && !oldPos.equals(newPos)) {
node.position = oldPos; // Reset to start position
layoutAnimation.add(node, 'position', oldPos, newPos);
}
});
layoutAnimation.start();
}
// Animate group expansion/collapse
diagram.addDiagramListener('SubGraphExpanded', (e) => {
const group = e.subject;
// Animate member nodes fading in
const expandAnimation = new go.Animation({
duration: 300,
easing: go.AnimationEasing.EaseOutQuad
});
group.memberParts.each(member => {
expandAnimation.add(member, 'opacity', 0, 1);
});
expandAnimation.start();
});// Hover animations
const nodeTemplate = new go.Node('Auto', {
mouseEnter: (e, node) => {
const hoverAnimation = new go.Animation({
duration: 200,
easing: go.AnimationEasing.EaseOutQuad
});
const shape = node.findObject('SHAPE') as go.Shape;
if (shape) {
hoverAnimation.add(shape, 'strokeWidth', shape.strokeWidth, 3);
hoverAnimation.add(node, 'scale', node.scale, 1.1);
}
hoverAnimation.start();
},
mouseLeave: (e, node) => {
const restoreAnimation = new go.Animation({
duration: 200,
easing: go.AnimationEasing.EaseOutQuad
});
const shape = node.findObject('SHAPE') as go.Shape;
if (shape) {
restoreAnimation.add(shape, 'strokeWidth', shape.strokeWidth, 1);
restoreAnimation.add(node, 'scale', node.scale, 1);
}
restoreAnimation.start();
}
})
.add(
new go.Shape('RoundedRectangle', {
name: 'SHAPE',
fill: 'lightblue',
stroke: 'blue',
strokeWidth: 1
}),
new go.TextBlock({ margin: 8 })
.bind('text', 'name')
);
// Selection animations
diagram.addDiagramListener('ChangedSelection', (e) => {
e.subject.each((part: go.Part) => {
const selectionAnimation = new go.Animation({
duration: 150,
easing: go.AnimationEasing.EaseOutQuad
});
if (part.isSelected) {
// Animate selection
selectionAnimation.add(part, 'scale', 1, 1.05);
} else {
// Animate deselection
selectionAnimation.add(part, 'scale', part.scale, 1);
}
selectionAnimation.start();
});
});// Conditional binding with complex logic
const advancedTemplate = new go.Node('Auto')
.add(
new go.Shape('RoundedRectangle')
.bind('fill', '', (data, obj) => {
// Access both data and GraphObject for complex decisions
const node = obj.part as go.Node;
const isHighlighted = node?.isHighlighted;
if (data.status === 'error') return isHighlighted ? 'red' : 'lightcoral';
if (data.status === 'warning') return isHighlighted ? 'orange' : 'lightyellow';
if (data.status === 'success') return isHighlighted ? 'green' : 'lightgreen';
return isHighlighted ? 'blue' : 'lightblue';
})
.bind('strokeWidth', 'priority', (priority) => priority === 'high' ? 3 : 1),
new go.TextBlock({ margin: 8 })
.bind('text', '', (data) => {
// Multi-field text composition
let text = data.name || 'Unnamed';
if (data.count !== undefined) text += ` (${data.count})`;
if (data.status) text += ` [${data.status.toUpperCase()}]`;
return text;
})
.bind('font', 'importance', (importance) => {
const size = importance === 'high' ? '14pt' : '12pt';
const weight = importance === 'high' ? 'bold' : 'normal';
return `${weight} ${size} sans-serif`;
})
);
// Model-level binding for diagram properties
diagram.bind('background', '', (modelData) => {
return modelData.theme === 'dark' ? '#2d2d2d' : 'white';
});
// Object-level binding for accessing parent data
const groupMemberTemplate = new go.Node('Auto')
.add(
new go.Shape('Rectangle')
.bind('fill', '', (data, obj) => {
const node = obj.part as go.Node;
const group = node?.containingGroup;
const groupData = group?.data;
// Use group data to influence member appearance
return groupData?.memberColor || data.color || 'white';
}),
new go.TextBlock({ margin: 4 })
.bind('text', 'name')
);// Update visuals based on data changes
model.addChangedListener((e) => {
if (e.change === go.ChangeType.Property && e.propertyName === 'status') {
const node = diagram.findNodeForData(e.object);
if (node) {
// Animate status change
const statusAnimation = new go.Animation({
duration: 300,
easing: go.AnimationEasing.EaseInOutQuad
});
const shape = node.findObject('SHAPE') as go.Shape;
if (shape) {
statusAnimation.add(shape, 'opacity', 0.5, 1);
statusAnimation.add(node, 'scale', 1.2, 1);
}
statusAnimation.start();
}
}
});// Batch animations for better performance
function animateMultipleNodes(nodes: go.List<go.Node>, targetPositions: go.Map<go.Node, go.Point>) {
const batchAnimation = new go.Animation({
duration: 1000,
easing: go.AnimationEasing.EaseInOutQuad
});
nodes.each(node => {
const targetPos = targetPositions.getValue(node);
if (targetPos) {
batchAnimation.add(node, 'position', node.position, targetPos);
}
});
batchAnimation.start();
}
// Disable animations during bulk operations
function bulkUpdate(updateFunc: () => void) {
const wasEnabled = diagram.animationManager.isEnabled;
diagram.animationManager.isEnabled = false;
try {
updateFunc();
} finally {
diagram.animationManager.isEnabled = wasEnabled;
}
}