Resume

GraphLoop

https://graphloop.app

About

It started as a personal frustration project - I was trying to quickly balance statistics during another gamejam and got sick of trying to track formulas across Excel, Desmos, and WolframAlpha. The problem I personally needed solved is the functionality of spreadsheets not being readable enough to optimally tackle balancing of game systems.
Now it’s become a little simulation playground where you can connect variables, build graphs, and run experiments.

It includes: dark mode, multi-canvas workspace, hierarchical object management, json/csv/pdf export and import, accessibility features like ARIA labels and titles.

The application uses React for its user interface, Tailwind CSS for styling, and Zustand for global state management, ensuring a responsive experience.

Bidirectional Dependency System

The main and most powerful feature is the bidirectional relationship between connected variables. When X changes, Y automatically updates based on the formula. Conversely, when Y is manually changed, the system solves for X using inverse calculations.

updateStat: (objectId, statId, value) => set((state) => {
  const updatedStats = new Set<string>();
  
  const updateStatAndCascade = (
    currentState: any, 
    targetObjectId: string | null, 
    targetStatId: string, 
    newValue: number,
    depth: number = 0
  ): any => {
    // Prevent infinite loops
    if (depth > 10 || updatedStats.has(targetStatId)) {
      return currentState;
    }
    
    updatedStats.add(targetStatId);
    
    // Update the stat value
    let newState = { ...currentState };
    if (!targetObjectId) {
      newState.globalStats = currentState.globalStats.map(stat =>
        stat.id === targetStatId ? { ...stat, value: newValue } : stat
      );
    } else {
      newState.objects = currentState.objects.map(obj =>
        obj.id === targetObjectId
          ? {
              ...obj,
              stats: obj.stats.map(stat =>
                stat.id === targetStatId ? { ...stat, value: newValue } : stat
              )
            }
          : obj
      );
    }
    
    // Find and process bidirectional dependencies
    const relevantDependencies = currentState.statDependencies.filter(dep => 
      dep.bidirectional && (dep.sourceStatId === targetStatId || dep.targetStatId === targetStatId)
    );
    
    for (const dependency of relevantDependencies) {
      try {
        let calculatedValue: number;
        let nextTargetStatId: string;
        
        if (dependency.sourceStatId === targetStatId) {
          // Forward: X changed, update Y
          calculatedValue = evaluateFormula(dependency.formula, newValue);
          nextTargetStatId = dependency.targetStatId;
        } else {
          // Reverse: Y changed, solve for X
          calculatedValue = solveForX(dependency.formula, newValue);
          nextTargetStatId = dependency.sourceStatId;
        }
        
        if (isFinite(calculatedValue)) {
          // Recursively cascade to next dependency
          newState = updateStatAndCascade(
            newState, 
            nextTargetObjectId, 
            nextTargetStatId, 
            calculatedValue, 
            depth + 1
          );
        }
      } catch (error) {
        console.warn('Error evaluating bidirectional dependency:', error);
      }
    }
    
    return newState;
  };
  
  return updateStatAndCascade(state, objectId, statId, value, 0);
}),

Data Persistence and Cleanup

Cleanup routines to prevent memory leaks.

cleanupOrphanedDependencies: (existingGraphNodeIds) => set((state) => {
  console.log('Cleaning up orphaned dependencies...');
  
  const orphanedDependencies = state.statDependencies.filter(dep => 
    !existingGraphNodeIds.has(dep.graphNodeId)
  );
  
  if (orphanedDependencies.length > 0) {
    const cleanedDependencies = state.statDependencies.filter(dep => 
      existingGraphNodeIds.has(dep.graphNodeId)
    );
    
    return { statDependencies: cleanedDependencies };
  }
  
  return state;
}),