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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy app
Expand Down Expand Up @@ -41,6 +41,6 @@ jobs:
- uses: astral-sh/setup-uv@v5
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm, cache-dependency-path: frontend/package-lock.json }
- run: cd backend && uv sync && (uv run uvicorn app.main:app --port 8000 &)
- run: cd backend && uv sync --frozen && (uv run uvicorn app.main:app --port 8000 &)
- run: cd frontend && npm ci && npx playwright install --with-deps chromium
- run: cd frontend && npm run test:e2e
133 changes: 82 additions & 51 deletions .serena/project.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
# the name by which the project can be referenced within Serena
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "Arena3Dweb"


# list of languages for which language servers are started; choose from:
# al angular ansible bash clojure
# cpp cpp_ccls crystal csharp csharp_omnisharp
# dart elixir elm erlang fortran
# fsharp go groovy haskell haxe
# hlsl html java json julia
# kotlin lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor powershell python
# python_jedi python_ty r rego ruby
# ruby_solargraph rust scala scss solidity
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
# R dropped: legacy code being deleted through the v3 migration, and its LSP
# (R `languageserver` package) isn't installed — its failure aborted the whole
# language-server manager. Active dev is TS (frontend) + Python (backend).
languages:
- typescript
- python

# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
Expand All @@ -59,23 +22,19 @@ ignore_all_files_in_gitignore: true

# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}

# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []

# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
# Example:
# ignored_paths:
# - "examples/**"
# - ".worktrees/**"
# - "**/bin/**"
# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []

Expand Down Expand Up @@ -135,3 +94,75 @@ read_only_memory_patterns: []
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []

# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
ls_additional_workspace_folders: []

# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nextflow nix ocaml
# pascal perl php php_phpactor php_phpantom
# powershell python python_basedpyright python_jedi python_pyrefly
# python_ty qml r rego ruby
# ruby_solargraph rust scala scss solidity
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some language servers require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple language servers, the first language server that supports a given file will be used for that file.
# The first language server is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
language_servers:
- typescript
- python

# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- .

# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:

# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ Stateless FastAPI — the frontend holds all scene state; the server validates i
- **Within frontend**: components emit/subscribe on the `EventBus` and read/write the `store`; the render loop reacts to `ctx` flags (`renderInterLayerEdgesFlag`, label flags, etc.).

### Network Data Model
- Networks upload as TSV with mandatory columns `SourceNode`, `SourceLayer`, `TargetNode`, `TargetLayer` (optional: `Weight`, `Channel`, edge color columns).
- Networks upload as TSV with mandatory columns `SourceNode`, `SourceLayer`, `TargetNode`, `TargetLayer` (optional: `Weight`, `Channel`, edge color columns). A minimal 2-column edgelist (`SourceNode`, `TargetNode`) is also accepted — all nodes land in a single default layer.
- Node/edge attribute files add per-node color/size/url/description and per-edge (optionally per-channel) color.
- Sessions export/import as JSON with full node/edge/layer/scene state. `POST /api/external` returns a token URL so another app can hand off a session.
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Changelog

## [3.1.0] - 2026-08-10

### Added

- Network upload accepts a minimal 2-column edgelist (`SourceNode`, `TargetNode` only) — all nodes are spread out in a single default layer named `Layer1`. The optional `Weight` and `Channel` columns still apply. A downloadable example lives in the Help → Examples tab.

- Edge weight can now be shown as **edge thickness**, not only opacity. The Edge Actions panel replaces the "Edge Opacity By Weight" checkbox with a "Show Edge Weight As" radio — Nothing / Opacity / Width / Both — plus intra- and inter-layer width sliders for whichever property weight isn't driving. Sessions carry the choice as the independent `edgeOpacityByWeight` and `edgeWidthByWeight` booleans; files written before this default to opacity, so they render unchanged. Thickness needed `Line2` (instanced quads) because WebGL renders every line primitive at exactly 1px regardless of `linewidth`.

### Changed

