A Neo4j graph database mapping Brazilian Jiu-Jitsu techniques, positions, and the relationships between them — with a live interactive visualisation and a REST API.
BJJ is inherently a graph problem. Positions connect to techniques, techniques chain into other techniques, some techniques counter others, and everything feeds back into positions. A relational database forces you to flatten that into join tables. Neo4j lets the data keep its natural shape — and opens up queries that would be painful or impossible in SQL, like shortest path between two positions or finding the most "central" technique in the entire game.
(Position)-[:AVAILABLE_FROM]-->(Technique)
(Technique)-[:TRANSITIONS_TO]-->(Position)
(Technique)-[:CHAINS_INTO]---->(Technique)
(Technique)-[:COUNTERS]------->(Technique)
13 positions — Standing, Closed Guard, Open Guard, Half Guard, Butterfly Guard, De La Riva, Ashi Garami, Mount, Back Control, Side Control, Knee on Belly, North-South, Turtle.
43 techniques across six categories:
| Category | Count | Examples |
|---|---|---|
| Submissions | 15 | Rear Naked Choke, Triangle, Armbar, Heel Hook |
| Sweeps | 6 | Butterfly Sweep, Scissor Sweep, Berimbolo |
| Passes | 6 | Knee Slice, Torreando, Double Under |
| Escapes | 4 | Elbow-Knee Escape, Bridge & Roll |
| Takedowns | 5 | Double Leg, Single Leg, Guard Pull |
| Transitions | 1 | Berimbolo |
200+ relationships with weighted frequency data drawn from IBJJF, ADCC, and sub-only event statistics:
| Relationship | Meaning | Weight |
|---|---|---|
AVAILABLE_FROM |
Technique is accessible from this position | Setup frequency (1–100) |
TRANSITIONS_TO |
Technique lands you in this position | Transition frequency (1–100) |
CHAINS_INTO |
Technique naturally flows into another | Combination frequency + notes |
COUNTERS |
Technique specifically defeats another | Counter frequency (1–100) |
(:Position {
id, name, description,
difficulty: 'beginner' | 'intermediate' | 'advanced',
dominant: 'top' | 'bottom' | 'neutral'
})
(:Technique {
id, name, description, type,
difficulty, gi_only,
competition_frequency // 1–100
})
Every technique has a competition_frequency score — a normalised value reflecting how often it appears as a finish or meaningful attempt in high-level competition. The Rear Naked Choke scores 92. The Baseball Bat Choke scores 20. Node sizes in the visualisation are proportional to this value.
- Docker + Docker Compose
- Node.js 18+
docker-compose up -dNeo4j will be available at http://localhost:7474 (login: neo4j / bjjgraph123).
npm installcp .env.example .envThe defaults work out of the box with the Docker Compose setup.
npm run seedThis clears any existing data, creates constraints, and inserts all positions, techniques, and relationships. Output looks like:
🥋 BJJ Move Graph — Database Seeder
🗑️ Clearing existing data...
📐 Creating constraints and indexes...
🥋 Seeding 13 positions...
⚡ Seeding 43 techniques...
🔗 Seeding 62 AVAILABLE_FROM relationships...
🔗 Seeding 31 TRANSITIONS_TO relationships...
🔗 Seeding 24 CHAINS_INTO relationships...
🔗 Seeding 12 COUNTERS relationships...
📊 Database summary:
AVAILABLE_FROM 62
TRANSITIONS_TO 31
CHAINS_INTO 24
Technique 43
COUNTERS 12
Position 13
✅ Seed complete. Open http://localhost:7474 to explore.
npm run devOpen http://localhost:3000 for the interactive graph. The Neo4j browser is at http://localhost:7474 if you want to run raw Cypher queries.
The frontend renders the full graph using Vis.js Network. Nodes are sized by competition frequency and colour-coded by type:
- 🔷 Blue diamonds — Positions
- 🔴 Red — Submissions
- 🟢 Green — Sweeps
- 🟡 Amber — Passes
- 🟣 Purple — Escapes
- 🩵 Cyan — Takedowns
- 🩷 Pink — Transitions
Click any node to see its description, setup frequency, chains, and counters in the sidebar. Use the filter bar to isolate technique types. The Stats tab shows submission chain rankings and the most-connected techniques in the graph.
Base URL: http://localhost:3000
| Method | Endpoint | Description |
|---|---|---|
GET |
/positions |
All positions |
GET |
/positions/:id |
Single position with technique count |
GET |
/positions/:id/techniques |
Techniques available from this position |
GET |
/positions/:id/danger |
Submission threats from this position |
GET |
/positions/:id/paths-to-submission |
Routes to a finish from this position |
| Method | Endpoint | Description |
|---|---|---|
GET |
/techniques |
All techniques (optional ?type=submission) |
GET |
/techniques/most-frequent |
Top techniques by competition frequency |
GET |
/techniques/most-connected |
Hub techniques by total graph connections |
GET |
/techniques/:id |
Full detail — chains, counters, positions |
| Method | Endpoint | Description |
|---|---|---|
GET |
/analysis/graph |
Full node + edge graph for visualisation |
GET |
/analysis/path?from=X&to=Y |
Shortest path between two positions |
GET |
/analysis/submission-chains |
Top technique combination sequences |
GET |
/analysis/sector-stats |
Breakdown by technique type |
GET |
/analysis/position-connectivity |
Positions ranked by technique availability |
GET /positions/closed-guard/danger
[
{ "name": "Triangle Choke", "frequency": 78, "setup_frequency": 75, "difficulty": "intermediate" },
{ "name": "Armbar", "frequency": 85, "setup_frequency": 70, "difficulty": "beginner" },
{ "name": "Kimura", "frequency": 65, "setup_frequency": 60, "difficulty": "beginner" }
]GET /analysis/path?from=closed-guard&to=back-control
{
"from": "closed-guard",
"to": "back-control",
"path_length": 3,
"node_names": ["Closed Guard", "Kimura", "Back Control"],
"rel_types": ["AVAILABLE_FROM", "TRANSITIONS_TO"]
}The cypher/examples.cypher file has 12 ready-to-run queries for the Neo4j browser. A few highlights:
-- Shortest path from closed guard to back control
MATCH (start:Position {id: 'closed-guard'}),
(end:Position {id: 'back-control'})
MATCH path = shortestPath((start)-[*..8]-(end))
RETURN [node IN nodes(path) | coalesce(node.name, '')] AS path, length(path) AS steps;
-- Most connected techniques (hub moves)
MATCH (t:Technique)
OPTIONAL MATCH (t)-[out]->()
OPTIONAL MATCH ()-[in]->(t)
WITH t, count(DISTINCT out) AS outDegree, count(DISTINCT in) AS inDegree
RETURN t.name, inDegree + outDegree AS total_connections
ORDER BY total_connections DESC LIMIT 10;
-- Top submission chains
MATCH (a:Technique {type: 'submission'})-[r:CHAINS_INTO]->(b:Technique {type: 'submission'})
RETURN a.name, b.name, r.frequency, r.notes
ORDER BY r.frequency DESC;bjj-move-graph/
├── docker-compose.yml # Neo4j 5.15 instance
├── src/
│ ├── db/
│ │ └── driver.ts # Neo4j driver singleton
│ ├── seed/
│ │ ├── data/
│ │ │ ├── positions.ts # 13 position definitions
│ │ │ ├── techniques.ts # 43 technique definitions
│ │ │ └── relationships.ts # All weighted relationships
│ │ └── seed.ts # Seeder runner
│ ├── queries/
│ │ ├── byPosition.ts # Position-based queries
│ │ ├── mostFrequent.ts # Frequency and hub analysis
│ │ └── shortestPath.ts # Path-finding queries
│ └── api/
│ ├── server.ts # Express app
│ └── routes/
│ ├── positions.ts
│ ├── techniques.ts
│ └── analysis.ts
├── cypher/
│ └── examples.cypher # 12 example queries for Neo4j browser
└── public/
└── index.html # Vis.js interactive visualisation
Competition frequency values are normalised estimates based on aggregated finish data from IBJJF gi competition, ADCC, and major sub-only promotions. They reflect general prevalence across skill levels — not elite-only or white-belt-only populations. Gi-only techniques (Bow and Arrow Choke, Baseball Bat Choke, etc.) reflect gi competition frequency only.