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 --open

ERD

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 --open

Schemas 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"}
  ]
end

Or 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 --list

CLI 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/visualizations

ExDoc 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"]
    ]
  ]
end

Custom 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.html

How it works

  1. Walker discovers .ex files under lib/
  2. Parser extracts module definitions from AST (Code.string_to_quoted/2)
  3. Graph builds namespace hierarchy from directory structure
  4. Coupling reads the compiler manifest for cross-module dependencies (optional)
  5. SchemaExtractor (ERD only) extracts Ecto schema fields and associations from AST
  6. 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}
end

2. Create the template

Your HTML template needs two placeholders:

<!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()} | :skip

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

  1. Add both dependencies to your mix.exs:
def deps do
  [
    {:ex_code_view, "~> 0.1.0"},
    {:usage_rules, "~> 1.1", only: :dev}
  ]
end
  1. Configure usage_rules in config/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]
  ]
  1. Run sync:
mix usage_rules.sync

What's included

Main rules (usage-rules.md) — Overview, CLI usage, configuration, programmatic API, JSON schema, and common gotchas.

Sub-rules:

Skills:

License

MIT