ExCodeView
Interactive code visualizations for Elixir projects. Analyzes your codebase and generates self-contained HTML files you can open in any browser.
Views
City
3D software city. Modules are buildings (height = lines of code), namespaces are districts, coupling dependencies are arcs between buildings. Uses Three.js with an isometric camera.
mix view city --openERD
Entity-Relationship Diagram for Ecto schemas. Shows tables grouped by Phoenix context with associations drawn between them. Cross-boundary associations (between contexts) are highlighted in red.
mix view erd --openSchemas start expanded with fields and association details visible. Click to collapse. Hover to bold connected association lines. Pan with mouse drag, zoom with scroll wheel.
The ERD extracts schema information directly from AST (Code.string_to_quoted/2) — your project does not need to be compiled and Ecto does not need to be a dependency of ExCodeView.
Installation
Add to your mix.exs as a dev dependency:
def deps do
[
{:ex_code_view, "~> 0.1.0"}
]
endOr from a local path:
{:ex_code_view, path: "../ex_code_view"}Usage
# Default view (city)
mix view
# Specific view
mix view erd
mix view city
# Open in browser after generation
mix view erd --open
# Custom output path
mix view erd -o docs/erd.html
# Raw JSON (for debugging or custom viewers)
mix view --json -o analysis.json
# List available views
mix view --listCLI Options
All configuration can be overridden per-invocation via CLI flags:
| Flag | Description |
|---|---|
--output, -o | Exact output file path |
--output-dir | Directory for generated files |
--output-template |
Filename template (default: ex_code_view) |
--view | Initial active tab (default: first registered view) |
--open | Open in browser after generation |
--json | Output raw analysis JSON |
--list | List available views and exit |
--source-dir |
Directory to scan (default: lib) |
--extensions | File extensions to include (repeatable) |
--exclude | Glob patterns to exclude (repeatable) |
Template variables: {{date}} (ISO 8601 UTC date).
Precedence: --output > --output-dir + --output-template > application config > defaults.
# Custom output directory and naming
mix view --output-dir docs --output-template "ex_code_view-{{date}}"
# Override source scanning
mix view --source-dir src --extensions .ex --extensions .exs --exclude "generated/**"Application Config
Defaults can be set in config/config.exs under the :ex_code_view key. CLI flags always take precedence.
config :ex_code_view,
output_dir: "tmp",
default_view: "erd",
source_dir: "lib",
extensions: [".ex"],
exclude: ["generated"],
views: [MyPackage.Views.Sunburst]| Key | Default | Description |
|---|---|---|
:output_dir | CWD | Directory for generated HTML/JSON files |
:default_view | "city" |
View used when no view name is passed to mix view |
:source_dir | "lib" | Directory to scan for source files |
:extensions | [".ex"] | File extensions to include in analysis |
:exclude | [] | Patterns to exclude from analysis (matched against relative paths) |
:views | [] | External view modules to register (see "Creating Custom Views") |
CI/CD
ExCodeView has no runtime overhead — no processes, no supervision tree — so it works in any MIX_ENV. Generate visualizations as part of your deployment pipeline or documentation build.
GitHub Actions
- name: Generate visualization
run: |
mix compile
mix view --output-dir doc/visualizationsExDoc Extras
Generate the visualization into your docs directory and reference it as an ExDoc extra:
mix view --output-dir doc/visualizations# mix.exs
def project do
[
docs: [
extras: ["doc/visualizations/ex_code_view.html"]
]
]
endCustom Naming for Versioned Artifacts
Use --output-template to include dates or other identifiers:
mix view --output-dir artifacts --output-template "ex_code_view-{{date}}"
# Produces: artifacts/ex_code_view-2026-04-20.htmlHow it works
- Walker discovers
.exfiles underlib/ - Parser extracts module definitions from AST (
Code.string_to_quoted/2) - Graph builds namespace hierarchy from directory structure
- Coupling reads the compiler manifest for cross-module dependencies (optional)
- SchemaExtractor (ERD only) extracts Ecto schema fields and associations from AST
- Renderer injects JSON data + JS into an HTML template
Output is a single self-contained HTML file with embedded data and JavaScript. No server required.
Creating Custom Views
ExCodeView is extensible. Third-party packages can add new visualizations.
1. Implement the behaviour
defmodule MyPackage.Views.Sunburst do
@behaviour ExCodeView.View
@impl true
def name, do: "sunburst"
@impl true
def description, do: "Sunburst diagram of module hierarchy"
@impl true
def template_path do
Path.join(:code.priv_dir(:my_package), "views/sunburst/template.html")
end
@impl true
def js_sources do
base = Path.join(:code.priv_dir(:my_package), "views/sunburst/js")
~w(lib.js app.js) |> Enum.map(&Path.join(base, &1))
end
@impl true
def prepare(analysis, _opts), do: {:ok, analysis}
end2. Create the template
Your HTML template needs two placeholders:
{{DATA}}— replaced with the analysis JSON{{SCRIPT}}— replaced with your concatenated JS
<!DOCTYPE html>
<html>
<body>
<script>window.__DATA__ = {{DATA}};</script>
<script type="module">
{{SCRIPT}}
</script>
</body>
</html>3. Register the view
In the consuming application's config:
config :ex_code_view, views: [MyPackage.Views.Sunburst]
Then: mix view sunburst --open
4. Use the analysis API
If your view needs to run the analysis pipeline programmatically:
{:ok, analysis} = ExCodeView.analyze(project_dir)
# analysis.modules, analysis.namespaces, analysis.dependencies, etc.
The prepare/2 callback receives this analysis struct and can transform it (add fields, filter data, etc.) before it's injected into the template as JSON.
Creating Custom Schema Extractors
Schema extractors pull data model information from module ASTs. The built-in extractor handles Ecto schemas. Implement ExCodeView.SchemaExtractor to support other frameworks (e.g., Ash).
The behaviour
@callback extract(module_body :: Macro.t(), module_id :: String.t()) ::
{:ok, ErdSchema.t()} | :skipmodule_body— the AST inside adefmoduleblockmodule_id— fully qualified module name (e.g.,"MyApp.Accounts.User")-
Return
{:ok, %ErdSchema{}}if the module defines a data model,:skipotherwise
ErdSchema struct
%ExCodeView.Schema.ErdSchema{
module_id: "MyApp.Accounts.User",
table_name: "users",
fields: [%ErdField{name: "email", type: "string"}],
associations: [%ErdAssociation{type: "has_many", name: "posts", target: "MyApp.Blog.Post"}]
}
Association types: "belongs_to", "has_many", "has_one", "many_to_many".
See ExCodeView.SchemaExtractors.Ecto for a complete reference implementation.
Programmatic API
{:ok, analysis} = ExCodeView.analyze(project_dir, opts)
Returns {:ok, %ExCodeView.Schema.Analysis{}} or {:error, reason}.
Options
| Option | Type | Default | Description |
|---|---|---|---|
:source_dir | string |
from config or "lib" | Directory to scan |
:extensions | list |
from config or [".ex"] | File extensions to include |
:exclude | list |
from config or [] | Glob patterns to exclude |
Analysis struct
The returned struct contains:
| Field | Type | Description |
|---|---|---|
roots | list of strings | Top-level namespace names |
modules |
list of %Module{} | All discovered modules with id, file, depth, namespace, metrics |
namespaces |
list of %Namespace{} | Namespace hierarchy with id, path, root, modules, children |
dependencies |
list of %Dependency{} | Cross-module dependencies with from, to, count, dep_type |
erd_schemas |
list of %ErdSchema{} | Data model schemas (populated by ERD view's prepare/2) |
available_metrics | list of strings |
Metric keys present in module metrics ("loc", "public_functions") |
JSON Schema
The JSON injected into view templates follows this structure:
{
"roots": ["my_app"],
"modules": [
{"id": "MyApp.Foo", "file": "my_app/foo.ex", "depth": 0,
"namespace": "my_app", "metrics": {"loc": 42, "public_functions": 3}}
],
"namespaces": [
{"id": "my_app", "path": "my_app", "root": "my_app",
"modules": ["MyApp.Foo"], "children": []}
],
"dependencies": [
{"from": "MyApp.Foo", "to": "MyApp.Bar", "count": 1,
"dep_type": "runtime", "references": []}
],
"erd_schemas": [
{"module_id": "MyApp.Foo", "table_name": "foos",
"fields": [{"name": "title", "type": "string"}],
"associations": [{"type": "belongs_to", "name": "bar", "target": "MyApp.Bar"}]}
],
"available_metrics": ["loc", "public_functions"]
}
Module IDs are dot-separated (MyApp.Accounts.User). Namespace IDs are slash-separated (my_app/accounts), matching the directory structure.
LLM Integration
ExCodeView ships with usage_rules support. When you use usage_rules in your project, your LLM automatically gets instructions for using and extending ExCodeView.
Setup
-
Add both dependencies to your
mix.exs:
def deps do
[
{:ex_code_view, "~> 0.1.0"},
{:usage_rules, "~> 1.1", only: :dev}
]
end-
Configure
usage_rulesinconfig/config.exs:
config :usage_rules,
file: "AGENTS.md",
usage_rules: [:ex_code_view]To also generate pre-built skills:
config :usage_rules,
file: "AGENTS.md",
usage_rules: [:ex_code_view],
skills: [
location: ".claude/skills",
deps: [:ex_code_view]
]- Run sync:
mix usage_rules.syncWhat's included
Main rules (usage-rules.md) — Overview, CLI usage, configuration, programmatic API, JSON schema, and common gotchas.
Sub-rules:
ex_code_view:views— Creating custom views with theExCodeView.Viewbehaviourex_code_view:schema-extractors— Creating custom schema extractors with theExCodeView.SchemaExtractorbehaviourex_code_view:viewer-js— Writing viewer JavaScript (concatenation pattern, lib.js convention, testing)
Skills:
add-visualization— Step-by-step guide for creating a new visualization end-to-endconfigure-ex-code-view— Installation, configuration, common recipes, and troubleshooting
License
MIT