- Channel curvature sliders reach much further — intra-layer 10–60 (was 10–20), inter-layer 1–30 (was 1–10) — so the channels of a multi-channel edge can be pulled well apart instead of running nearly parallel. Defaults are unchanged, so existing sessions render exactly as before.

### Fixed

- Edges no longer glow. Thick edges cover a large share of the screen, and bloom on all of them was blinding against dark backgrounds. Bloom is now selective (three's off-screen bloom-composer pattern) and reads from node spheres alone, so nodes still glow, edges and layer planes stay crisp, and the extra pass costs a few spheres rather than a second full scene.
- Curved channel edges rendered as dotted lines with beads at the curve points once thickness landed. `LineMaterial`'s `worldUnits` mode assumes a perspective camera — its fragment shader traces a view ray from the camera origin and discards anything farther than half a width from the segment, which under this app's orthographic camera discards along the whole segment. Widths are now screen-space, sized against a shared resolution uniform kept on the frustum size (so the numbers still mean world units), retargeted on resize and for the PNG export.
- Node colors rendered washed out/dark in the 3D scene compared to the 2D navigator. The bloom composer was blitting linear color straight to the sRGB canvas (missing `OutputPass`), and the ambient light was left at the pre-r155 intensity that physical lighting divides by PI.

### `Dependencies`

| Tool | Previous version | New version |
| ----------------- | ---------------- | ------------ |
| fastapi | 0.139.0 | 0.141.1 |
| pandas | 3.0.3 | 3.0.5 |
| uvicorn | 0.50.0 | 0.52.1 |
| httpx2 | 2.5.0 | 2.10.0 |
| mypy | 2.1.0 | 2.3.0 |
| pandas-stubs | 3.0.3.260530 | 3.0.5.260730 |
| ruff | 0.15.20 | 0.16.2 |
| @playwright/test | 1.61.1 | 1.62.1 |
| @types/three | 0.185.0 | 0.185.4 |
| eslint | 10.6.0 | 10.8.1 |
| prettier | 3.9.4 | 3.9.6 |
| typescript-eslint | 8.62.1 | 8.66.0 |
| vite | 8.1.3 | 8.2.1 |
| vitest | 4.1.9 | 4.1.10 |

Backend dependencies updated to latest via `uv lock --upgrade`; transitives moved with them (notably starlette 1.3.1 → 1.6.0 and websockets 16.0 → 17.0.1). No known vulnerabilities on either side. `pydantic-core` stays at 2.46.4 — pydantic pins it exactly. TypeScript stays on 6.0.3: 7.0.2 typechecks fine but typescript-eslint 8.x refuses to load against the TS 7 API, so linting breaks; revisit once typescript-eslint ships TS >=7.1 support. CI now runs `uv sync --frozen` so it installs exactly the locked set the Docker image ships.

## [3.0.0] - 2026-07-27

Full rewrite: migrated from R/Shiny to a **FastAPI (Python) backend + Vite / TypeScript / Three.js frontend**. Algorithms (layouts, clustering, topology) ported 1:1 via python-igraph.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- Badges -->

[![Docker Pulls](https://img.shields.io/docker/pulls/pavlopouloslab/arena3dweb.svg)](https://hub.docker.com/r/pavlopouloslab/arena3d)
[![Docker Pulls](https://img.shields.io/docker/pulls/pavlopouloslab/arena3dweb.svg)](https://hub.docker.com/r/pavlopouloslab/arena3dweb)
[![Live Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://www.arena3d.org)
[![GitHub Repo](https://img.shields.io/badge/GitHub-PavlopoulosLab%2FArena3D-blue)](https://github.com/pavlopoulos-lab/arena3d)

Expand Down
2 changes: 2 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# Input validation
MANDATORY_NETWORK_COLUMNS = ["SourceNode", "SourceLayer", "TargetNode", "TargetLayer"]
OPTIONAL_NETWORK_COLUMNS = ["Channel", "Weight"]
# Layer assigned to every node when a minimal 2-column edgelist omits the layer columns
DEFAULT_LAYER_NAME = "Layer1"
MANDATORY_JSON_OBJECTS = ["layers", "nodes", "edges"]
OPTIONAL_JSON_OBJECTS = ["scene", "universalLabelColor", "direction", "edgeOpacityByWeight"]
MANDATORY_JSON_NODE_COLUMNS = ["name", "layer"]
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class SessionImportResponse(BaseModel):
universalLabelColor: str
direction: bool
edgeOpacityByWeight: bool
edgeWidthByWeight: bool
scramble_nodes: bool
warnings: list[str] = []

Expand Down
2 changes: 2 additions & 0 deletions backend/app/routers/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

@router.post("/api/network", response_model=NetworkModel)
async def upload_network(file: UploadFile) -> NetworkModel:
"""Parse an uploaded network TSV — the 4-column Arena3D format, or a minimal
2-column edgelist (SourceNode, TargetNode) whose nodes all land in one default layer."""
raw_bytes = await file.read()
# DoS fix: reject oversized uploads before pandas parses the whole file
# into memory — MAX_EDGES was only checked *after* the parse completed.
Expand Down
9 changes: 8 additions & 1 deletion backend/app/services/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _validate(df: pd.DataFrame) -> None:
if not set(config.MANDATORY_NETWORK_COLUMNS).issubset(df.columns):
raise NetworkValidationError(
"Your network file must contain at least these four columns: "
"SourceNode, SourceLayer, TargetNode, TargetLayer"
"SourceNode, SourceLayer, TargetNode, TargetLayer — or just "
"SourceNode and TargetNode for a minimal edgelist (single layer)."
)
# Reject rows with an empty/whitespace mandatory cell — a blank becomes NaN
# and would crash EdgeModel construction with a 500 instead of a clean 400.
Expand Down Expand Up @@ -63,6 +64,12 @@ def parse_network_tsv(text: str) -> NetworkModel:
df = pd.read_csv(StringIO(text), sep="\t", dtype=str)
except pd.errors.EmptyDataError as e:
raise NetworkValidationError("The network file is empty or has no columns.") from e

# minimal 2-column edgelist: no layer columns -> every node in one default layer
if "SourceLayer" not in df.columns and "TargetLayer" not in df.columns:
df["SourceLayer"] = config.DEFAULT_LAYER_NAME
df["TargetLayer"] = config.DEFAULT_LAYER_NAME

_validate(df)

# subset legit columns (mandatory + optional Channel/Weight, in fixed order)
Expand Down
3 changes: 3 additions & 0 deletions backend/app/services/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ def normalize_session(data: dict[str, Any]) -> dict[str, Any]:
"universalLabelColor": _default(data.get("universalLabelColor"), "#FFFFFF"),
"direction": _default(data.get("direction"), False),
"edgeOpacityByWeight": _default(data.get("edgeOpacityByWeight"), True),
# Sessions written before edge thickness existed default to off, so
# they keep rendering exactly as they did.
"edgeWidthByWeight": _default(data.get("edgeWidthByWeight"), False),
"scramble_nodes": scramble,
"warnings": warnings,
}
18 changes: 18 additions & 0 deletions backend/tests/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ def test_missing_mandatory_column_400() -> None:
assert "four columns" in resp.json()["detail"]


def test_two_column_edgelist_single_default_layer() -> None:
body = _post("SourceNode\tTargetNode\nA\tB\nB\tC\n").json()
assert body["layers"] == ["Layer1"]
assert {n["layer"] for n in body["nodes"]} == {"Layer1"}
assert len(body["edges"]) == 2


def test_two_column_edgelist_with_optional_columns() -> None:
body = _post("SourceNode\tTargetNode\tWeight\tChannel\nA\tB\t2\tch1\nB\tC\t4\tch2\n").json()
assert body["layers"] == ["Layer1"]
assert body["channels"] == ["ch1", "ch2"]


def test_single_layer_column_still_400() -> None:
resp = _post("SourceNode\tTargetNode\tTargetLayer\nA\tB\tL1\n")
assert resp.status_code == 400


def test_non_numeric_weight_400() -> None:
tsv = f"{HDR}\tWeight\nA\tL1\tB\tL2\theavy\n"
assert _post(tsv).status_code == 400
Expand Down
Loading
Loading