# 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.
```bash
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.
```bash
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:
```elixir
def deps do
[
{:ex_code_view, "~> 0.1.0"}
]
end
```
Or from a local path:
```elixir
{:ex_code_view, path: "../ex_code_view"}
```
## Usage
```bash
# 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.
```bash
# 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.
```elixir
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
```yaml
- 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:
```bash
mix view --output-dir doc/visualizations
```
```elixir
# 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:
```bash
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
```elixir
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:
- `{{DATA}}` — replaced with the analysis JSON
- `{{SCRIPT}}` — replaced with your concatenated JS
```html
<!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:
```elixir
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:
```elixir
{: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
```elixir
@callback extract(module_body :: Macro.t(), module_id :: String.t()) ::
{:ok, ErdSchema.t()} | :skip
```
- `module_body` — the AST inside a `defmodule` block
- `module_id` — fully qualified module name (e.g., `"MyApp.Accounts.User"`)
- Return `{:ok, %ErdSchema{}}` if the module defines a data model, `:skip` otherwise
### ErdSchema struct
```elixir
%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
```elixir
{: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:
```json
{
"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](https://hex.pm/packages/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`:
```elixir
def deps do
[
{:ex_code_view, "~> 0.1.0"},
{:usage_rules, "~> 1.1", only: :dev}
]
end
```
2. Configure `usage_rules` in `config/config.exs`:
```elixir
config :usage_rules,
file: "AGENTS.md",
usage_rules: [:ex_code_view]
```
To also generate pre-built skills:
```elixir
config :usage_rules,
file: "AGENTS.md",
usage_rules: [:ex_code_view],
skills: [
location: ".claude/skills",
deps: [:ex_code_view]
]
```
3. Run sync:
```bash
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:**
- `ex_code_view:views` — Creating custom views with the `ExCodeView.View` behaviour
- `ex_code_view:schema-extractors` — Creating custom schema extractors with the `ExCodeView.SchemaExtractor` behaviour
- `ex_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-end
- `configure-ex-code-view` — Installation, configuration, common recipes, and troubleshooting
## License
MIT