or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

binding-animation.mdcore-diagram.mddata-models.mdgeometry-collections.mdgraphobject-hierarchy.mdindex.mdinteractive-tools.mdlayout-system.mdtheme-management.mdvisual-elements.md
tile.json

binding-animation.mddocs/

Data Binding and Animation

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.

Data Binding System

Binding Class

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})`)
);

ThemeBinding extends Binding

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'))
);

Animation System

AnimationManager

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'
}

Animation

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'
}

AnimationTrigger

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'
}

Usage Examples

Basic Animations

// 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();

Layout Animations

// 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();
});

Interactive Animations

// 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();
  });
});

Advanced Binding Patterns

// 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')
);

Common Patterns

Responsive Visual Updates

// 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();
    }
  }
});

Performance Optimization

// 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;
  }
}