Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 0.7.8

- **Removed LayerStrategy.topological**: Removed from enum
- Two strategies remain: `pattern` and `longestPath`
- **Documentation updates**: Added missing documentation for 0.7.6 and 0.7.5 features
- Added comprehensive GraphLayout API section to README.md (section 10)
- Enhanced README section 2.4 with startId middle element examples and startType parameter
- Added detailed startId/startType section to CYPHER_GUIDE.md with performance tips
- Updated table of contents

## 0.7.7

- **CONTAINS operator**: Substring matching in WHERE clauses
Expand All @@ -11,7 +21,7 @@
- **GraphLayout API**: Automatic layer/column computation for graph visualizations
- Eliminates hardcoded column positioning (`computeLayout()` on path results)
- Provides `nodeDepths` (structural positioning) and `variableDepths` (grouped positioning)
- Three strategies: `pattern`, `longestPath`, `topological`
- Two strategies: `pattern`, `longestPath` (topological was removed in 0.7.7+2)
- Handles cycles, orphan nodes, disconnected components automatically
- 13 comprehensive tests including diamonds, cycles, and mixed directions

Expand Down
101 changes: 101 additions & 0 deletions CYPHER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,107 @@ MATCH user
MATCH person:Person
```

## Starting from Specific Nodes (startId)

The `startId` parameter lets you start pattern matching from a specific node, making queries more efficient by only exploring relevant subgraphs.

### Basic Usage

```cypher
# Start from a specific person
query.match('person-[:WORKS_FOR]->team', startId: 'alice')
# Only matches paths starting from alice

# Start from a specific team
query.match('person-[:WORKS_FOR]->team', startId: 'engineering')
# Only matches paths where team = engineering
```

### Starting from Any Position

**Important:** `startId` can match **any element** in the pattern, not just the first:

```cypher
# Pattern: a->b->c
# startId can match 'a', 'b', OR 'c'

# Start from first element
query.matchPaths('person->team->project', startId: 'alice')
# Matches paths where person = alice

# Start from middle element
query.matchPaths('person->team->project', startId: 'engineering')
# Matches paths where team = engineering

# Start from last element
query.matchPaths('person->team->project', startId: 'web_app')
# Matches paths where project = web_app
```

### Performance Optimization with startType

When starting from middle or last elements, use `startType` to tell graph_kit which position to check:

```cypher
# Without startType: checks all positions (slower)
query.matchPaths(
'person->team->project',
startId: 'engineering'
)
# Checks if 'engineering' matches person, team, OR project

# With startType: only checks specified type (faster!)
query.matchPaths(
'person->team->project',
startId: 'engineering',
startType: 'Team'
)
# ONLY checks if 'engineering' matches team position
```

### When to Use startType

Use `startType` for better performance when:
- Starting from middle or last elements
- Working with long patterns (4+ elements)
- Running performance-critical queries
- You know the node type of your startId

**Example with long pattern:**

```cypher
# 5-element pattern
query.matchPaths(
'a->b->c->d->e',
startId: 'node_d',
startType: 'NodeTypeD' # Skip checking a, b, c positions
)
```

### Common Patterns

```cypher
# Find all projects for a team (start from middle)
query.matchPaths(
'person->team->project',
startId: 'engineering',
startType: 'Team'
)

# Find all people working on a project (start from end)
query.matchPaths(
'person->team->project',
startId: 'web_app',
startType: 'Project'
)

# Backward traversal from specific node
query.matchPaths(
'project<-[:ASSIGNED_TO]-team<-[:WORKS_FOR]-person',
startId: 'web_app'
)
```

## Node Types and Labels

### Node Types
Expand Down
157 changes: 155 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ In-memory, typed directed multigraph with:
- [7. Comparison with Cypher](#7-comparison-with-cypher)
- [8. Design and performance](#8-design-and-performance)
- [9. JSON Serialization](#9-json-serialization)
- [10. Examples index](#10-examples-index)
- [10. Graph Layout for Visualizations](#10-graph-layout-for-visualizations)
- [11. Examples index](#11-examples-index)
- [License](#license)


Expand Down Expand Up @@ -174,6 +175,57 @@ final webAppTeam = query.match(
print(webAppTeam); // {project: {web_app}, team: {engineering}, person: {alice, bob}}
```

#### Starting from Middle or Last Elements

`startId` can match **any position** in the pattern, not just the first element:

```dart
// Start from middle element (team)
final teamConnections = query.matchPaths(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'engineering' // team is in the middle!
);
// Returns paths where 'team' variable = 'engineering'

// Start from last element (project)
final projectPaths = query.matchPaths(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'web_app' // project is last!
);
// Returns paths where 'project' variable = 'web_app'

// Start from any element in longer chains
final middleChain = query.matchPaths(
'a->b->c->d->e',
startId: 'node_c' // Start from middle 'c' element
);
```

#### Performance Optimization with startType

When starting from middle/last elements, use `startType` to skip unnecessary position checks:

```dart
// Without startType: checks all 3 positions (person, team, project)
final paths = query.matchPaths(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'engineering'
);

// With startType: ONLY checks 'team' position (faster!)
final paths = query.matchPaths(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'engineering',
startType: 'Team' // Optimization hint
);
```

**When to use `startType`:**
- Starting from middle or last positions
- Large patterns (4+ elements)
- Performance-critical queries
- You know the node type of your startId

### 2.5 Row-wise Results - Preserve Path Relationships

```dart
Expand Down Expand Up @@ -852,7 +904,108 @@ final members = query.match('team<-[:MEMBER_OF]-user', startId: 'team1');
print(members['user']); // {alice}
```

## 10. Examples index
## 10. Graph Layout for Visualizations

Automatically compute layer/column positions for graph visualizations, eliminating brittle hardcoded positioning logic.

### The Problem

Hardcoding column positions breaks when graph structure changes:

```dart
// BAD: Hardcoded switch statement - breaks when patterns change
final column = switch (nodeType) {
'Group' => 0,
'Policy' => 1,
'Asset' => 2,
'Virtual' => 3,
_ => 0,
};
```

### The Solution: GraphLayout

```dart
final paths = query.matchPaths('group->policy->asset->virtual');
final layout = paths.computeLayout();

// Column positions computed automatically!
final groupColumn = layout.variableLayer('group'); // 0
final policyColumn = layout.variableLayer('policy'); // 1
final assetColumn = layout.variableLayer('asset'); // 2
final virtualColumn = layout.variableLayer('virtual'); // 3
```

### Key Features

**Automatic positioning**: Computes layer/column for every node based on graph structure

**Two positioning modes**:
- `nodeDepths` - Exact structural position for each node ID
- `variableDepths` - Typical position for grouping by variable name (uses median to handle outliers)

**Handles edge cases gracefully**:
- Orphan nodes (disconnected from roots)
- Cycles
- Multiple disconnected components
- Nodes reachable via multiple paths

### Basic Usage

```dart
// Get path results
final paths = query.matchPaths('group-[:HAS_POLICY]->policy-[:GRANTS_ACCESS]->asset');
final layout = paths.computeLayout();

// Get column for pattern variables
final policyColumn = layout.variableLayer('policy');

// Get column for specific node ID
final nodeColumn = layout.layerFor('node_123');

// Render by column
for (var layer = 0; layer <= layout.maxDepth; layer++) {
final nodesInColumn = layout.nodesInLayer(layer);
renderColumn(layer, nodesInColumn);
}
```

### Layout Strategies

Two strategies available (default: `longestPath`):

```dart
// Pattern order (fast, predictable - follows query left-to-right)
final layout = paths.computeLayout(strategy: LayerStrategy.pattern);

// Longest path (best for complex graphs with diamonds, minimizes crossings)
final layout = paths.computeLayout(strategy: LayerStrategy.longestPath);
```

### GraphLayout Properties

```dart
layout.maxDepth // Number of layers - 1
layout.roots // Root nodes (layer 0 entry points)
layout.allNodes // All unique node IDs in paths
layout.allEdges // All unique edges in paths
layout.nodeDepths // Map<String, int> of node ID → layer
layout.variableDepths // Map<String, int> of variable → typical layer
layout.nodesByLayer // Map<int, Set<String>> of layer → node IDs
```

### Complete Example

See `bin/layout_demo.dart` for a comprehensive before/after comparison showing how GraphLayout eliminates hardcoded positioning.

**Why use GraphLayout?**
- ✓ No hardcoded column positions
- ✓ Automatically adapts to graph structure changes
- ✓ Handles orphan nodes, cycles, disconnected components
- ✓ Works with any pattern, any node types
- ✓ Both structural and grouped positioning available

## 11. Examples index

### Dart CLI Examples
- `bin/showcase.dart` – comprehensive graph demo with multiple query examples
Expand Down
28 changes: 0 additions & 28 deletions lib/src/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ enum LayerStrategy {
/// Assigns each node to MAX depth across all paths to minimize edge crossings.
/// Best for: Complex graphs with diamonds, multiple paths, general visualization.
longestPath,

/// Use topological sort (requires DAG + Graph instance).
/// Falls back to longestPath if graph contains cycles.
/// Best for: Dependency visualization, build systems, task scheduling.
topological,
}

/// Computed layout information for visualizing graph query results.
Expand Down Expand Up @@ -176,15 +171,6 @@ extension PathMatchLayout on List<PathMatch> {
case LayerStrategy.longestPath:
nodeDepths = _computeLongestPathDepths(allNodes, allEdges);
break;
case LayerStrategy.topological:
if (graph == null) {
throw ArgumentError(
'Graph instance required for topological strategy. '
'Either provide graph parameter or use a different strategy.',
);
}
nodeDepths = _computeTopologicalDepths(graph, allNodes, allEdges);
break;
}

// 3. Find roots (nodes with no incoming edges in the path set)
Expand Down Expand Up @@ -338,18 +324,4 @@ extension PathMatchLayout on List<PathMatch> {

return nodeDepths;
}

/// Strategy 3: Topological sort-based layering.
///
/// Uses Kahn's algorithm for DAG ordering. Falls back to longestPath
/// if graph contains cycles.
Map<String, int> _computeTopologicalDepths(
Graph graph,
Set<String> nodes,
Set<EdgeTriple> edges,
) {
// Just use longest path on the path edges - simpler and more reliable
// than trying to use the full graph topology
return _computeLongestPathDepths(nodes, edges);
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: graph_kit
description: A lightweight, in-memory graph library with pattern-based queries and efficient traversal for Dart and Flutter applications.
version: 0.7.7
version: 0.7.8
homepage: https://github.com/vento007/graph_kit
repository: https://github.com/vento007/graph_kit
issue_tracker: https://github.com/vento007/graph_kit/issues
Expand Down
Loading