diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 70d7ae1..0000000 --- a/.claude/launch.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { "name": "norma", "runtimeExecutable": "python3", "runtimeArgs": ["backend/server.py", "--mode", "local", "--port", "8765", "--no-open"], "port": 8765 } - ] -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 98f7754..0000000 --- a/.dockerignore +++ /dev/null @@ -1,20 +0,0 @@ -# Keep the build context to what the image needs. -.git -.github -.serena -.claude -docs -deploy -tests -backend/tests -backend/.venv -backend/.ruff_cache -backend/.mypy_cache -backend/.pytest_cache -frontend/node_modules -frontend/dist -frontend/test-results -frontend/playwright-report -frontend/e2e -**/__pycache__ -*.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 1119d14..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - -on: - push: - branches: [master, NORMA_v3, update_architecture] - pull_request: - -jobs: - backend: - runs-on: ubuntu-latest - defaults: - run: { working-directory: backend } - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - run: uv sync --frozen - - run: uv run ruff check . - - run: uv run ruff format --check . - - run: uv run mypy norma - - run: uv run pytest - - frontend: - runs-on: ubuntu-latest - defaults: - run: { working-directory: frontend } - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: { node-version: 22, cache: npm, cache-dependency-path: frontend/package-lock.json } - - run: npm ci - - run: npm run lint - - run: npx prettier --check src e2e - - run: npx tsc --noEmit - - run: npm test -- --run - - run: npm run build - - e2e: - runs-on: ubuntu-latest - needs: [backend, frontend] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: { node-version: 22, cache: npm, cache-dependency-path: frontend/package-lock.json } - - run: cd frontend && npm ci && npx playwright install --with-deps chromium - # playwright.config.ts builds the page and starts backend/server.py itself - - run: cd frontend && npm run test:e2e diff --git a/.gitignore b/.gitignore index 401f651..4a1f431 100644 --- a/.gitignore +++ b/.gitignore @@ -40,16 +40,3 @@ vignettes/*.pdf user_temp/*.html user_temp/*.rda - -# Python -__pycache__/ -.pytest_cache/ -.ruff_cache/ -.mypy_cache/ -.venv/ - -# Node -node_modules/ -frontend/dist/ -frontend/test-results/ -frontend/playwright-report/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index d2e1dd3..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 - hooks: - - id: ruff-check - args: [--fix] - files: ^backend/ - - id: ruff-format - files: ^backend/ - - - repo: local - hooks: - - id: eslint - name: eslint - language: system - entry: bash -c 'cd frontend && npx eslint src e2e' - files: ^frontend/(src|e2e)/.*\.ts$ - pass_filenames: false - - id: prettier - name: prettier - language: system - entry: bash -c 'cd frontend && npx prettier --check src e2e' - files: ^frontend/(src|e2e)/ - pass_filenames: false - - id: tsc - name: tsc - language: system - entry: bash -c 'cd frontend && npx tsc --noEmit' - files: ^frontend/.*\.ts$ - pass_filenames: false diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 2e510af..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/cache -/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 1d3b834..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,169 +0,0 @@ -# the name by which the project can be referenced within Serena/when chatting with the LLM. -project_name: "NORMA" - -# 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 deno -# elixir elm erlang fortran fsharp -# gdscript gleam 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 -# wolfram 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 Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH) -# - 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 - -# 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" - -# 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 - -# line ending convention to use when writing source files. -# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) -# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. -line_ending: - -# The language backend to use for this project. -# If not set, the global setting from serena_config.yml is used. -# Valid values: LSP, JetBrains -# Note: the backend is fixed at startup. If a project with a different backend -# is activated post-init, an error will be returned. -language_backend: - -# whether to use project's .gitignore files to ignore files -ignore_all_files_in_gitignore: true - -# advanced configuration option allowing to configure language server-specific options. -# Maps the language key to the options. -# 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 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: -- "." - -# 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 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: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. -# This extends the existing exclusions (e.g. from the global configuration) -# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html -excluded_tools: [] - -# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). -# This extends the existing inclusions (e.g. from the global configuration). -# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html -included_optional_tools: [] - -# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. -# This cannot be combined with non-empty excluded_tools or included_optional_tools. -# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html -fixed_tools: [] - -# list of mode names that are to be activated by default, overriding the setting in the global configuration. -# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. -# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply. -# Otherwise, this overrides the setting from the global configuration (serena_config.yml). -# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply -# for this project. -# This setting can, in turn, be overridden by CLI parameters (--mode). -# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes -default_modes: - -# list of mode names to be activated additionally for this project, e.g. ["query-projects"] -# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. -# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes -added_modes: - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -# time budget (seconds) per tool call for the retrieval of additional symbol information -# such as docstrings or parameter information. -# This overrides the corresponding setting in the global configuration; see the documentation there. -# If null or missing, use the setting from the global configuration. -symbol_info_budget: - -# list of regex patterns which, when matched, mark a memory entry as read‑only. -# Extends the list from the global configuration, merging the two lists. -read_only_memory_patterns: [] - -# list of regex patterns for memories to completely ignore. -# Matching memories will not appear in list_memories or activate_project output -# and cannot be accessed via read_memory or write_memory. -# To access ignored memory files, use the read_file tool on the raw file path. -# Extends the list from the global configuration, merging the two lists. -# Example: ["_archive/.*", "_episodes/.*"] -ignored_memory_patterns: [] diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index ad0f1c3..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,107 +0,0 @@ -# AGENTS.md - -Guidance for coding agents (Claude Code, Codex, …) working in this repository. - -NORMA 3 (The Network Makeup Artist) visualises and analyses networks together with their annotated -groups. It is a **browser application** (Vite / TypeScript / Cytoscape.js, in `frontend/`) served by a -**dependency-free Python server** (`backend/`, standard library only) that adds the REST API and the -STRING / Arena3D / database relays. Everything computational (layouts, clustering, statistics, -exports) runs in the browser; the server never sees network data except for the token API. - -The app was migrated from R/Shiny (branch `master`) to this stack (branches `NORMA_v3`, -`update_architecture`). The architecture decisions live in `docs/superpowers/specs/`. - -## Rules for Agents - -- **Never push to remote.** Commit only when explicitly asked, one commit per feature. -- **Always use the `token-saviour` skill** — and the tools/skills it routes to — wherever it makes sense. -- **When a plan is active** (`docs/superpowers/plans/*.md`): one feature per commit, tick the checkboxes - as you go, and verify each feature at runtime before moving to the next. -- **Before every commit**: run lint, typecheck and the relevant test suite (commands below). Verify UI - changes at runtime with Playwright. -- **Keep the server dependency-free.** `backend/` must run with a plain `python3`; dev tools (pytest, - ruff, mypy) go in the `dev` group only. -- **Keep the single-file build working.** `frontend/dist/norma.html` must open from disk without a server. -- **Keep responses concise** — summarise rather than dumping full files. - -## Running the App - -**Backend** (no runtime dependencies; `uv` only for dev tools): -```bash -python3 backend/server.py # http://localhost:8000, serves frontend/dist -python3 backend/server.py --help # every option; also norma.config.json / NORMA_* env -cd backend && uv sync # dev tools into .venv -``` - -**Frontend:** -```bash -cd frontend -npm install -npm run dev # http://localhost:5173 — /api, /string-api, … proxied to :8000 -npm run build # frontend/dist/ (norma.html is self-contained) -``` - -**Docker:** `docker compose up -d` (two-stage image: node build → python:slim). - -**Tests:** -```bash -cd backend && uv run pytest # server unit/integration tests (fake upstreams, no network) -cd frontend && npm test # Vitest unit tests -cd frontend && npm run test:e2e # Playwright (starts backend/server.py itself) -``` - -**Lint / format / typecheck:** -```bash -cd backend && uv run ruff check . && uv run ruff format . && uv run mypy norma -cd frontend && npm run lint && npm run format && npx tsc --noEmit -``` - -`pre-commit install` wires the same checks into git. - -## Architecture Overview - -### Backend (`backend/norma/`) -`http.server`-based, stateless apart from the in-memory token store. -- `config.py` — `DEFAULTS`, `ENV_MAP`, `load_config` (defaults < JSON file < `NORMA_*` env < CLI), - `client_config` (the subset sent to the page as `/norma-config.js`). -- `api.py` — token store for `POST /api/external` / `GET /api/session/TOKEN` (TTL, size and count caps). -- `relays.py` — read-only relays to STRING, Arena3D and the database importers; strict upstream - allowlists, body limits, rate spacing. -- `handler.py` — `NormaHandler` routing, security headers, access log; `ThreadingServer`, `RedirectHandler`. -- `static.py` — static root (`frontend/dist`) and the blocklist for deployment files. -- `main.py` — argparse, TLS, browser opening, serve loop. `backend/server.py` is the entry point. - -### Frontend (`frontend/src/`) -The former 15K-line ` - - - - -
- -
- -
3.0The Network Makeup Artist
-
- -
- -
- -
- - - - - - -
- - - -
-
- -
- - -
- -
-
- - - - - -
- -
- -
-
-
- 0 nodes - 0 edges - 0 channels - 0 groups - -
-
- - - - -
-

Delete file

- -
-
- - -
-
-
- - -
-

Export image

-

Saves the current view as it looks, including group shading, shapes and labels.

-
- Format - - - - - -
-
- - - - - -

- -
- - -
- -
.png
-
- - - -
-
- - -
-
-
- - - -
- -
- - - - - -
Drag nodes · Shift-drag or Shift-click to select several · scroll to zoom · double-click to recenter
-
- - - - - 100% -
- - -
- -

-
- -
-
- -
-
-
- -
-
Source
-
Target
-
Direction
-
Edge ID
-
-
-
- -
-
-
-
- -
-

The Network Makeup Artist

-

See how annotated groups sit in a network: bubble sets, hulls and fog clouds, pie nodes and shapes, group-aware layouts, numeric values, a 3D view, communities, enrichment and network comparison.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Bubble-set groups -outlines that hug only the members - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -−2 -0 -2 -log2FC -Numeric values -color scales, sizes, legends, steps - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Ribosome - -Proteasome - -Spliceosome - -Glycolysis - -−log10 FDR -Communities and enrichment -Leiden, MCL, Walktrap; term tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -3D network -rotate, tilt and zoom freely - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -A - -B - -C -shared -edges -Compare networks -UpSet plots, Venn diagrams, statistics -
-
    -
  1. Load a networkupload files, fetch one from a database, or open an example
  2. -
  3. Add groups and valuesannotations, expression colors or numbers, communities
  4. -
  5. Arrange, analyse, sharelayouts, statistics, images, Arena3D, Cytoscape, Gephi
  6. -
-
- - - - -
- -

NORMA 3.0 runs in your browser: your files stay on this computer, except what you send to online databases or Arena3D yourself. Networks of up to 10,000 nodes.

-
-
-
- -
-
-
-

Network Profiler

-

Topology statistics for one or more networks, computed the way igraph computes them. Each network is treated as a simple undirected graph: self-loops and parallel edges are set aside first.

-
-
-
-
Networks to profile
-
-
-
- - - -
-
-
-
-
-

Group analysis (current view)

-

Statistics of the ticked groups of the current view, the network of groups, and an enrichment test, all on the shown part of the view (ticked groups and channels).

-
- - - -
-
-
-

Enrichment

-

Tests whether groups of an annotation (terms) are over-represented, with a one-sided hypergeometric test and Benjamini–Hochberg FDR per tested set.

-
- - - -
- - - - - - - - -
-
-
- - -
-
-
-
-
-

Layout benchmark

-

Runs every layout on the current view (its ticked groups and channels) without changing it, and scores how well each separates the groups: the silhouette width of the node positions (higher is better), the share of nodes inside another group's outline, and how much the outlines overlap (lower is better). Group layouts use the settings of the Layout section.

-
- - - -
-
-
-
-
-

Runtime table

-

Times the main steps on random networks of growing size (generated from fixed seeds; the table lists their edges), in this browser: reading the network file, building the network, the weighted layout, edge bundling, the profile statistics, Louvain and the group separation score. The view is not changed.

-
- - - -
-
-
-
-
-
- -
-
-
-

Network Comparison

-

Compare two to ten networks from your open views or from the network files: which nodes and edges they share, how similar they are, whether hubs stay hubs, and how their topology differs.

-
-
-
-
Networks to compare
-
-
-
- - - - -
- -
-
-
-
-
- -
-
-

NORMA API

-

Other applications can open NORMA with their own networks, groups and values, so that a pathway database, an analysis pipeline, a notebook or a web page can show its results in NORMA with one call. There are three ways in, all taking the same payload:

-
-
RESTPOST the payload to /api/external on a server running server.py; open the link it returns. Works from any language.
-
LinksA link to norma.html that points to the data (?data=, ?network=) or carries it (#json=). Needs no server.
-
postMessageA web page opens NORMA in a window or an iframe and sends it the payload directly. Needs no server.
-
- -

This copy of NORMA answers at …

-
- Python template script -

A ready-to-run client that checks a NORMA server, sends a network with groups, reads the stored payload back and builds a link that needs no server; it parses every answer and uses only Python's standard library. Adapt its build_payload() to your data.

-

python3 norma_api_client.py --server http://localhost:8000/ --open

-
- -

The payload

-

A JSON object. Give the network in one of three forms, and add groups, values and settings as needed:

- - - - - - - - - - - - - - - - -
FieldTypeMeaning
nametextThe view's name (and the prefix of its files in Files).
edgeslistThe network as {"source", "target"} objects, each optionally with type (the channel), weight (a number) and directed (true/false).
nodeslistOptional, with edges: {"id", …} objects whose other fields become node attributes (shown in node details and searchable).
filesobjectThe network as NORMA files instead: network (text, or a list for several networks), annotation (text or list) and expression (text), in the tab-separated formats. A list item can be {"name", "text"}.
networkobjectOr a complete NORMA view (nodes, edges, groupOrder, …) as saved by Save view file; see JSON format.
groupsobject or list{"Group": ["node", …]}, or [{"name", "members", "color", "description"}]. Nodes may be in several groups.
annotationslistMore groupings, each {"name", "groups"}; the view's Grouping list switches between them.
expressionobject{"node": "#e11d48"} (colors), {"node": 1.7} (one numeric value) or {"node": {"t0": 0.2, "t1": 1.4}} (several columns, e.g. time points).
directedtrue/falseRead files.network as directed.
settingsobjectDisplay settings by their names in a settings file (for example edgeDirection, edgeCurveStyle, showGroupHulls, hullStyle, nodeFillSelect, legendShow), plus theme and layout (fr, kk, stress, cose, circle, …).
tabtext"3d" opens the 3D page instead of the 2D page.
format / views—A whole session file ("format": "norma3-session") can be sent as the payload too.
-

With edges or files, NORMA adds the network, groupings and values to Files like uploaded files and opens them in a new view, so everything in NORMA works on them. Networks larger than 10,000 nodes are cut to their first 10,000. The example in Try it shows all common fields.

- -

REST

-

When NORMA runs with server.py (see Running NORMA locally or on a server), the server offers:

- - - - - - - - -
Web services (all answer in JSON)
RequestAnswer
POST /api/external with the payload as JSON (Content-Type: application/json){"token", "url", "expiresInHours"}. Open url in a browser: NORMA starts with the payload loaded.
GET /api/session/TOKENThe stored payload (NORMA fetches it when opened with ?session=TOKEN); 404 once it has expired.
GET /api/health{"status": "ok", "api": "1.0"}, to check that a server offers the API.
-

The token is made by the server; you never build the link yourself. Payloads are kept in the server's memory for 24 hours (set NORMA_API_TTL_HOURS), up to 50 MB each; the API accepts calls from any web page (CORS).

-

curl

-
curl -X POST http://localhost:8000/api/external \
-  -H "Content-Type: application/json" \
-  -d '{"name": "My network",
-       "edges": [{"source": "A", "target": "B"}, {"source": "B", "target": "C"}],
-       "groups": {"Module 1": ["A", "B"], "Module 2": ["C"]}}'
-
-# answer:
-# {"token": "k3J9…", "url": "http://localhost:8000/norma.html?session=k3J9…",
-#  "expiresInHours": 24}
-

Python

-
import requests, webbrowser
-
-payload = {
-    "name": "My network",
-    "edges": [{"source": s, "target": t} for s, t in [("A", "B"), ("B", "C"), ("C", "A")]],
-    "groups": {"Module 1": ["A", "B"], "Module 2": ["C"]},
-    "expression": {"A": 1.5, "B": -0.4, "C": 2.1},
-    "settings": {"layout": "fr", "showGroupHulls": True},
-}
-r = requests.post("http://localhost:8000/api/external", json=payload, timeout=30)
-r.raise_for_status()
-webbrowser.open(r.json()["url"])      # opens NORMA with the network
-

R

-
library(httr)
-payload <- list(
-  name   = "My network",
-  edges  = data.frame(source = c("A", "B"), target = c("B", "C")),
-  groups = list(`Module 1` = c("A", "B"), `Module 2` = list("C"))
-)
-r <- POST("http://localhost:8000/api/external", body = payload, encode = "json")
-browseURL(content(r)$url)
-

JavaScript

-
const r = await fetch("http://localhost:8000/api/external", {
-  method: "POST",
-  headers: { "Content-Type": "application/json" },
-  body: JSON.stringify({ name: "My network", edges, groups }),
-});
-const { url } = await r.json();
-window.open(url, "_blank");
- - -

Links need no server: any copy of norma.html, on a web server or opened from disk, reads them when it starts.

-
http://localhost:8000/norma.html?data=https://example.org/my-payload.json
-http://localhost:8000/norma.html?network=https://example.org/net.txt&annotation=https://example.org/groups.txt&expression=https://example.org/values.txt&layout=fr
-http://localhost:8000/norma.html#json=eyJuYW1lIjoiTXkgbmV0d29yayIsImVkZ2VzIjpb…
-
    -
  • ?data=URL fetches a payload file. ?network=, ?annotation= and ?expression= fetch NORMA files; add &name=, &layout=, &theme= or &tab=3d if you like. The site holding the files must allow cross-site downloads (CORS), as GitHub raw files, Zenodo and most data portals do.
  • -
  • #json= carries the payload itself, as base64url-encoded JSON. It suits small networks (links longer than a few thousand characters may be cut by chat and mail programs); the part after # is never sent to any server. Make a link below builds one.
  • -
-

A button on your own site:

-
<a href="http://localhost:8000/norma.html?data=https://example.org/pathway-42.json"
-   target="_blank">Open in NORMA</a>
- -

postMessage

-

A web page can open NORMA and hand it data directly, with no server and no size limit other than the browser's. NORMA sends {"type": "norma:ready"} to the page that opened or embedded it when it has started (and answers {"type": "norma:ping"} the same way). Send {"type": "norma:load", "payload": …, "requestId": …}; NORMA answers {"type": "norma:loaded", "ok": true, "summary": …} or {"ok": false, "error": …} with the same requestId. Each load opens a new view, so a page can send several networks.

-

In a new window

-
// open NORMA (any copy: a file, a web server or server.py)
-const norma = window.open("http://localhost:8000/norma.html", "_blank");
-
-window.addEventListener("message", (e) => {
-  if (e.source !== norma) return;
-  if (e.data.type === "norma:ready") {           // NORMA has started
-    norma.postMessage({
-      type: "norma:load",
-      requestId: 1,
-      payload: { name: "My network", edges, groups, expression },
-    }, "*");
-  }
-  if (e.data.type === "norma:loaded") {          // the answer
-    console.log(e.data.ok ? e.data.summary : e.data.error);
-  }
-});
-

In an iframe

-
<iframe id="norma" src="norma.html" style="width:100%;height:700px;border:0"></iframe>
-<script>
-  const frame = document.getElementById("norma");
-  window.addEventListener("message", (e) => {
-    if (e.data.type === "norma:ready")
-      frame.contentWindow.postMessage({ type: "norma:load", payload }, "*");
-  });
-</script>
- -

Try it

-

Edit the payload and choose what to do with it.

- -
- - - - -
-
- - -
-
- -

Limits and privacy

-
    -
  • Payloads up to 50 MB; networks up to 10,000 nodes are shown in full.
  • -
  • Data sent with postMessage or #json links stays in the browser. With REST, the payload is kept only in the memory of the server you sent it to, for the time set there, and anyone with the link can open it until then; don't use it for confidential data on a shared server.
  • -
  • NORMA only displays what it receives: payloads cannot run code, and NORMA sends nothing back except the norma:loaded summary to the page that asked.
  • -
-
-
- -
-
- -
-
- How to cite NORMA -

If NORMA helps your work, please cite:

-
    -
  • Karatzas E, Koutrouli M, Baltoumas FA, Papanikolopoulou K, Bouyioukos C, Pavlopoulos GA. The network makeup artist (NORMA-2.0): distinguishing annotated groups in a network using innovative layout strategies. Bioinformatics Advances. 2022;2(1):vbac036. doi:10.1093/bioadv/vbac036
  • -
  • Koutrouli M, Karatzas E, Papanikolopoulou K, Pavlopoulos GA. NORMA: The Network Makeup Artist, a web tool for network annotation visualization. Genomics, Proteomics & Bioinformatics. 2022;20(3):578–586. doi:10.1016/j.gpb.2021.02.005
  • -
-

Please also cite the databases and methods you use; see Resources and methods.

-
-

This is NORMA's technical help: how to load data, use each control and page, save your work and run NORMA on a server. For an overview of NORMA, its data sources, what's new and how to cite it, see About.

- -
Getting started
-

Getting started

-

NORMA 3.0 shows networks whose nodes belong to groups (annotations) and whose node pairs can be linked by several kinds of edge (channels); unlike earlier versions of NORMA, it handles these multi-edge networks. The application is a single file, norma.html, that opens in a browser. It starts on the Welcome tab, which shows what NORMA does and offers quick ways to open an example, fetch a network from a database or upload files; the tab stays there, so you can always go back to it. Opening a network moves to 2D Network. It reads the same tab-delimited files as NORMA, plus a richer JSON format.

-

The screen has eight pages. Welcome is the start page, 2D Network is the interactive view, 3D Network shows the same view in three dimensions, Network Profiler computes topology statistics and group analyses, Network Comparison sets up to ten networks side by side, API explains how other programs can open NORMA with their data, Help is this page, and About describes NORMA, its data sources and how to cite it. The NORMA logo at the top left always leads back to Welcome. The ? next to a sidebar heading opens the matching section here. Finding your way around explains the rest of the screen.

-
    -
  1. Upload your files or open an example in the sidebar's Upload Data tab, or fetch a network from a database (STRING, Reactome, OmniPath, NDEx, IntAct or GO-CAM) in the Database importers tab.
  2. -
  3. Tick networks and choose an annotation and an expression file, then choose Open in new view.
  4. -
  5. Style that view in the Display tab: layout, groups, colors, sizes, labels and edges.
  6. -
  7. Repeat with other combinations. Each becomes its own view; switch between them with the View list in the top bar.
  8. -
  9. Save a picture with Export image (camera button, top bar), or the data in other formats with the Export tab.
  10. -
- -

Finding your way around

-
    -
  • Top bar (purple): the View list, which picks the visualization you are working on, with Duplicate, Rename and Delete…, and the Grouping list (amber), which switches the current view to any annotation in Files while keeping node positions. For views opened from a built-in demo or a JSON file, the groups come with the data and the list only shows how many there are. On the right are Export image (the camera button), undo and redo and the theme.
  • -
  • Page tabs: Welcome (the start page, always available), 2D Network, 3D Network, Network Profiler, Network Comparison and Help. In the row below them, three colored labels say what the current view shows: the Network (blue), the Grouping with its number of groups (amber) and where node Colors come from (pink). Hover a label for its full text. At the right end of that row are the numbers of nodes, edges, channels and groups, with a directed or mixed direction tag when edges have a direction; when some groups are unticked, the group count reads, for example, 8/9. The labels and counts stay visible on every page, so the Network Profiler and Network Comparison pages always tell you which view "Current view" means.
  • -
  • Sidebar tabs: Upload Data (blue) holds examples, your files and opening saved work; Database importers (red) fetches networks and groupings from STRING, Reactome, OmniPath, NDEx, IntAct and the Gene Ontology; Export (purple) saves pictures, files for NORMA and other tools, Arena3D networks and sessions; Display (green) holds everything about how the current view looks. The Examples list only chooses what Open example opens next; it doesn't show what is on screen. The View list and the colored labels do.
  • -
  • Opening and closing sections: click a section's title (or focus it and press Enter or Space) to fold it away or open it again; the ? in a title opens its help instead. All sections start folded, so each tab shows a short list of titles; open the ones you need. Inside a section, small colored headings with a line (for example Direction and arrows or Session file (.json)) separate groups of related options.
  • -
  • Section order and colors: every sidebar section has a color of its own, and neighbouring sections always have clearly different ones. Upload Data: Examples (green), Files (blue) and Open saved work (purple). Database importers: STRING (red), Reactome (blue), OmniPath (teal), NDEx (orange), IntAct (green) and Gene Ontology (purple); click an importer's title to open or close it. Export: NORMA files (blue), Image (pink), Other tools (orange), Save your work (slate) and Arena3D (teal). The Display tab starts with the groups and how they are drawn and placed, then the look of nodes and edges, then extras: Node groups (amber), Group highlighting (magenta), Layout (green; 3D layout, teal, on the 3D page), Colors (pink), Nodes (sky blue), Labels (orange), Edges (slate), Edge channels (lime), Legend (crimson), Attributes (cyan) and Performance (brown). The labels below the page tabs use the matching colors: Network blue like Files, Grouping amber like Node groups, and Colors pink like Colors.
  • -
- -

Views

-

A view is one visualization: a set of networks with an annotation and an expression file, or an example or JSON file, together with how it looks. Each view keeps its own display settings, node positions, zoom, ticked groups and channels, group colors and shapes, channel colors, edge style including bundling, and its 3D positions and camera, so you can prepare several pictures from the same uploads and move between them without losing work. The theme and the search options apply to every view.

-
    -
  • Duplicate copies the current view, for example to compare two annotations or two layouts of the same network.
  • -
  • Rename names the current view; press Enter to keep the name or Escape to cancel.
  • -
  • Grouping (top bar) regroups the current view by another annotation, or by none, and keeps node positions; the same as ticking an annotation in Files and choosing Show in this view.
  • -
-

In the Upload Data tab, Show in this view replaces what the current view shows with the ticked files and keeps its display settings; if only the annotation or expression file changed, node positions are kept. Open in new view shows the ticked files in a new view and leaves the current one as it was. Open example always opens a new view, reusing an empty untitled one if there is one.

- -

Undo and redo

-

The arrows in the top bar undo and redo changes to the current view: moved nodes, layouts, spread, display settings, ticked groups and channels, group and channel colors, and switching or clearing what the view shows. Keyboard: Ctrl+Z (⌘Z on a Mac) undoes, Ctrl+Shift+Z or Ctrl+Y redoes. A quick series of changes, such as dragging a slider or a layout's animation, counts as one step. Each view has its own history of up to 60 steps, kept while the page is open. Zooming, panning, searching, selecting and the theme are not part of the history.

- -
Data
-

Examples

-

The first block of the list holds NORMA's example datasets. The TP53 documentation example also brings a small file of simulated log2 fold changes and adjusted p-values, for trying the numeric color scale; tick it under Expressions. Opening one adds its network, annotations and expression file to Files, selects them and shows the result in a new view. Their other annotations stay in the list so you can switch between them.

- - - - - - - - - - - - - - -
ExampleFilesSource
STRING: TP53 interactorsNetwork (weighted), groups, expressionSTRING
STRING: BCAR3 interactorsNetwork; GO biological process, GO molecular function, KEGGSTRING
Drosophila Tau networkNetwork; KEGG, Louvain communities; expressionPMID 31488613, doi:10.1523/JNEUROSCI.0391-19.2019
Human gene co-expressionNetwork; GO biological process, molecular function, cellular component; KEGG; MCODE cluster colorsPMID 19081792, doi:10.1371/journal.pone.0003911, bioinfow.dep.usal.es/coexpression
COVID-19Network; InterPro, SMART, GO (three), KEGGIntAct
Gallus gallusNetwork; KEGGBioGRID
Signalling cascade (showcase)Directed multi-edge network with five channels; four cascade levels; simulated log2FC and adjusted p-values. Opens with arrows, curved parallel edges, the numeric color scale and the legend.Simulated for NORMA 3.0
Healthy vs disease (showcase)Two networks over the same 64 nodes (the disease network loses about a fifth of the edges and gains links in one module); four modules; simulated log2FC. Opens with both networks overlaid as two channels; compare them on the Network Comparison page.Simulated for NORMA 3.0
Arena3D example (showcase)The seven-layer example network from Arena3D's documentation; its layers become groups. Try Open in Arena3D to send it back.Arena3D
TP53, NORMA documentationThe small example from NORMA's help pagesNORMA
-

The second block holds built-in demos in JSON form: a curated multi-edge operon, two synthetic networks of four overlapping modules with attributes on nodes, edges and groups, and random networks from 20 to 5,000 nodes. The 40-node random directed multi-edge network opens with arrows, curved parallel edges and a hierarchical layout: its four groups form a signalling cascade from receptors to target genes, pairs carry one to three channels, and about a quarter of them also have a feedback edge in the opposite direction. The 20-node random network opens with edge labels: each edge carries an interaction attribute (activates, inhibits, binds, …) shown at its middle. All demos are generated from fixed seeds and can be downloaded under Example files. Clear view empties the current view but keeps your files.

- -

Sample data and sample output

-

Every example opens with one click, from Open an example on the Welcome page or the Examples list in the Upload Data tab. Its files (the sample data) can be downloaded under Example files, to see the formats or to try uploading them. The links below open a sample result directly; they behave exactly like results from your own data:

- - - - - - - - - - -
Sample outputShows
Drosophila Tau networkA published network with KEGG pathways, Louvain communities and expression colors; try the Network Profiler and Group analysis on it.
STRING: TP53 interactorsA STRING network with evidence channels, groups and expression.
Signalling cascadeDirected, multi-channel edges with numeric values, a color scale and a legend.
Healthy vs diseaseTwo networks to compare on the Network Comparison page.
Arena3D exampleSeven layers, ready to open in Arena3D.
Four overlapping modules in 3DPie-chart nodes and group layouts on the 3D page.
-

Each link can also be bookmarked or shared: norma.html?example=NAME (add &tab=3d for the 3D page).

- -

Example files

-

Sample data: every file behind the examples, so you can see the file formats and try NORMA's upload with them. Click a name to download the file; the same examples can be opened directly from the Examples list or with the sample-output links in Sample data and sample output.

-
-

Built-in demos

-

The demos are generated from fixed random seeds, so each download matches what Open example shows. Network and Groups are NORMA files: the network has a Weight column when any edge is weighted (edges without one are written with weight 1) and a Type column when there are several channels. JSON keeps everything, including attributes, group descriptions and, for the labelled demo, the settings that turn edge labels on.

-
- -

Files and formats

-

Upload one or many files at once with Upload files…, or drop them on the upload box. Detect file type recognizes networks by their header and expression files by their colors; anything else is read as an annotation. Pick a type yourself if detection guesses wrong. Networks with more than 10,000 nodes are cut to their first 10,000 nodes, with a note in the list. Arena3D network files (columns SourceNode, SourceLayer, TargetNode, TargetLayer, Weight, Channel) are recognized too: each becomes a network and an annotation with one group per layer. The list below it chooses whether network rows without a Direction value are read as undirected or directed. The name box names a single upload; with several files, each keeps its file name. Every file appears in one of three lists (Networks, Annotations, Expressions), in A–Z order, and stays there until you delete it (see Deleting files). Files live only in this browser tab.

-

Deleting files

-

There are four ways to remove things completely:

-
    -
  • Delete… in the top bar (next to Rename) lists the current view and every file it shows (its network files, grouping and expression file), each with a tick box; untick what you want to keep. Removing the view closes it (the previous view is shown; removing the only view leaves an empty one). For a view opened from an example or a JSON file, only the view itself can be removed, since its data belongs to the view.
  • -
  • The ✕ next to the Grouping menu removes the grouping file the view uses.
  • -
  • The ✕ next to a network, annotation (grouping) or expression file in Upload Data → Files removes that file.
  • -
  • Delete all files… under the Files lists removes every file at once.
  • -
-

Each asks Are you sure you want to completely remove …? and names the views that will change; nothing happens until you choose Yes, remove. Deleting a file:

-
    -
  • removes the file from Files, from the Grouping menu and from every other file list and menu, together with everything NORMA read from it (for STRING and database imports, also the extra node details that came with the network);
  • -
  • updates every view that shows it, including views you are not looking at: a view keeps its remaining networks, node positions, group colors and ticked groups and channels, and loses the deleted network's nodes and edges, or the deleted groups or colors; a view left with no network becomes empty (its display settings stay);
  • -
  • clears the undo history of those views, so the deleted data can't come back, and removes Network Profiler and Network Comparison results that were computed from it;
  • -
  • leaves out the deleted data from sessions saved afterwards;
  • -
  • renames views that were named after their files automatically (views you renamed keep their names).
  • -
-

Groupings that were fetched for a deleted network (for example from STRING) are not deleted with it; delete them separately if you don't need them. Deleting never touches the files on your disk, and cannot be undone inside NORMA: to use a file again, upload it again. Views opened from examples or JSON files hold their own data; empty them with Clear view.

-

Network file

-

Tab-separated, with a header row Source, Target and optionally Weight. Connections are undirected: A–B and B–A are the same connection, so repeats and self-loops are removed and reported.

-
Source	Target	Weight
-CDKN1A	TP53	5
-TP53	MDM2	1
-

An optional Type column (also accepted as Channel, Layer or Interaction) keeps several edges between the same pair, one per type, and makes each type a channel. Only a repeat of the same pair and type is removed. This column is new in NORMA 3.0: earlier versions of NORMA read only Source, Target and Weight and kept a single connection per pair.

-
Source	Target	Weight	Type
-TP53	MDM2	0.9	experiments
-TP53	MDM2	0.7	textmining
-

An optional Direction column (also accepted as Directed, Dir or Arrow) marks each row as directed (also yes, true, 1 or ->) or undirected (also no, false, 0 or -). A directed row points from Source to Target, so A→B and B→A are two connections; for undirected rows they are one. Rows with an empty or unknown value follow the direction chosen when uploading. This column is new in NORMA 3.0; see Directed and undirected networks.

-
Source	Target	Type	Direction
-EGFR	GRB2	binding	undirected
-MAPK1	ELK1	phosphorylation	directed
-ELK1	FOS	expression	directed
-

Annotation file

-

No header. Each line is a group name, a tab, then the group's nodes separated by commas without spaces. Group names may contain spaces and commas; node names may not contain commas. A node can be in any number of groups.

-
Group-2	BCL2L1,MDM4,MDM2,CHEK2
-Group-5	TP53,EP300
-

Expression file

-

No header. Each line is a node name, a tab, then a color: a name such as red or a hex code such as #ff0000. Nodes without a color are gray.

-
CDKN1A	blue
-MDM4	#00ff00
-

Numeric values can be used instead of colors (new in NORMA 3.0): each line is a node name, a tab, then one or more numbers, such as a log2 fold change and an adjusted p-value. An optional first line names the columns; without it they are called value, value 2 and so on. NA, empty cells and similar entries count as missing. Decimal commas are accepted. Detect file type recognizes such files, and the list shows how many value columns they have.

-
Gene	log2FC	padj
-CDKN1A	2.4	0.000001
-BCL2L1	-1.8	0.0003
-TP53	NA	NA
-

Choosing what to show

-

Tick one or more networks, choose one annotation and one expression file (or None), then choose Show in this view or Open in new view. A note under the buttons tells you when the selection differs from what the view shows. If only the annotation or expression changed, Show in this view updates in place and keeps node positions, so you can flip between GO, KEGG and community groupings of the same layout, or open each grouping in its own view to keep them side by side.

-

With an expression file selected, nodes are filled with its colors and groups are shaded behind them, as in NORMA. Without one, nodes are filled with their groups' colors, split into a pie for nodes in several groups.

-

Annotation names not in the network

-

Node names in an annotation must also appear in the network. The check happens twice:

-
    -
  • When you upload an annotation, its names are compared with the networks uploaded together with it, or, if there are none, with the ticked networks, or else with every network in the list. Names found in none of them are discarded right away, groups left empty are removed, and a warning lists what was dropped. Download corrected annotation saves the cleaned file, which is what NORMA's companion R script produces. If no name matches at all, the file is not kept. If no network has been uploaded yet, the check waits until the annotation is shown.
  • -
  • When you show files, annotation and expression entries for nodes that aren't in the selected networks are left out and listed in the status notes, again with a corrected annotation to download.
  • -
- -

Several networks and multi-edges

-

New in NORMA 3.0. Previous versions of NORMA did not support multi-edge graphs: every pair of nodes could have only one connection, and repeated connections were removed when a network was loaded. NORMA 3.0 keeps all of them, as parallel edges in separate channels.

-

Parallel edges come from three sources: a Type column in a network file, several network files shown together, or JSON files with more than one edge per pair.

-

Ticking more than one network overlays them. Nodes with the same name are merged, and each network becomes its own edge channel, so a connection found in two networks is drawn as two parallel edges in two colors. Edges and nodes then carry a network / networks attribute naming their source, which you can search for.

-

Ways to work with parallel edges:

-
    -
  • Edge channels turns each channel on or off and recolors it.
  • -
  • Edges → Parallel edges: merge per node pair draws one edge per pair, thicker the more channels it carries. It's gray when channels disagree and in the channel's color when there's only one. Clicking a merged edge lists all its channels.
  • -
  • Edges → Shape: curved fans parallel edges apart so each stays visible.
  • -
  • Layout → Use checked channels only positions nodes using only the channels you've ticked, so you can lay the graph out by one network and compare the others on top.
  • -
  • Edges → Shape: bundled draws edges running in similar directions together, and Labels → Edge labels can name each edge's channel.
  • -
  • Network Profiler reports, for the current view, how many node pairs are shared between channels and how similar each pair of channels is; Network Comparison sets whole networks side by side.
  • -
  • Export → NORMA files → Keep channels as a Type column saves parallel edges in a NORMA network file instead of merging them.
  • -
- -

Directed and undirected networks

-

New in NORMA 3.0. Earlier versions of NORMA treated every connection as undirected. Edges can now point from a source to a target, and a network can mix directed and undirected edges.

-

Bringing direction in

-
    -
  • When uploading, the list under the file type sets how network rows are read: undirected (A–B), the default, or directed (Source → Target). Choose directed before uploading if A→B and B→A are different interactions: an undirected upload treats them as the same connection and keeps only one.
  • -
  • A Direction column in the network file overrides that choice row by row (see Network file), so one file can mix both kinds.
  • -
  • In JSON, "directed": true at the top makes every edge directed, and "directed": true or false on an edge sets that edge (see JSON format).
  • -
  • The built-in Random directed multi-edge network shows a signalling cascade with up to three channels per pair and some feedback edges.
  • -
-

Handling a network as directed or undirected

-

Display → Edges → Direction decides, for the current view:

-
    -
  • Treat all edges as undirected (default): direction is ignored everywhere, as in earlier versions of NORMA. When the data does mark edges as directed, the top bar shows a direction ignored tag; click it to switch to As in the data.
  • -
  • As in the data: edges marked as directed get arrows and are treated as directed; the rest are undirected.
  • -
  • Treat all edges as directed: every edge points from its source to its target, whatever the file says.
  • -
-

With arrows shown, Arrow chooses a triangle, vee, curved triangle or chevron, and Arrow size scales them. The top bar shows a directed or mixed direction tag next to the counts.

-

What direction changes

-
    -
  • Parallel edges: A→B and B→A are separate edges. With Shape: curved they are drawn apart; merge per node pair merges only edges running the same way.
  • -
  • Node size and details: In-degree and Out-degree count incoming and outgoing edges (an undirected edge counts as both). Betweenness and closeness follow edge directions when the shown part has directed edges; clustering ignores direction. A node's details show in- and out-degree and mark each neighbor with → (outgoing) or ← (incoming); an edge's details show its direction.
  • -
  • Layout: Hierarchical places sources above their targets. Force-directed layouts and the group strategies ignore direction.
  • -
  • Network Profiler and Network Comparison: with Use edge direction ticked, they add directed statistics and match edges by direction (see below).
  • -
  • Exports: NORMA network files get a Direction column when any edge is directed, and JSON keeps each edge's direction. Earlier versions of NORMA read neither.
  • -
-

Search, group shading, shapes, bundling and edge labels work the same for directed and undirected edges.

- -

Database importers

-

The Database importers tab fetches networks and groupings from public databases; no account is needed. Every import is added to Files (a network plus one annotation per grouping) and opens in a new view, where the Grouping list switches between its groupings. Nodes keep useful identifiers as attributes, in particular uniprot, which the Gene Ontology importer uses. Networks with directed edges open with arrows. Very large results are cut to 10,000 nodes, and every importer has a Cancel button. When NORMA runs with server.py, all requests go through the server (see Running NORMA locally or on a server); otherwise the browser calls the services directly, which some browsers block. STRING is described in Importing from STRING.

-

Reactome

-

Curated pathways from Reactome. Type a pathway name (choose the species) or a stable identifier such as R-HSA-69278, choose Search pathways, pick one, and choose Fetch pathway network. NORMA reads the pathway's reactions (up to Most reactions) and their participating molecules: nodes are the proteins (and, if ticked, small molecules), and two nodes are linked when they take part in the same reaction, weighted by how many reactions they share. Reactions with more than the chosen number of molecules are left out, since they would link everything to everything. Groupings: the pathway's sub-pathways (the proteins of the reactions each contains) and, optionally, the 40 largest reactions.

-

OmniPath

-

Signalling, transcriptional and ligand–receptor interactions from OmniPath for human, mouse or rat. Type gene symbols or UniProt accessions and tick the datasets (OmniPath core, pathway, kinase–substrate and ligand–receptor extras, CollecTRI and DoRothEA). NORMA fetches their interactions with all partners (keeping the Most partners best supported by curation effort) or only those among the given proteins. Edges keep OmniPath's direction; Channels split them by effect (stimulation, inhibition, both, unsigned) or by interaction type, and their weight is the curation effort. Groupings: protein complexes with at least two members in the network, intercellular roles (ligand, receptor, …) and, optionally, an annotation resource such as SignaLink pathways or NetPath.

-

NDEx

-

Public networks from the Network Data Exchange. Search with words or paste a network UUID, pick a network (the list shows its size and owner) and choose Fetch network. Node names, edge interactions and numeric weights are read from the network, its saved layout is kept, and node attributes become node attributes in NORMA. Groups depend on the network ("limited"): every node attribute with 2 to 60 distinct values covering at least 30% of the nodes (such as a type or module) becomes a grouping, up to six.

-

IntAct

-

Experimentally detected molecular interactions from IntAct, through its PSICQUIC service. Type gene names or UniProt accessions and choose the organism (both partners must belong to it). Interaction records below the Lowest MI-score are left out; the others are merged per pair and interaction type (physical association, direct interaction, …), which become channels, with the best MI-score as the weight. Most partners keeps the highest-scoring partners, and Most records limits how much is read. IntAct has no groupings of its own, so NORMA adds one that separates the query proteins from their partners; add GO terms or communities for more.

-

Gene Ontology

-
    -
  • GO-CAM network (limited): GO-CAM models from the Gene Ontology link the activities of gene products causally. Choose the organism, List models, filter and pick one (or type a model ID), and Fetch GO-CAM network. Nodes are the gene products that enable the activities; directed edges follow the causal relations (directly positively regulates, provides input for, …), which become channels. Groupings: the biological process, cellular component and molecular function of each activity.
  • -
  • GO groupings add GO terms as groups to the network already shown, from QuickGO annotations of the nodes' UniProt accessions (the node name or its uniprot attribute). Tick the aspects; Leave out electronic annotations skips IEA evidence, NOT annotations are always skipped, and each aspect keeps up to Groups per aspect terms with at least Smallest group members (terms that contain every node are left out). The groupings are added to Files and shown in the current view.
  • -
- -

Importing from STRING

-

New in NORMA 3.0. STRING, in the Database importers tab, fetches a protein interaction network, and functional groupings for it, directly from the STRING database. It needs an internet connection.

-

The network

-
    -
  • Proteins or genes: one or more names, such as TP53, p53 or a UniProt accession, separated by spaces, commas or new lines. STRING matches each name to its best protein and the status notes say how each was matched and which names weren't found.
  • -
  • Organism: pick one from the list, or choose Other organism and type its NCBI taxon ID (for example 7955 for zebrafish). Any organism in STRING can be used.
  • -
  • Network: functional associations use all of STRING's evidence; physical interactions only keeps proteins that bind each other or form a complex.
  • -
  • Interactors to add (0–500) extends the network with the proteins most confidently connected to yours, best first. With 0 and several names, only the connections among them are fetched; with a single name STRING always adds its 10 best interactors.
  • -
  • Minimum confidence: highest (0.900), high (0.700), medium (0.400), low (0.150) or a custom value; weaker connections are left out.
  • -
  • Evidence channels: gene neighborhood, gene fusion, co-occurrence, co-expression, experiments, curated databases and text mining. Untick channels to leave that evidence out: each connection's confidence is then recomputed from the ticked channels only, the way STRING combines them, and connections that fall below the minimum are dropped (the notes say how many).
  • -
  • Edges: one edge per channel makes a multi-edge network in which each channel has STRING's color and its own score as the weight, so you can tick channels on and off, merge or bundle them later. One edge per pair keeps a single edge with the combined score.
  • -
-

Each protein carries its STRING identifier, STRING's description of it, and whether it was one of your query proteins, as attributes: they appear in the node's details and can be searched.

-

Groupings from functional terms

-
    -
  • Terms: enriched in the network (recommended) runs STRING's enrichment analysis on the network's proteins and keeps terms with an FDR at or below the chosen value; all annotations of its proteins takes every term any of them has. STRING doesn't share full KEGG annotations for licensing reasons, so KEGG pathways come only with enriched terms.
  • -
  • Term collections: GO biological process, molecular function and cellular component, KEGG, Reactome and WikiPathways pathways, UniProt keywords, Pfam, InterPro and SMART domains, diseases, tissues, subcellular localization, phenotype ontologies (human, mammalian, fly, worm, zebrafish, fission yeast), STRING's local network clusters and reference publications. Which ones have terms depends on the organism and the STRING version; the notes list what STRING returned. Any other collection STRING offers includes collections not in the list.
  • -
  • Groups per collection keeps the most significant (enrichment) or largest (all annotations) terms, and Smallest group leaves out terms with fewer proteins in the network.
  • -
-

Each collection becomes an annotation in Files, named after the network and the collection, and the view opens grouped by the first available of KEGG, Reactome, GO biological process and so on. Switch collections with the Grouping list in the top bar; node positions stay. Every group carries the term's identifier and collection and, for enrichment, its FDR, p-value and number of proteins in the genome, shown in the group's details (its i button). Fetch groupings for the current view adds more collections, or the same ones with other settings, to a STRING network that is already shown.

-

STRING server

-

By default NORMA asks https://string-db.org, which always answers with the latest STRING version; the notes say which version was used. To get the same answers every time, enter a version address such as https://version-12-0.string-db.org. NORMA identifies itself to STRING and waits a second between requests, as STRING asks, so a full import takes a few seconds. Cancel stops a request that takes too long. Untick Fetch protein descriptions to save one request.

-

Networks and groupings fetched from STRING can be saved as NORMA files or JSON like any other; please cite STRING (Szklarczyk et al., Nucleic Acids Research 2023;51(D1):D638–D646, doi:10.1093/nar/gkac1000) when you use them.

- -

JSON format

-

The only required fields are nodes and edges:

-
{
-  "nodes": [
-    { "id": "A", "groups": ["g1", "g2"], "size": 40, "color": "#e07a7a", "tissue": "liver" },
-    { "id": "B", "group": "g1" }
-  ],
-  "edges": [
-    { "source": "A", "target": "B", "type": "experiments", "weight": 0.8, "pmid": 123 },
-    { "source": "B", "target": "A", "type": "regulation", "directed": true }
-  ],
-  "directed": false,
-  "groupAttrs": { "g1": { "label": "Group one", "description": "…", "curated": true } },
-  "nodeColors": { "g1": "#e8a15f" },
-  "edgeColors": { "experiments": "#b464c9" },
-  "groupShapes": { "g1": "diamond" },
-  "config": { "showEdgeLabels": true, "edgeLabelContent": "attr:pmid" }
-}
-
    -
  • Nodes: id, group or groups, size and color have fixed meanings; any other field is kept as an attribute.
  • -
  • Edges: id, source, target, type (the channel) and weight have fixed meanings; any other field is kept as an attribute. Any number of edges may join the same pair, for example one per channel. directed (true or false) sets an edge's direction.
  • -
  • Direction: a top-level "directed": true makes every edge without its own directed field directed.
  • -
  • Numeric values: "values": { "log2FC": 1.2, "padj": 0.003 } on a node gives it numeric values for the color scale and node size.
  • -
  • Groups: in groupAttrs, label, description and color have fixed meanings; anything else is a group attribute. Attributes can also be nested under an attrs object.
  • -
  • Colors: nodeColors and edgeColors fix group and channel colors.
  • -
  • Shapes: groupShapes gives a group a node shape. Names are Cytoscape's: rectangle, round-rectangle, diamond, triangle, vee, pentagon, hexagon, octagon, star, rhomboid, tag, concave-hexagon, barrel; groups not listed are circles.
  • -
  • Group order: an optional groupOrder list sets the order of groups in the legend and the order palette colors are handed out.
  • -
  • Settings: an optional config object holds display settings in the same shape as a settings file. For example, "edgeLabelContent": "attr:pmid" labels edges with their pmid attribute.
  • -
- -
Viewing and styling
-

Exploring the network

-
    -
  • Selecting several nodes: Shift-click (or Ctrl/⌘-click) adds or removes a node, Shift-drag on the background draws a selection box, Ctrl+A selects every visible node and Escape clears the selection. Selected nodes get a thick yellow border and halo, which disappears as soon as they are deselected; a bar at the top right shows how many are selected with Zoom to selection and Clear, and dragging any selected node moves them all.
  • -
  • Drag nodes to move them, scroll to zoom, and drag the background to pan. Zoom has no practical limit in either direction, and the view always keeps the network in frame: a network smaller than the canvas can't be pushed off it, and a larger one can't be panned past its edges. The +, – and Fit buttons at the bottom left zoom around the center; the number under them is the zoom level. Double-click the background to fit everything, or a node to center it.
  • -
  • Click a node for its groups, degree (with in- and out-degree for directed edges), betweenness, closeness, clustering coefficient (computed on the ticked groups and channels), attributes and neighbors; with directed edges, → and ← mark outgoing and incoming connections. Group names in that panel open the group's details.
  • -
  • Neighbourhoods and paths (new in NORMA 3.0): the node details offer Open neighbourhood as a view with 1, 2 or 3 steps. With nodes selected, Neighbourhood in the selection bar opens them with their direct neighbours; with exactly two selected, Shortest paths opens every shortest path from the node selected first to the second, keeping only the edges on those paths, and says how many paths there are. Both follow edge directions where edges are directed, use only the ticked groups and channels, and open as new views that keep the nodes' positions, groups, colors and settings; the starting nodes are selected and carry a role attribute (center, source or target).
  • -
  • Click an edge for its channel, endpoints, direction, weight and attributes.
  • -
  • Click the i next to a group for its description, attributes, member and edge counts, density, and the groups it shares members with.
  • -
  • Click empty space to clear the highlight.
  • -
  • Find nodes, at the top left of the network (the – button at its right minimizes the panel to a small Find nodes button, which still shows the number of matches; click it to open the panel again), highlights matching nodes and shows how many matched. Choose how to match: Contains, Exact name, Starts with, Ends with (suffix) or Regular expression. A regular expression can be typed as is, such as ^Rp[LS]\d+$, or between slashes with flags, such as /kinase$/i. Matching ignores case unless Match case is ticked, and also checks attribute values while Search attributes is ticked. Press Enter to zoom to the matches and Escape to clear.
  • -
  • The numbers at the top right count nodes, edges, edge channels and groups in the view; with groups unticked, the group count shows ticked/all, for example 8/9.
  • -
- -

3D Network

-

New in NORMA 3.0. The 3D Network page shows the current view in three dimensions. It is the same view as 2D Network: groups, colors, pie slices, node shapes, borders, labels, edge labels, arrows, curved parallel edges, group shading, ticked groups and channels, search results and selected nodes all carry over, and every setting in the Display tab except the 2D Layout section changes both pictures. Node positions and the camera are separate for 3D, and each view keeps its own.

-

Moving around

-
    -
  • Drag the background to rotate, right-drag (or Alt-drag) to pan, and scroll to zoom. Turning and tilting have no limit: drag on, or keep pressing a turn or tilt button, and the network rolls over as often as you like, so it can be seen from any side, including upside down.
  • -
  • Drag a node to move it within the screen plane; with several nodes selected, dragging one moves them all.
  • -
  • Click a node or an edge for its details, exactly as in 2D; click the background to clear. Shift-click selects nodes and Shift-drag draws a selection box. Double-click a node to turn around it, or the background to fit everything.
  • -
  • The buttons at the bottom left zoom (+, –, Fit), turn (↺, ↻), tilt (↑, ↓), reset the view (⟲) and switch automatic rotation on and off (Auto). With the 3D picture focused, the arrow keys turn and tilt, + and – zoom, and F fits.
  • -
  • Find nodes, Zoom to selection and Export image work here too and act on the 3D picture.
  • -
-

3D layout

-

On this page the Display tab shows a 3D layout section in place of the 2D one. As in 2D, layouts arrange only the nodes of ticked groups and, with Use checked channels only, only the ticked channels count.

-
    -
  • Force-directed 3D, weighted: the Fruchterman–Reingold idea in three dimensions; heavier edges pull harder. Each connected piece is laid out on its own and smaller pieces are placed around the largest. It runs in the background and is used the first time a view is shown in 3D.
  • -
  • Sphere, Cube grid and Random place nodes on a sphere, in a cube or at random, best-connected nodes first.
  • -
  • Groups as 3D clusters: each ticked group becomes a ball of its nodes, with hubs in the middle. Groups that share many edges are placed close together, and the balls never overlap. Nodes in several groups sit between their groups.
  • -
  • Groups on stacked layers: the 2D layout lies flat and each group gets its own layer, one above the other, so the links between groups run between the layers.
  • -
  • Same as 2D (flat), Height by hierarchy level and Height by degree keep the 2D layout and add height: none, the level from the top of a hierarchy (following edge directions when edges are directed), or the number of connections, so hubs rise above the rest. Height step sets the distance between levels.
  • -
-

Spread pushes nodes apart or pulls them together, as in 2D. Double-click the slider to go back to 1×.

-

Look

-
    -
  • View from turns the camera to the front, top, side or a tilted angle and fits the network.
  • -
  • Perspective: normal, strong (wide angle, more depth) or none (parallel projection, where sizes don't change with distance).
  • -
  • Nodes: shaded spheres or flat, as in 2D. Group shapes and pies are always drawn flat, facing you.
  • -
  • Depth fading blends distant nodes and edges into the background, which helps tell near from far.
  • -
  • Floor grid and axes draws a grid under the network and short x, y and z axes at its center.
  • -
  • Rotate automatically turns the network slowly; Rotation speed sets how fast.
  • -
-

Group shading follows the nodes as seen from the camera. Edge bundling applies to the 2D picture only; in 3D, edges are straight, and parallel edges curve apart when Shape is curved or bundled. On large networks the picture is simplified while you move it (no labels, pies or arrows) and drawn in full as soon as you stop; at most 500 node labels and 300 edge labels are drawn at a time, the nearest first.

- -

Layout

-

This section is about the 2D Network page; the 3D page has its own layouts (see 3D Network). Every layout arranges only the shown part of the view: nodes in ticked groups, connected by edges of ticked channels while Use checked channels only is on. Nodes of unticked groups stay where they are, so ticking them again brings them back in place.

-

The switch at the top of Layout chooses how nodes are arranged. Only one side is active at a time; the other is shown greyed out and marked frozen, and Run layout runs only the active side. The choice is saved with the view.

-

By connections

-

Positions come from the edges alone; groups play no part. Force-directed (cose) places connected nodes near each other. Force-directed, weighted is the Fruchterman–Reingold layout NORMA uses: heavier edges pull their nodes closer, and the result is the same every time for the same input. Kamada–Kawai (new in NORMA 3.0) treats every pair of nodes as a spring whose length is the number of steps between them and relaxes the springs one node at a time; stress majorization starts from a pivot-MDS layout and then minimises the same kind of stress (the mismatch between on-screen and network distances) for all nodes together, which is usually faster and keeps distances most faithfully. Both ignore edge weights, lay out each connected part separately, and handle parts of up to 2,500 nodes. They are also available as group strategies and inside groups. Concentric puts high-degree nodes in the middle; hierarchical arranges nodes in levels from a root, following edge directions when the view has directed edges. Networks above 700 nodes load with a grid layout, since a force-directed layout can freeze the page for a while at that size; you can still run it yourself.

-

By groups

-

Group arrangement decides where each group goes, and Layout inside each group arranges the group's own nodes (circle, force-directed, concentric, hierarchical, grid or random). A node in several groups sits at the average of its positions, between those groups. Only groups ticked under Node groups count.

-
    -
  • Groups as separate blocks treats each group, plus one block for nodes in no group, as a disc sized to fit its nodes, and places the discs so they never overlap. Force-directed puts groups that share many edges next to each other and packs the rest around them; Circle lines them up on a ring; Grid packs them in rows; Hierarchical stacks them in levels starting from the best-connected group; Concentric puts the largest group in the middle with the others in rings by size. Space between groups sets the gap.
  • -
  • NORMA-2.0 strategies (Karatzas et al., 2022, Bioinformatics Advances 2:vbac036, Figure 1) use the same weights and steps as NORMA's own code, with the algorithm you pick under Algorithm the strategy runs. A small diagram shows how the selected strategy works; links a strategy adds exist only while positions are computed. Weights change the result only with the two force-directed algorithms. -
      -
    • Strategy 1, virtual node per group: each group gets a hidden hub linked to all its members with weight 50. Real edges get weight 5, and every pair of members gets a light link (0.1). The algorithm runs on this larger network and the hubs are then removed.
    • -
    • Strategy 2, group gravity: every pair of nodes in a group gets a link at the network's highest weight. Real edges inside a group are multiplied by Force strength, and edges between groups, or to ungrouped nodes, are divided by it.
    • -
    • Strategy 3, super nodes per group: each group becomes a single node and ungrouped nodes stay as they are. The algorithm lays out this collapsed network and Force strength pushes the result outward.
    • -
    - Strategies 1 and 2 place every node themselves. With them, Layout inside each group can also be Keep the strategy's arrangement; any other choice rearranges each group around where the strategy put it and keeps groups from overlapping.
  • -
-

Group size scales each group's local layout. Force strength runs from 1 to 20 (NORMA's default is 10). Use checked channels only applies to both sides. A group with more than 400 nodes gets 60 random partner links per node instead of links to every other member, to keep the layout fast. While By groups is active, it is also used when a network loads and when you switch annotations.

-

Fit to screen zooms to show the whole network. Spread changes how strongly nodes repel each other compared with the pull of their links, from a quarter to four times the strength of the current arrangement. To the right, dense clusters open up and loosely linked parts move apart; to the left, linked nodes are drawn together while nodes are kept from overlapping. It starts from the current positions and keeps the overall arrangement, and going back to 1× restores them. Double-click the slider to go back to 1×. Moving nodes by hand starts again from their new positions, and running a layout resets it.

- -

Node groups

-

Groups are listed A–Z (numbers in numeric order, so Group-2 comes before Group-10). The Order menu under the filter also sorts them by their number of nodes (the count shown on each row), Most nodes first or Fewest nodes first (groups of equal size stay A–Z), or keeps the File order of the annotation file. Nodes in no group always come last. The legend follows the same order, each view remembers the choice, and group colors don't change. Node names in the details panels, and the files in Files and in every file menu (Grouping, Network Profiler, Network Comparison, enrichment) are always listed A–Z.

-

Lists the groups of the current annotation with their node counts. Untick a group to hide nodes that are only in inactive groups; a node stays visible while any of its groups is active, and its pie shows only its active groups. Click a swatch to recolor a group. Nodes that no group lists appear under Not in any group.

-

Type in the filter box to narrow long lists such as GO terms. While a filter is set, the two buttons activate or deactivate only the matching groups, which makes it quick to show, for example, only terms containing "signaling". Pies show up to 16 slices; a node in more active groups gets a gray slice for the rest.

-

Node shapes

-

The small shape button on each group row opens a choice of shapes: circle, square, rounded square, diamond, triangle, vee, pentagon, hexagon, octagon, star, rhomboid, tag, concave hexagon and barrel. Use the arrow keys to move through them and Escape to close. The same shapes are offered in a group's details panel (its i button). Assign shapes gives each listed group a different shape at once, and Reset shapes makes them circular again; with a filter set, both apply only to the matching groups.

-

A node takes its group's shape only when exactly one of its groups is ticked. Nodes shown with two or more active groups stay circular, so their pie slices stay readable. Unticking groups therefore changes shapes: a node in a square group and a diamond group is a two-color circle, becomes a square when the diamond group is unticked, and a diamond when the square one is. Shapes combine with either fill: with expression colors, the shape shows the group and the color shows expression. Each view keeps its own shapes, and they are saved in JSON.

- -

Group highlighting

-

Shades the area behind each active group in its color, as NORMA does. Opacity controls how strong the shading is. Three styles are available:

-
    -
  • Convex hull draws a sharp outline around all of the group's nodes. Groups of one or two nodes get a circle or capsule.
  • -
  • Bubble sets draws contours that hug the group, in the style of Collins, Penn and Carpendale's Bubble Sets (IEEE TVCG 2009): the outline follows the group's nodes closely, links them with narrow bands so each group stays in one piece where possible, and bends around nodes that are not in the group, leaving holes where outsiders sit inside. Interleaved groups therefore stay readable where convex hulls would cover each other. Contours are recomputed when nodes move, groups are ticked or the zoom changes a lot; on networks with more than 1,500 shown nodes they are updated once the movement stops.
  • -
  • Fog cloud draws a soft, blurred area.
  • -
-

All three styles appear in image exports (SVG included) and on the 3D Network page, where bubble sets are drawn from the camera's point of view and simplified while the picture moves.

- -

Colors

-
    -
  • Node fill chooses between group colors, expression colors (from an expression file or each node's color field) and numeric values on a color scale, when the view has numeric values.
  • -
  • Numeric values (shown when the view has them): -
      -
    • Column picks which value to show, for example log2FC or padj.
    • -
    • Transform: use the values as they are, their −log10 (so small p-values become large), or their absolute values.
    • -
    • Scale: diverging scales (blue–white–red, purple–white–orange, purple–white–green, brown–white–teal) show values below and above a Center (0 by default), which suits fold changes; sequential scales (viridis, magma, blues, reds) run from low to high, which suits p-values and intensities. Most of these scales stay readable under common forms of color vision deficiency.
    • -
    • Range: symmetric around the center (diverging scales), from the lowest to the highest shown value, or custom; values beyond the range get the end colors.
    • -
    • No value sets the color of nodes without a value. A line under the controls says how many shown nodes have a value and their range.
    • -
    - Steps (time points or conditions) appear when there are several value columns: ◀ and ▶ move one step, Play steps through them repeatedly at the chosen speed, and the slider jumps to any step, while node positions stay put. The current step shows at the top of the network. Same color scale for all steps (on by default) fixes the scale over all columns so colors can be compared between steps. Add the numeric files in Files as steps adds every column of the other numeric files as further steps, named file: column; nodes a file doesn't list get no value for its steps. - A numeric file selected in Files switches the fill to numeric values automatically.
  • -
  • Group palette recolors all groups. The Okabe–Ito, IBM and Viridis palettes stay distinguishable under common forms of color vision deficiency. Past a palette's own colors, further colors are spread evenly around the color wheel, which keeps around 100 groups distinguishable.
  • -
  • Channel palette does the same for edge channels. Classic uses STRING's evidence colors.
  • -
-

Themes (top bar) change the interface and canvas background, not the data colors.

- -

Node size

-

Nodes can keep the size given in the data or be sized by degree, in-degree, out-degree, betweenness, closeness, clustering coefficient or a numeric value (the column and transform chosen under Colors; on a diverging scale the distance from the center, so strongly raised and strongly lowered nodes are both large). With directed edges, betweenness and closeness follow edge directions. Sizes run from Min px for the lowest value to Max px for the highest. Metrics are computed on the nodes of ticked groups; Use checked channels only (on by default) also limits them to the ticked channels. Sizes update as you tick and untick. Node scale multiplies every size, from a tenth to five times.

- -

Labels

-
    -
  • Node labels show node names, centered on the node by default; the position list moves them above, below or to a side. Node label size sets their size, and Grow labels with node size scales each label with its node, so bigger nodes get bigger names.
  • -
  • Edge labels are drawn at the middle of each edge, whether straight, curved or bundled. They can show the channel, the weight, both, or any edge attribute in the network. Merged edges show how many channels they combine and their highest weight. Orientation runs them along the edge or keeps them horizontal, and Edge label size sets their size. With edge labels on, straight edges are drawn with a slightly slower method that supports labels.
  • -
  • Label colors: node and edge labels can follow the theme, use black, dark gray, white, blue, red or green, match their node's color or their edge's channel color, or use any color chosen with Custom…. White labels get a dark outline so they stay readable on a light background.
  • -
  • Hide labels when zoomed out drops labels that would appear smaller than the chosen size on screen, which keeps large networks readable and fast.
  • -
- -

Edges

-
    -
  • Direction: handle edges as in the data, all as directed, or all as undirected; with arrows shown, choose their shape and size (see Directed and undirected networks).
  • -
  • Parallel edges: draw every channel separately, or merge each node pair into one edge (see multi-edges); directed edges merge only with edges running the same way.
  • -
  • Shape: straight lines are fastest. Curved edges fan parallel edges apart, and Fan-out sets how far.
  • -
  • Bundled draws edges that run in similar directions together, like cables in a tray, which untangles dense networks and shows the main routes between groups. It uses force-directed edge bundling (Holten and van Wijk, 2009): edges whose direction, length, position and overlap are similar pull on each other, while springs keep each edge smooth. Bundling strength sets how readily edges join a bundle and how tightly they are drawn together. Bundles are computed in the background, so the page stays usable: about 5 seconds for 3,000 edges and half a minute for 8,000. They are recomputed after a layout, after dragging nodes, after using Spread, and when groups or channels are ticked or unticked. Parallel edges between the same pair follow the same bundle. Only visible edges are bundled, up to 10,000 at a time; merging parallel edges or unticking channels brings larger networks under that limit.
  • -
  • Thickness: a fixed width, or mapped from each edge's weight between Min and Max px. Edges without a weight get the middle width.
  • -
  • Edge opacity: how solid edges are drawn; lower it to let dense networks show their nodes.
  • -
- -

Edge channels

-

Lists each kind of edge with its count. Untick a channel to hide its edges; click its swatch to recolor it. For files loaded from Files, each network (or each value of its Type column) is a channel.

- -

Legend

-

Display → Legend → Show a legend on the network places a legend at the bottom right of the 2D and 3D network (it steps aside while a node's or edge's details are open). It can include the color scale of numeric values with its ticks and the "no value" color, the groups with their colors and, with Group shapes, their node shapes, and the edge channels when there are two or more. An optional title goes on top. Groups and channels follow what is ticked, and the legend updates as colors, shapes or settings change.

-

Views made by Show as network on the comparison page come with the legend switched on and an extra Networks part that says which letter stands for which network.

-

In Export image, Add the legend puts the same legend to the right of the picture, at the picture's scale, in every format; in SVG files it is a separate, editable group. The legend is included whether or not it is shown on screen.

- -

Attributes

-

Summarizes the extra data carried by nodes, edges and groups: each field, its kind (text, number, yes/no, list), its range or values, and how many items have it. Values appear when you click a node, an edge or a group's i, and search looks through node attribute values.

- -
Analysis
-

Network Profiler

-

Besides the statistics below, the page holds Group analysis, the Layout benchmark and the Runtime table.

-

Tick any mix of the current view and networks from Files, then choose Compute statistics. Results appear side by side so networks can be compared, and Download table saves them as tab-separated text. Each network is profiled as a simple undirected graph. The current view is profiled as shown: only nodes of ticked groups and edges of ticked channels count, and parallel edges from different channels count as one connection.

-

With Use edge direction ticked (it is off by default), networks with directed edges get a second table, Direction: directed edges, reciprocity (the share of directed edges whose reverse also exists), maximum in- and out-degree, source and sink nodes, strongly connected components, the share of node pairs that can reach each other, and the average directed path length and diameter. Undirected edges can be walked both ways. The main table always treats edges as undirected, as igraph does for these statistics.

- - - - -
StatisticMeaning
-

Below the table:

-
    -
  • Degree distribution: how many nodes have each degree. Tick log–log to check for a power law.
  • -
  • Most central nodes: the top ten by the measure you choose. Click a name to find it in the 2D Network page when it's in the current view.
  • -
  • Communities: choose an algorithm and choose Find communities; Louvain is shown by default. Add as annotation saves the communities (without single nodes) to Files as a new annotation you can view like any other; NORMA's Tau example ships such a file. All algorithms work on the simple undirected network, use a fixed random seed so results repeat, and are scored by the same modularity. -
      -
    • Louvain (Blondel et al., 2008): greedy modularity optimisation. Resolution above 1 gives more, smaller communities.
    • -
    • Leiden (Traag et al., 2019): Louvain with a refinement step that guarantees every community is connected; same resolution setting.
    • -
    • Label propagation (Raghavan et al., 2007): each node repeatedly takes the most common label among its neighbours; very fast. Communities that end up in pieces are split into connected parts.
    • -
    • Walktrap (Pons and Latapy, 2006): merges communities that short random walks of the chosen walk length cannot tell apart, and keeps the partition with the highest modularity. Limited to 2,000 nodes.
    • -
    • Markov clustering, MCL (van Dongen, 2000): simulates flow on the network; higher inflation gives more, smaller clusters. Clusters that end up in pieces are split into connected parts.
    • -
  • -
  • Edge channels (current view only, with two or more channels): how many node pairs carry one, two or more channels, and how much each pair of channels overlaps (Jaccard index of their node pairs).
  • -
-

Path-based statistics (distances, betweenness, closeness) visit every node from every node, so networks above 8,000 nodes skip them.

- -

Group analysis

-

The Group analysis part of the Network Profiler page works on the current view: its ticked groups and channels.

-
    -
  • Group statistics: for each group, its nodes, the edges inside it and leaving it, its density inside (edges inside divided by all possible pairs), the mean degree inside, its conductance (edges leaving divided by the smaller of the group's and the rest's total degree; lower means a better-separated group) and its share of the modularity. Above the table, the modularity of the grouping says how well the groups as a whole follow the network's structure (above about 0.3 is clear structure). For this number, a node in several groups counts for its first group and a node in none counts as a group of its own. Download statistics saves the table.
  • -
  • Open group network opens a new view with one node per group, sized by the group's size, and an edge between two groups weighted (and labelled) by how many of their members are connected; each node also carries its group's edges inside as an attribute. Parallel channels count once. This is the network NORMA-2.0's strategy 3 lays out internally.
  • -
  • Enrichment tests whether the groups of an annotation (the terms, for example KEGG pathways) are over-represented, with a one-sided hypergeometric test: -
      -
    • Test: the nodes selected in the view (for example a cluster you selected by hand, or the nodes a Select button picked), or each group of an annotation (for example Louvain communities or the view's own groups).
    • -
    • Terms from: any annotation in Files, or the current view's groups.
    • -
    • Background: the shown nodes of the current view (the usual choice), or those plus every node of the term annotation.
    • -
    • Only terms and tested sets with at least Smallest overlap nodes count. p-values are adjusted with the Benjamini–Hochberg procedure within each tested set, and terms with an FDR above the limit are left out.
    • -
    - The table lists each enriched term with its overlap, its size in the background, the fold enrichment, the p-value and the FDR; Select selects the overlapping nodes in the view, and Download results saves every row with the node names.
  • -
- -

Group separation and layout benchmark

-

The Group separation box in the 2D Layout section measures how well the current layout separates the ticked groups. It updates after every layout, after dragging nodes or using Spread, and when groups are ticked or unticked.

-
    -
  • Silhouette: the mean silhouette width (Rousseeuw, 1987) of the nodes that are in exactly one group, from their positions. For each node, it compares the mean distance to its own group with the mean distance to the nearest other group. It runs from −1 (groups intermixed) through 0 (no separation) to 1 (compact, well-separated groups). On large networks it is computed on a fixed sample of 1,500 nodes.
  • -
  • Inside other outlines: the share of shown nodes that lie inside the convex outline of a group they don't belong to. These are the nodes a hull drawing would misleadingly shade.
  • -
  • Outline overlap: the share of the outlined area that is covered by two or more group outlines. Groups of fewer than three nodes have no outline.
  • -
-

The Layout benchmark at the end of the Network Profiler page runs every layout on the current view without changing it: the seven layouts by connections (random as a baseline), the six group arrangements and the three NORMA-2.0 strategies, with the Layout section's settings for group layouts. For each it reports the run time and the three scores, as the mean (and standard deviation) over 1, 3 or 5 runs; the best value in each column is highlighted. Use applies a layout to the view, and Download table saves the results as tab-separated text, ready for a supplementary table.

- -

Network Comparison

-

The Network Comparison page sets two to ten networks side by side. Tick them in the list: Open views compare what each view shows: its files, and only its ticked groups and channels, and Network files compare uploaded or example networks directly. Choose Compare; each network gets a letter (A to J) and a color used throughout the results. Nodes are matched by name and edges by the pair of nodes they join, ignoring channels. With Use edge direction ticked (off by default), direction counts: A→B, B→A and an undirected A–B are three different edges, and directed statistics are added to the topology table. Untick it to match edges regardless of direction.

-
    -
  • Networks compared: the size of each network and how many nodes and edges all of them share.
  • -
  • Node overlap and Edge overlap: for two or three networks, a Venn diagram with the count in every region (circle sizes are schematic). For any number, an UpSet plot: each column is a group of nodes or edges found in exactly the networks marked by dots below it, sorted largest first; the bars at the left give each network's total. Hover a bar for its count. With many networks only the 40 largest combinations are drawn; the rest are listed in the downloaded table.
  • -
  • Jaccard similarity: for each pair, shared items divided by items in either network, from 0 (nothing in common) to 1 (identical). Darker cells are more similar; hover for the counts.
  • -
  • Degree agreement on shared nodes: the Spearman correlation of a node's degree in two networks, over the nodes they share. Close to 1 means hubs in one are hubs in the other; negative values (red) mean roles swap. A dash means fewer than three shared nodes.
  • -
  • Degree distributions: for each network, the share of nodes with at least a given degree, on log–log axes.
  • -
  • Topology side by side: every Network Profiler statistic for each network, with the highest value in each row in bold.
  • -
-

Download table saves the node and edge membership (1 or 0 per network) and the topology table as tab-separated text.

-

Show as network (new in NORMA 3.0) opens the compared networks as one view, with a legend that explains its colors: which letter stands for which network, which node colors mean "in which networks", and which edge colors mean "A only", "B only" or "Shared". Every edge becomes a channel that says which networks contain it: A only and B only in the networks' colors and Shared in gray (for three or more networks, combinations such as A + C and In all). Nodes are grouped the same way. Tick channels in the Display tab to see, for example, only what the disease network gained, and use edge direction as set for the comparison.

- -
Saving and sharing
-

Interpreting the results

-

NORMA's results are pictures and numbers about how groups sit in a network. How to read them:

-

The network picture

-
    -
  • Groups: a shaded area (hull, bubble set or fog cloud) shows which nodes belong to a group; a pie-chart node belongs to several groups (one slice each), and a node shape stands for one group. Overlapping shading means shared members or nodes placed close together, not necessarily shared biology.
  • -
  • Positions: in layouts by connections, nodes that are close are well connected, so groups that come out compact and separate really are separated in the network. Layouts by groups place groups apart on purpose, which makes them easy to see but says nothing about how separated they are; use the group separation score on a connection layout to judge that.
  • -
  • Colors, sizes and edges: node colors show group membership, expression colors or numeric values (read them against the legend; with Same color scale for all steps, colors can be compared between time points). Node size follows the chosen measure (for example degree: bigger nodes have more connections). Edge colors are channels (evidence types, interaction types or networks), thickness can follow weight, and arrows show direction.
  • -
  • Edge weights from databases: STRING's confidence score (0–1), the number of shared reactions (Reactome), curation effort (OmniPath) and the IntAct MI-score (0–1). Higher means better supported.
  • -
-

Network Profiler

-
    -
  • Size and density: density is the share of possible connections present; biological networks are usually sparse (well below 0.1).
  • -
  • Connectivity: the number of connected components and the size of the largest; the average path length and diameter say how far apart nodes are (small values mean a "small-world" network).
  • -
  • Clustering coefficient (0–1): how often a node's neighbours are connected to each other; high values mean tightly knit neighbourhoods.
  • -
  • Degree distribution: a straight line on the log–log plot suggests a few highly connected hubs and many poorly connected nodes.
  • -
  • Centrality: nodes with high betweenness lie on many shortest paths (bridges between modules); high closeness means close to all others; high degree means many partners. The top-ten lists point to candidate key nodes.
  • -
  • Communities and modularity: each community is a set of nodes more connected among themselves than to the rest. Modularity above about 0.3 indicates clear community structure; compare the communities with your annotation using Group analysis or enrichment.
  • -
-

Group analysis and enrichment

-
    -
  • Density inside higher than the network's density, and conductance close to 0, mean a group is a real module of the network; conductance near 1 means its members mostly connect outside the group. The modularity of the grouping summarises this for all groups (above about 0.3: the grouping follows the network's structure).
  • -
  • Enrichment: fold enrichment above 1 means a term is more common in the tested set than in the background; the FDR (Benjamini–Hochberg adjusted p-value) says how likely that is by chance, and terms with FDR ≤ 0.05 are usually reported. Small overlaps (2–3 nodes) and the choice of background strongly affect the result; the background should be the nodes you could have picked.
  • -
-

Group separation and layout benchmark

-
    -
  • Silhouette (−1 to 1): above about 0.5, groups are well separated in the picture; 0.25–0.5 moderately; below 0.25 they overlap; negative values mean nodes sit closer to other groups than to their own.
  • -
  • Outline intrusion and overlap (0–100%): the share of nodes inside another group's outline and of shaded area covered twice; lower is clearer. The benchmark compares layouts on these scores, averaged over repeated runs.
  • -
-

Network Comparison

-
    -
  • Venn and UpSet plots count the nodes and edges found in exactly each combination of networks; the tallest UpSet bars show the largest shared or unique parts.
  • -
  • Jaccard similarity (0–1): shared items divided by items in either network; 1 means identical. Degree agreement (Spearman, −1 to 1) says whether shared nodes are similarly connected in both networks.
  • -
  • Show as network colors each edge by the networks that contain it (A only, B only, shared), so gained and lost connections can be seen directly.
  • -
- -

Export and saved work

-

The Export tab saves and sends: NORMA files, Image, Other tools, Save your work and, at the bottom, Arena3D. Saved work is opened again under Upload Data → Open saved work. Three kinds of NORMA JSON file keep work for later, and each is saved and opened with its own button:

- - - - - - - -
FileHoldsSave / open
View file (.json)One view: its network, groups, colors, attributes and node positions (see JSON format)Save view file / Open view file… or Paste JSON…
Session file (.json)Every open view and every file in FilesSave session file / Open session file…
Settings file (.json)Display settings only, to apply the same look to other viewsSave settings file / Open settings file…
-

The other JSON files NORMA writes are for other programs: Arena3D (.json) for Arena3D and Cytoscape (.cyjs) for Cytoscape.

-
    -
  • NORMA files save the current view as a network, annotation or expression file that NORMA can read. -
      -
    • Parallel edges are merged into one connection and keep their highest weight, unless you tick Keep channels as a Type column. When any edge is directed, a Direction column is added and A→B and B→A stay separate rows. Files with Type or Direction columns open in NORMA 3.0 as multi-edge or directed networks; earlier versions of NORMA expect only Source, Target and Weight and may not read them, so leave the box unticked for files meant for those versions.
    • -
    • Nodes without connections are left out, since NORMA can't list them.
    • -
    • Commas in node names become semicolons.
    • -
    • With no expression colors loaded, each node gets its first group's color.
    • -
    -
  • -
  • Only ticked groups and channels (on by default) makes every export below save just the shown part of the view; untick it to save the whole network.
  • -
  • Export image… (also the camera button in the top bar) saves the current view as a picture: -
      -
    • Formats: PNG (supports a transparent background), JPEG (smaller files), WebP (small, sharp web images), SVG (vector: stays sharp at any size and can be edited in Illustrator, Inkscape or PowerPoint, with nodes, edges, labels and group shading in separate layers) and PDF (one page with a high-resolution image, sized so one screen pixel is one point).
    • -
    • Area: the whole network (the ticked groups and channels, with a margin for group shading) or exactly what is on screen.
    • -
    • Resolution: 1× to 8× the on-screen size. The screen counts as 96 dpi, so each step adds 96 dpi at the size the network has on screen: 1× is 96 dpi, 2× 192 dpi, 3× 288 dpi (about the 300 dpi journals ask for), 4× 384 dpi, 6× 576 dpi and 8× 768 dpi. Below the options, the dialog shows the image's exact size in pixels, the dpi and print size (inches and centimeters) at the on-screen size, and the print size at 300 dpi. PNG and JPEG files store this dpi, so layout and print programs open them at the right size. For a figure of a given width, divide the pixel width by the dpi you need: a 3,000-pixel-wide image prints 10 inches (25.4 cm) wide at 300 dpi. SVG is vector graphics, sharp at any size, so its "resolution" only sets the picture's size in the file; PDF pages have the on-screen size. Very large images are reduced to the biggest size the browser can draw, and the dialog says so.
    • -
    • Background: the theme color, white, transparent (PNG, WebP and SVG) or any color.
    • -
    • Add the legend puts the legend set under Display → Legend to the right of the picture.
    • -
    • Include group shading adds the shaded group areas, and Leave out selection and search highlighting (on by default) draws the network without yellow selection borders or dimmed nodes; the selection is restored afterwards.
    • -
    - The picture shows the view as it looks: layout, colors, shapes, pies, labels, edge labels and bundled edges. On the 3D Network page the dialog saves the 3D picture from the current angle, in any of the same formats (SVG included); Whole network fits the network into the picture without changing the angle.
  • -
  • Other tools: GraphML (yEd, Cytoscape, igraph, NetworkX), GEXF 1.3 with node colors, sizes and positions (Gephi), SIF (Cytoscape's simple interaction format: source, channel, target) and Cytoscape (.cyjs) (with positions, for Cytoscape desktop and Cytoscape.js). Nodes carry their groups (separated by semicolons), color, size, numeric values and attributes; edges their channel, color, weight, direction and attributes. Like the other exports, they include only ticked groups and channels unless that option is off.
  • -
  • Session: Save session file saves every open view (network, positions, settings, 3D camera) and every file in Files, including groupings fetched from STRING, in one JSON file; Open session file… brings it all back, replacing the current views and files.
  • -
  • Arena3D: open the view in Arena3D, or save it in Arena3D's formats, with each group as a layer (see Arena3D).
  • -
  • Save view file saves everything in the current view, including attributes, colors and group descriptions. Open view file… and Paste JSON… load that format into a new view.
  • -
  • Save settings file stores the current view's display options (palettes, layout, sizing, labels, shading, edges) and the theme in a small file that Open settings file… applies to the current view.
  • -
- -

Arena3D

-

Arena3D shows multilayer networks in 3D. Under Export → Arena3D, NORMA turns the current view into an Arena3D network in which each ticked group is a layer:

-
    -
  • Layers appear in the order of the group list, side by side, each floor in its group's color; nodes in no group get a layer of their own.
  • -
  • Inside a layer, nodes keep their 2D positions from NORMA, scaled to fit the layer, and their size.
  • -
  • Edges between members of the same group stay in that group's layer; edges between groups run between the layers. A node in several groups appears in each of its layers, and with Link the copies of a node that is in several layers (on by default) its copies are joined by white edges.
  • -
  • Edge colors are the channel colors; with weights, stronger edges are more opaque. Several channels are kept as Arena3D channels, and directed edges switch on Arena3D's direction display.
  • -
  • Node colors: as shown in NORMA (group, expression or numeric colors; pie nodes take the color of the layer they are drawn in), or the color of each node's layer.
  • -
  • Only the ticked groups and channels are exported, like everywhere else in NORMA. Arena3D takes up to 20 layers: with more ticked groups (counting the layer for nodes in no group), only the first 20 in the group list are exported, and the status note says how many groups and nodes were left out. Untick groups you don't need to choose which 20 go.
  • -
-

Comparing networks in Arena3D: after Compare, the Network Comparison page offers the same three ways out for the comparison, with one layer per compared network (A, B, … in the networks' colors). Each layer holds that network's nodes and edges; nodes and edges found only in that network take its color, and common ones are gray. All networks are laid out together, so a node sits at the same place in every layer. Between layers chooses what links the layers: Common edges (an edge found in networks A and B also runs from its source in layer A to its target in layer B, in white), Common nodes (the copies of a node found in several networks are joined, in yellow), or both. Up to 20 layers fit, so all ten comparable networks can go.

-

Three ways out (for the current view, under Export → Arena3D):

-
    -
  • Open in Arena3D sends the network to Arena3D (/api/external) and opens the returned link in a new tab. Some browsers block direct calls to other sites; running NORMA with server.py avoids that, because the server passes the request on (see Running NORMA locally or on a server). The address can be changed under Arena3D options.
  • -
  • Arena3D (.json) saves the same network in Arena3D's JSON format, to load in Arena3D yourself.
  • -
  • Table (.txt) saves Arena3D's tab-separated upload format: SourceNode, SourceLayer, TargetNode, TargetLayer, Weight, Channel. NORMA can read such files back (see Files and formats).
  • -
- -
Running, API and troubleshooting
-

API for other applications

-

Other programs can open NORMA with their own networks, groups and values, through the API tab's three routes (full reference, code for curl, Python, R and JavaScript, and a tester are on that tab):

-
    -
  • REST (needs server.py): POST /api/external with a JSON payload returns {"token", "url"}; opening url (norma.html?session=TOKEN) shows the data. Payloads are kept in the server's memory for 24 hours (NORMA_API_TTL_HOURS). GET /api/health tells whether a server offers the API.
  • -
  • Links (no server needed): norma.html?data=URL loads a payload file, ?network=URL&annotation=URL&expression=URL loads NORMA files (the other site must allow cross-site downloads), and #json=… carries a small payload inside the link.
  • -
  • postMessage (no server needed): a page that opens NORMA in a window or iframe receives norma:ready, sends {"type": "norma:load", "payload": …} and gets norma:loaded back with a summary or an error.
  • -
-

The payload is a JSON object: name; the network as edges (source, target, optional type, weight, directed), as NORMA files or as a complete network view; optional nodes with attributes, groups (and more groupings in annotations), expression (colors or numbers per node), settings (display settings, theme, layout) and tab. A whole session file is accepted too. Each call opens a new view; the network, groupings and values also appear in Files. If a link's data cannot be read, NORMA opens the API tab and says why.

- -

Running NORMA locally or on a server

-

NORMA is one web page (norma.html) plus a small server program (server.py, Python 3.8 or newer, no extra packages). The same package runs NORMA on your own computer or as a public web server; everything that differs is a setting.

-

On your own computer

-
    -
  • Double-click run_local.sh (macOS, Linux) or run_local.bat (Windows), or run python3 server.py: NORMA opens in your browser at http://localhost:8000, reachable only from this computer.
  • -
  • Without Python: open norma.html directly. Everything works except the REST API and the relays; database importers then call the services directly, which some browsers block.
  • -
-

As a public web server

-
    -
  • Run python3 server.py --mode hosted --config norma.config.json (start from norma.config.hosted.json), behind a web server that provides HTTPS on port 443 (deploy/nginx.conf), as a system service (deploy/norma.service) or in a container (Dockerfile, docker-compose.yml). The server can also serve HTTPS itself (--tls-cert, --tls-key, optional --http-redirect-port 80).
  • -
  • Hosted mode listens on all addresses, trusts the proxy's X-Forwarded-* headers, logs requests without IP addresses, sends security headers (HSTS over HTTPS) and never opens a browser.
  • -
  • Any static web server (nginx, Apache, GitHub Pages) can also serve the folder; settings then come from norma-config.js, and the REST API and relays are not available.
  • -
-

Settings

-

Settings come from, in increasing priority: built-in defaults, the settings file (norma.config.json, or --config / NORMA_CONFIG), environment variables and command-line options; python3 server.py --print-config shows the result, and --help lists every option. The server hands the page its part of the settings as /norma-config.js.

- - - - - - - - - - - - - - -
SettingOption / variableMeaning
mode--mode, NORMA_MODElocal (default) or hosted; sets sensible defaults for the rest
server.host, server.port--host, --port, NORMA_HOST, NORMA_PORTWhere the server listens (local 127.0.0.1, hosted 0.0.0.0; port 8000)
server.publicUrl--public-url, NORMA_PUBLIC_URLThe address users see (e.g. https://norma.example.org/); used in API links
server.tlsCert, tlsKey, httpRedirectPort, hsts--tls-cert, --tls-key, --http-redirect-portServe HTTPS directly, redirect HTTP to it, send Strict-Transport-Security
server.trustProxyNORMA_TRUST_PROXYRead the reverse proxy's forwarded headers (hosted: on)
server.accessLog--access-log, NORMA_ACCESS_LOGoff (local), anonymous (hosted: no IP addresses) or full
api.enabled, ttlHours, maxMB, maxSessions--no-api, NORMA_API, NORMA_API_TTL_HOURS, NORMA_API_MAX_MBThe REST API and how long, how large and how many payloads it keeps
relays.string, arena3d, databases--no-relays, NORMA_RELAYSTurn the relays on or off; without them the page calls the services directly
site.*NORMA_CONTACT_EMAIL, NORMA_INSTITUTION, NORMA_NOTICEName, institution, contact e-mail and page, source code, licence name and link, privacy policy and imprint links, maintenance statement, tested browsers, and a notice shown at the top of the page (e.g. planned maintenance)
app.maxNodes, theme, startTab, cdnFallbackNORMA_MAX_NODESLargest network shown (default 10,000), starting theme and page, and whether Cytoscape.js may be downloaded if the bundled copy is missing
-

What the server offers

-
    -
  • API: NORMA's REST API (/api/external, /api/session/TOKEN, /api/health); see API for other applications.
  • -
  • STRING relay: /string-api/ passes read-only STRING calls to string-db.org addresses, spaced a second apart. With Database importers → STRING → STRING server → Connect set to Automatically, NORMA uses the relay when it is available and otherwise calls STRING directly.
  • -
  • Database relay: /db-api/fetch passes the importers' calls to Reactome, OmniPath, NDEx, IntAct, QuickGO and the GO API, and only to those services' API addresses.
  • -
  • Arena3D relay: /arena3d-api/external passes Open in Arena3D to arena3d.org.
  • -
  • Package contents: norma.html, index.html (forwards static web servers' root address to norma.html), norma-config.js, server.py, norma.config.json and norma.config.hosted.json, norma_api_client.py (Python template), run_local.sh / run_local.bat, Dockerfile, docker-compose.yml, deploy/ (nginx, systemd), vendor/ (Cytoscape.js), assets/, README.md and NAR_CHECKLIST.md.
  • -
  • STRING logo: to show STRING's official logo in the importer, save it as assets/string-logo.png.
  • -
- -

Performance

-
    -
  • Display → Performance → Draw with WebGL uses Cytoscape.js's WebGL renderer, which draws large networks much faster. It is experimental: the choice is made when the page starts, so NORMA asks to reload the page (save the session first to keep your views). Adding ?webgl=1 or ?webgl=0 to the address sets it for one visit. Exported images, group shading, the 3D page and everything else work the same.
  • -
  • The Runtime table at the end of the Network Profiler page times the main steps on random networks of 100 to 10,000 nodes in your browser: reading the network file, building the network, the weighted layout, edge bundling, the profile statistics, Louvain and the group separation score. Drawing time depends on the screen and is not included. Download table saves the times with the browser and processor count, for a supplementary table.
  • -
  • Network size: NORMA shows networks of up to 10,000 nodes. A network file, JSON file, session or combination of networks with more nodes is cut to its first 10,000 nodes (in the order they first appear) and the edges among them, and a message says how much was left out. To study part of a bigger network, filter it first (for example to a pathway, a STRING neighbourhood or a community).
  • -
  • Networks of a few thousand nodes are comfortable; beyond that, turn on WebGL drawing, turn off labels, curved edges and edge bundling, and use the grid or weighted force-directed layout.
  • -
- -

Keyboard shortcuts

- - - - - - - - - - - - - - - - - -
KeysAction
Ctrl+Z (⌘Z)Undo the last change to the view
Ctrl+Shift+Z or Ctrl+YRedo
Ctrl+A (⌘A)Select every shown node (2D and 3D Network pages)
EscapeClear the selection; in the search box, clear the search; in a dialog or shape chooser, close it
Enter in the search boxZoom to the matching nodes
Shift-drag on the backgroundDraw a selection box
Shift-click or Ctrl/⌘-clickAdd a node to the selection or remove it
Double-clickOn the background: fit the network; on a node: center it
Left/Right arrows on a tabMove between pages or between the Data and Display tabs
Arrow keys in the shape chooserMove between shapes; Enter picks one
Arrow keys on the 3D networkTurn and tilt the view (with Shift: in bigger steps)
+ / – on the 3D networkZoom in and out
F or 0 on the 3D networkFit the network to the screen
- -

Troubleshooting

-
    -
  • Only part of my network appears: NORMA shows up to 10,000 nodes and keeps the first 10,000 of a larger network (see Performance).
  • -
  • A network file is rejected: its first line must be the header Source, Target (optionally Weight and Type), separated by tabs.
  • -
  • Groups are missing: node names in the annotation must match the network exactly, including capitalization. Names that don't match are discarded when the annotation is uploaded and when it is shown; the status notes list them, and Download corrected annotation saves the cleaned file.
  • -
  • An annotation was not kept: none of its names matched the networks it was checked against. Upload it together with its network, or tick the right network before uploading it.
  • -
  • A node doesn't take its group's shape: it is shown with more than one active group, so it stays a circle. Untick the other groups to see the shape.
  • -
  • Arena3D doesn't open: if the browser blocked the new tab, use the Open Arena3D link in the status note. If Arena3D can't be reached, run NORMA with server.py, or save the Arena3D (.json) and load it in Arena3D.
  • -
  • STRING can't be reached: check the internet connection, and run NORMA with server.py (see Running NORMA locally or on a server) so that requests go through its relay. If the server is running but requests fail, set Connect to Through this server to see its error. If a request takes too long, lower Interactors to add or try again later.
  • -
  • STRING found none of the names: check the spelling and the organism; a gene name from one species often isn't found in another.
  • -
  • A STRING grouping is missing: that collection had no terms passing the FDR and group-size limits, or isn't available for the organism. The notes list what STRING returned; raise the FDR limit or lower Smallest group.
  • -
  • A file was read as the wrong type: remove it, pick its type in the upload box and upload it again.
  • -
  • The view is slow: try straight edges instead of bundled or curved ones, turn off group shading or use the hull style instead of fog, merge parallel edges, or deactivate groups you don't need.
  • -
  • Colors look alike: switch to a colorblind-safe palette, or click a swatch to set a color yourself.
  • -
  • No arrows are shown: the edges aren't marked as directed, or Edges → Direction is set to undirected. Choose Treat all edges as directed, or upload the file again as directed or with a Direction column.
  • -
  • B→A disappeared after uploading: the file was read as undirected, where A→B and B→A are the same connection. Delete it (its ✕ in Files) and upload it again with Networks: directed.
  • -
  • My parallel edges became one edge: the network file has no Type column, or Edges → Parallel edges is set to merge. Add a Type column, show the networks together as separate files, or switch back to drawing each channel.
  • -
  • A function ignores some nodes or edges: layouts, sizes, statistics, Network Comparison and exports work on the ticked groups and channels only. Tick them again, or untick Only ticked groups and channels before exporting.
  • -
  • An exported image is smaller than chosen: the browser can't draw images beyond about 16,000 pixels per side; the dialog says when the size was reduced. Choose a lower resolution or export only what is on screen, or use SVG, which has no size limit.
  • -
  • The 3D picture is slow: turn off group shading or shaded spheres, lower Depth fading, hide labels, or untick groups and channels you don't need. Large networks are drawn simplified while they move.
  • -
  • The 3D network looks flat: the layout is Same as 2D (flat), or the camera is facing it from the front. Run another 3D layout or choose View from → Tilted.
  • -
  • A few edges are missing: edges between two nodes that sit on top of each other can't be drawn. Run a layout again or use Spread.
  • -
- -
-
-
- -
-
-
- -
-

NORMA 3.0 The Network Makeup Artist

-

A browser application for visualising and analysing biological networks together with their functional annotations: how genes, proteins or other entities that share a pathway, complex, GO term or community sit in a network, and how several networks compare.

-
-
- -
-

How to cite

-

If NORMA helps your work, please cite:

-
    -
  • Karatzas E, Koutrouli M, Baltoumas FA, Papanikolopoulou K, Bouyioukos C, Pavlopoulos GA. The network makeup artist (NORMA-2.0): distinguishing annotated groups in a network using innovative layout strategies. Bioinformatics Advances. 2022;2(1):vbac036. PMID: 36699373, doi:10.1093/bioadv/vbac036 (NORMA-2.0, group-aware layout strategies)
  • -
  • Koutrouli M, Karatzas E, Papanikolopoulou K, Pavlopoulos GA. NORMA: The Network Makeup Artist, a web tool for network annotation visualization. Genomics, Proteomics & Bioinformatics. 2022;20(3):578–586. doi:10.1016/j.gpb.2021.02.005 (the original NORMA application)
  • -
-

Please also cite the databases and methods you use; see Resources and methods.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Bubble-set groups -outlines that hug only the members - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -−2 -0 -2 -log2FC -Numeric values -color scales, sizes, legends, steps - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Ribosome - -Proteasome - -Spliceosome - -Glycolysis - -−log10 FDR -Communities and enrichment -Leiden, MCL, Walktrap; term tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -3D network -rotate, tilt and zoom freely - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -A - -B - -C -shared -edges -Compare networks -UpSet plots, Venn diagrams, statistics -
- -

What NORMA does

-

Networks of interacting molecules are easier to understand when you can see which nodes belong together. NORMA takes a network and one or more annotations (groupings of nodes, such as pathways, complexes, GO terms, tissues or communities) and shows them together: groups are shaded with hulls, bubble sets or fog clouds, nodes in several groups become pie charts, and layouts can arrange the network by its connections or by its groups, so that groups separate cleanly.

-

On top of the picture, NORMA adds node colors from expression data or numeric values, node sizes from topology, several edge channels between the same nodes (evidence types, interaction types or whole networks), directed edges, a 3D view, network statistics, community detection, group enrichment and a side-by-side comparison of up to ten networks. Networks can be uploaded as simple tab-separated files, fetched from six public databases, and exported as images, as files for Cytoscape, Gephi and yEd, or straight into Arena3D.

-

Everything runs in your browser, from a single file (norma.html); a small optional server (server.py) relays database requests. NORMA 3.0 continues NORMA and NORMA-2.0, which introduced group-aware layout strategies.

- -

Highlights

-
-
Groups made visibleConvex hulls, bubble sets and fog clouds; pie-chart nodes; one shape per group; group colors and descriptions.
-
Group-aware layoutsThe NORMA-2.0 strategies, groups as blocks, clusters or layers, plus force-directed, Kamada–Kawai and stress majorization; a separation score and benchmark.
-
Multi-edge and directedSeveral channels per node pair, merged, fanned, bundled or labelled; arrows and direction-aware statistics.
-
Values and legendsExpression colors, numeric color scales, time-series steps, node sizes and exportable legends.
-
AnalysisNetwork profiles, five community detection methods, group statistics, enrichment, neighbourhoods and shortest paths.
-
ComparisonUp to ten networks with Venn and UpSet plots, similarity and degree statistics, drawn as one network or sent to Arena3D.
-
Database importersSTRING, Reactome, OmniPath, NDEx, IntAct and the Gene Ontology, with their groupings.
-
3D and exportA 3D page, high-resolution PNG, SVG and PDF, NORMA files, GraphML, GEXF, SIF, Cytoscape, Arena3D and sessions.
-
- -

Data sources

-

NORMA reads your own files and the resources below. Nothing is sent to a resource until you use its importer; see Database importers in Help for how each one works.

- - - - - - - - - - - -
ResourceNetworkGroupingsWhat NORMA fetches
STRINGYesYesProtein association networks with evidence channels; enriched GO, KEGG, Reactome, UniProt, domain, disease and tissue terms
ReactomeYesYesThe molecules of a pathway's reactions, linked when they share a reaction; sub-pathways and reactions as groups
OmniPathYesYesSigned, directed signalling, TF–target and ligand–receptor interactions; complexes, intercellular roles, pathway annotations
NDExYesLimitedPublic networks with their layout; groups from node attributes
IntActYesNoExperimentally detected interactions with MI-scores and interaction types
Gene OntologyLimited (GO-CAM)YesCausal GO-CAM models as networks; GO terms (QuickGO) as groups for any network
Arena3DExportViews and comparisons as multilayer networks, one layer per group or network
-

None of these services needs an account for public data. Please follow each resource's terms of use and cite it when you use its data (see Resources and methods).

- -

What's new in NORMA 3.0

-
-

Everything works on the groups and channels you tick

-

This is the main difference from earlier versions of NORMA. Untick groups under Node groups or channels under Edge channels, and every function works on the part that stays ticked: nodes in at least one ticked group, and edges of ticked channels between those nodes. Nothing has to be re-uploaded or filtered by hand.

-
    -
  • Layouts, both by connections and by groups (including the NORMA-2.0 strategies), arrange only the shown nodes using only the ticked channels; hidden nodes keep their places.
  • -
  • Node sizes by degree, betweenness, closeness or clustering, and the statistics in a node's details panel, are computed on the shown part.
  • -
  • Group shading, node shapes and pie slices follow the ticked groups: a node in two groups turns into a single-group shape when the other group is unticked.
  • -
  • Edge bundling, edge merging and edge labels use only the ticked channels.
  • -
  • Search finds only shown nodes, and Spread and Fit to screen work on what you see.
  • -
  • The Network Profiler and Network Comparison pages analyse the shown part of each view.
  • -
  • Exports (NORMA files and JSON) save the shown part, unless you untick Only ticked groups and channels.
  • -
-

So you can, for example, tick two KEGG pathways and one evidence channel, lay them out, size nodes by betweenness within that sub-network, profile it, compare it with another view, and export exactly that.

-
-
-

Multi-edge networks are supported

-

Previous versions of NORMA did not support multi-edge graphs. A network could hold only one connection per pair of nodes: repeated connections were removed automatically when a file was loaded, so a pair supported by, say, both experiments and text mining ended up as one plain edge.

-

NORMA 3.0 keeps every connection. The same pair of nodes can be linked by several edges, each belonging to a channel (an interaction type, an evidence source or a whole network):

-
    -
  • An optional Type column in a network file keeps one edge per pair and type; only exact repeats of the same pair and type are removed (see Network file).
  • -
  • Ticking several network files overlays them, with each network as its own channel (see Several networks and multi-edges).
  • -
  • JSON files and the built-in demos can carry any number of edges per pair, each with its own type, weight, direction and attributes.
  • -
  • Channels can be colored, ticked and unticked one by one; parallel edges can be fanned apart, merged per pair, bundled or labelled; and layouts, node sizes, the Network Profiler and Network Comparison can be limited to the ticked channels.
  • -
-
- -
-

Directed networks

-

Edges can point from a source to a target, and each view can handle its network as directed or undirected. Arrows show the direction; a Direction column in network files, a directed/undirected choice when uploading, and a directed field in JSON bring direction in; in- and out-degree, direction-following betweenness and closeness, reciprocity, strongly connected components and directed path lengths analyse it. See Directed and undirected networks.

-
- -
-

A 3D Network page

-

The new 3D Network tab shows the current view in three dimensions: the same groups, colors, pies, shapes, labels, arrows and group shading, with its own 3D layouts (weighted force-directed, groups as 3D clusters, groups on stacked layers, height by hierarchy or degree), rotate, pan and zoom controls, automatic rotation and 3D image export. See 3D Network.

-
-
-

Import networks and groupings from STRING

-

STRING in the Database importers tab fetches the interaction network of one or more proteins for any organism, with control over the number of interactors, confidence, network type and evidence channels, and turns enriched GO terms, KEGG and Reactome pathways, UniProt keywords, protein domains, diseases, tissues and more into groupings you can switch between. See Importing from STRING.

-
- -

Also new

-
    -
  • Deleting files and views: views, networks, groupings and expression files can be removed completely with Delete… next to Rename, the ✕ next to the Grouping menu, or the ✕ in Files.
  • -
  • API: other applications can open NORMA with their networks and groups by REST, links or postMessage; see the API tab.
  • -
  • Welcome tab and tidier menus: a permanent Welcome tab; every sidebar section folds open and closed from its title; separators group related options; the three NORMA JSON files are named view, session and settings files.
  • -
  • Database importers: besides STRING, Reactome, OmniPath, NDEx, IntAct and the Gene Ontology (GO-CAM networks and GO term groupings), in a tab of their own; exports moved to an Export tab.
  • -
  • Welcome page: NORMA starts on a page that shows what it does, with quick ways to open an example, fetch from STRING or upload files.
  • -
  • Classic layouts: Kamada–Kawai and stress majorization, for connections, as group strategies and inside groups.
  • -
  • Group analysis: group statistics and the modularity of a grouping, a network of groups, and enrichment of annotation terms in selected nodes or in each group of another annotation.
  • -
  • Comparison as a network: Show as network merges compared networks into one view whose edges say which networks contain them.
  • -
  • Local exploration: a node's neighbourhood or the shortest paths between two nodes, opened as a new view.
  • -
  • Time series and conditions: step through value columns or several numeric files, by hand or as an animation, with the layout fixed.
  • -
  • More export formats and sessions: GraphML, GEXF, SIF and Cytoscape (.cyjs) for Cytoscape, Gephi and yEd, and whole sessions (all views and files) in one file.
  • -
  • Performance: optional WebGL drawing for large networks, and a runtime table of the main steps.
  • -
  • Export to Arena3D: open the current view in Arena3D with one click, each group as its own layer, or save it as an Arena3D file (.json) or table (.txt); Arena3D tables can also be uploaded.
  • -
  • Showcase examples: a directed signalling cascade with simulated fold changes, a simulated healthy-vs-disease pair for Network Comparison, and Arena3D's seven-layer example (see Examples).
  • -
  • Numeric expression values: log2 fold changes, p-values or any numbers in expression files, shown on diverging or sequential color scales and as node size, with an optional legend that can also go into exported images.
  • -
  • More community detection: Leiden, label propagation, Walktrap and Markov clustering next to Louvain in the Network Profiler, each able to become a grouping.
  • -
  • A number for group separation: silhouette width, nodes inside other groups' outlines and outline overlap after every layout, and a layout benchmark that compares all layouts, including the NORMA-2.0 strategies, on the current view.
  • -
  • STRING import: fetch a protein's interaction network from the STRING database for any organism, with control over the number of interactors, confidence and evidence channels, and turn its enriched GO terms, KEGG and Reactome pathways, keywords, domains, diseases, tissues and more into groupings.
  • -
  • A 3D network: the 3D Network page shows the current view in three dimensions, with the same groups, colors, shapes, labels, arrows and shading, its own 3D layouts, and rotate, pan and zoom controls.
  • -
  • Working with several visualizations: views, each with its own settings; a Grouping list in the top bar to regroup a view while keeping positions; undo and redo for every change to a view.
  • -
  • A clearer screen: the View list in the top bar, colored labels saying which network, grouping and colors are shown, node, edge, channel and group counts, and color-coded sidebar sections split into Upload Data, Database importers, Display and Export (see Finding your way around).
  • -
  • Layout: arrangement by connections or by groups, NORMA-2.0's three group strategies, groups as non-overlapping blocks, a weighted Fruchterman–Reingold layout, and a Spread slider.
  • -
  • Groups: Bubble Sets–style contours that hug each group, node shapes per group alongside group colors, a filter for long group lists, pies of up to 16 slices, and group details with shared members.
  • -
  • Labels: adjustable sizes and colors for node and edge labels, edge labels showing channels, weights or any edge attribute, and hiding labels when zoomed out.
  • -
  • Edges: edge bundling, merging of parallel edges, curved fan-out, weight-based thickness and edge opacity.
  • -
  • Exploring: search by name, prefix, suffix or regular expression in a panel that can be minimized; selecting several nodes (shown in yellow); zoom without limits while the network stays in frame.
  • -
  • Data: attributes on nodes, edges and groups (Attributes, JSON format); annotation checks on upload with a corrected annotation to download (Files and formats).
  • -
  • Direction: networks are handled as undirected by default, as in earlier versions of NORMA; directed data can be shown with arrows in one click (see Directed and undirected networks).
  • -
  • Analysis: a Network Profiler with a short description of each statistic, Louvain communities that can be added as an annotation, and Network Comparison of up to ten networks with Venn diagrams, UpSet plots, similarity matrices, degree agreement and topology side by side.
  • -
  • Output: image export in high resolution as PNG, JPEG, WebP, vector SVG or PDF; NORMA files and JSON limited to the ticked groups and channels if you like.
  • -
  • Examples: NORMA's example datasets and built-in demos, with every file downloadable.
  • -
  • A white theme by default, and keyboard shortcuts.
  • -
- - -

Resources and methods

-

NORMA builds on the following software, data resources and published methods. When you use their data or results, please cite them as well.

-

Software

-
    -
  • Cytoscape.js draws the 2D network; statistic definitions follow igraph; color scales follow ColorBrewer and viridis.
  • -
-

Data resources

-
    -
  • STRING: Szklarczyk et al., Nucleic Acids Research 2023;51(D1):D638–D646.
  • -
  • Reactome: Milacic et al., Nucleic Acids Research 2024;52(D1):D672–D678.
  • -
  • OmniPath: Türei et al., Molecular Systems Biology 2021;17:e9923.
  • -
  • NDEx: Pratt et al., Cell Systems 2015;1(4):302–305.
  • -
  • IntAct: del Toro et al., Nucleic Acids Research 2022;50(D1):D648–D653.
  • -
  • Gene Ontology: Gene Ontology Consortium, Genetics 2023;224(1):iyad031.
  • -
  • Arena3D: Karatzas et al., Nucleic Acids Research 2021;49(W1):W36–W45, doi:10.1093/nar/gkab278.
  • -
-

Methods

-
    -
  • Community detection: Louvain (Blondel et al., J Stat Mech 2008:P10008), Leiden (Traag et al., Sci Rep 2019;9:5233), label propagation (Raghavan et al., Phys Rev E 2007;76:036106), Walktrap (Pons and Latapy, J Graph Algorithms Appl 2006;10(2):191–218) and MCL (van Dongen, PhD thesis, University of Utrecht, 2000).
  • -
  • Layouts: Kamada and Kawai (Inf Process Lett 1989;31(1):7–15); stress majorization (Gansner, Koren and North, Graph Drawing 2004, LNCS 3383:239–250) with a pivot-MDS start (Brandes and Pich, Graph Drawing 2006, LNCS 4372:42–53).
  • -
  • Edge bundling: Holten and van Wijk's force-directed method (Computer Graphics Forum 2009;28(3):983–990, doi:10.1111/j.1467-8659.2009.01450.x).
  • -
  • Statistics: enrichment p-values adjusted as in Benjamini and Hochberg (J R Stat Soc B 1995;57(1):289–300); group separation by the silhouette width of Rousseeuw (J Comput Appl Math 1987;20:53–65).
  • -
- -

Team, access and maintenance

-
    -
  • Team: NORMA is developed by the Pavlopoulos Lab (GitHub). Source code: github.com/PavlopoulosLab/NORMA.
  • -
  • Access: NORMA is free and open to all users. It needs no login, registration or e-mail address, and no installation: it runs in the web browser.
  • -
  • Licence: NORMA is released under the licence shown on the Welcome page (MIT by default), which allows free use, including non-commercial and commercial use. The data resources have their own terms of use.
  • -
-
    - -

    Privacy, data and cookies

    -
      -
    • Your data stays private. Files you upload, the networks you build, views and sessions are processed in your own browser and are not uploaded anywhere. Nobody else can see them. Deleting a file removes it and everything read from it; closing the browser tab removes everything.
    • -
    • What leaves your browser: only what you send yourself: the names you search for with a database importer (to that database), a network you open in Arena3D (to Arena3D), and a payload you send to the NORMA REST API (to that server). On a NORMA server, these requests pass through the server, which does not store them; API payloads are kept in the server's memory only for the time shown in the API tab, and only someone with the link can open them.
    • -
    • No cookies, no tracking. NORMA sets no cookies and uses no analytics, advertising or third-party tracking. The only thing it stores is the WebGL drawing preference, in your browser's local storage, when you change it. Public NORMA servers log requests without IP addresses by default.
    • -
    • Third-party content: NORMA loads no fonts, scripts or images from other sites (a copy of Cytoscape.js is bundled).
    • -
    -
    -
    - - -
    - - - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index af73f76..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,3072 +0,0 @@ -{ - "name": "norma", - "version": "3.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "norma", - "version": "3.0.0", - "dependencies": { - "cytoscape": "^3.34.3" - }, - "devDependencies": { - "@playwright/test": "^1.61.1", - "@types/cytoscape": "^3.31.0", - "@types/node": "^26.2.0", - "eslint": "^10.6.0", - "jsdom": "^30.0.1", - "prettier": "^3.9.4", - "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", - "vite": "^8.1.3", - "vite-plugin-singlefile": "^2.3.3", - "vitest": "^5.0.1" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", - "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.3.0", - "@csstools/css-color-parser": "^4.1.10", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", - "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.5.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", - "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", - "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", - "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.4.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", - "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", - "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", - "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@playwright/test": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", - "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.63.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", - "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", - "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", - "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", - "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", - "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", - "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", - "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", - "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", - "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", - "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", - "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", - "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", - "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", - "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", - "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/cytoscape": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.31.0.tgz", - "integrity": "sha512-EXHOHxqQjGxLDEh5cP4te6J0bi7LbCzmZkzsR6f703igUac8UGMdEohMyU3GHAayCTZrLQOMnaE/lqB2Ekh8Ww==", - "deprecated": "This is a stub types definition. cytoscape provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "dependencies": { - "cytoscape": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz", - "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", - "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/type-utils": "8.70.0", - "@typescript-eslint/utils": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.70.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", - "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", - "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", - "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.70.0", - "@typescript-eslint/types": "^8.70.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", - "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", - "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", - "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/utils": "8.70.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", - "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", - "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.70.0", - "@typescript-eslint/tsconfig-utils": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", - "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", - "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.70.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/mocker": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", - "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.1", - "estree-walker": "^3.0.3", - "magic-string": "^1.2.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/spy": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", - "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/bidi-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", - "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", - "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.2.0", - "@cacheable/utils": "^2.5.0", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/cytoscape": { - "version": "3.34.3", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", - "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", - "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "11.1.5 || >11.1.6 <12", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", - "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^6.1.23" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "6.1.23", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", - "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cacheable": "^2.5.0", - "flatted": "^3.4.2", - "hookified": "^1.15.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", - "@exodus/bytes": "^1.15.1", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.2", - "undici": "^8.9.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "canvas": "^3.2.3" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", - "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/playwright": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", - "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.63.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright-core": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", - "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", - "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.150.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", - "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tldts": { - "version": "7.4.13", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", - "integrity": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.13" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.13", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.13.tgz", - "integrity": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", - "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.70.0", - "@typescript-eslint/parser": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/utils": "8.70.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", - "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.6", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-singlefile": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", - "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">18.0.0" - }, - "peerDependencies": { - "rollup": "^4.59.0", - "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", - "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.1", - "chai": "^6.2.2", - "es-module-lexer": "^2.3.2", - "expect-type": "^1.4.0", - "magic-string": "^1.2.3", - "obug": "^2.1.4", - "picomatch": "^4.0.7", - "std-env": "^4.2.0", - "tinybench": "6.1.4", - "tinyexec": "1.3.0", - "tinyglobby": "^0.2.17", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^22.12.0 || ^24.0.0 || >=26.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.1", - "@vitest/browser-preview": "5.0.1", - "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.1", - "@vitest/coverage-v8": "5.0.1", - "@vitest/ui": "5.0.1", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", - "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^22.14.0 || >=24.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 25b7615..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "norma", - "private": true, - "version": "3.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc --noEmit && vite build", - "preview": "vite preview", - "lint": "eslint src e2e", - "format": "prettier --write src e2e", - "test": "vitest", - "test:e2e": "playwright test" - }, - "dependencies": { - "cytoscape": "^3.34.3" - }, - "devDependencies": { - "@playwright/test": "^1.61.1", - "@types/cytoscape": "^3.31.0", - "@types/node": "^26.2.0", - "eslint": "^10.6.0", - "jsdom": "^30.0.1", - "prettier": "^3.9.4", - "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", - "vite": "^8.1.3", - "vite-plugin-singlefile": "^2.3.3", - "vitest": "^5.0.1" - } -} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index f52e8cb..0000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' - -// E2E runs against server.py serving the page (relays off, API on). -export default defineConfig({ - testDir: './e2e', - fullyParallel: false, - retries: process.env.CI ? 2 : 0, - timeout: 60_000, - reporter: [['list']], - use: { - baseURL: 'http://127.0.0.1:8765', - trace: 'on-first-retry', - }, - projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], - webServer: { - command: - 'npm run build && python3 ../backend/server.py --port 8765 --no-open --no-relays --access-log off', - url: 'http://127.0.0.1:8765/api/health', - reuseExistingServer: !process.env.CI, - timeout: 30_000, - }, -}) diff --git a/frontend/public/assets/apple-touch-icon.png b/frontend/public/assets/apple-touch-icon.png deleted file mode 100644 index 9817c97..0000000 Binary files a/frontend/public/assets/apple-touch-icon.png and /dev/null differ diff --git a/frontend/public/assets/banner.svg b/frontend/public/assets/banner.svg deleted file mode 100644 index 48591ad..0000000 --- a/frontend/public/assets/banner.svg +++ /dev/null @@ -1,374 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Bubble-set groups -outlines that hug only the members - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -−2 -0 -2 -log2FC -Numeric values -color scales, sizes, legends, steps - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Ribosome - -Proteasome - -Spliceosome - -Glycolysis - -−log10 FDR -Communities and enrichment -Leiden, MCL, Walktrap; term tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -3D network -rotate, tilt and zoom freely - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -A - -B - -C -shared -edges -Compare networks -UpSet plots, Venn diagrams, statistics - \ No newline at end of file diff --git a/frontend/public/assets/favicon-32.png b/frontend/public/assets/favicon-32.png deleted file mode 100644 index a306295..0000000 Binary files a/frontend/public/assets/favicon-32.png and /dev/null differ diff --git a/frontend/public/assets/favicon.ico b/frontend/public/assets/favicon.ico deleted file mode 100644 index 6343766..0000000 Binary files a/frontend/public/assets/favicon.ico and /dev/null differ diff --git a/frontend/public/assets/favicon.svg b/frontend/public/assets/favicon.svg deleted file mode 100644 index da43f50..0000000 --- a/frontend/public/assets/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/frontend/public/assets/logo.svg b/frontend/public/assets/logo.svg deleted file mode 100644 index 81a4b8b..0000000 --- a/frontend/public/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ -NORMA \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html deleted file mode 100644 index 25312bf..0000000 --- a/frontend/public/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - -NORMA - - - - - -

    Open NORMA

    - - diff --git a/frontend/public/norma-config.js b/frontend/public/norma-config.js deleted file mode 100644 index 61181b0..0000000 --- a/frontend/public/norma-config.js +++ /dev/null @@ -1,22 +0,0 @@ -// NORMA page settings for static hosting or opening norma.html from disk. -// When NORMA runs with server.py, the server replaces this file with the -// settings from norma.config.json. Every field is optional. -window.NORMA_CONFIG = { - mode: "local", - site: { - name: "NORMA", - institution: "", - contactName: "Pavlopoulos Lab", - contactEmail: "", - contactUrl: "https://github.com/PavlopoulosLab/NORMA/issues", - sourceUrl: "https://github.com/PavlopoulosLab/NORMA", - licenceName: "MIT License", - licenceUrl: "https://opensource.org/licenses/MIT", - privacyUrl: "", - imprintUrl: "", - maintainedUntil: "", - notice: "", - testedBrowsers: [] - }, - app: { maxNodes: 10000, theme: "white", startTab: "welcome", cdnFallback: true } -}; diff --git a/frontend/src/api/tester.ts b/frontend/src/api/tester.ts deleted file mode 100644 index 64c62c1..0000000 --- a/frontend/src/api/tester.ts +++ /dev/null @@ -1,639 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import apiClientTemplate from '../../../examples/norma_api_client.py?raw' -import { - MAX_NETWORK_NODES, - escapeHtml, - getUsedGroups, - refreshNodeVisual, - sanitizeColor, -} from '../network_state' -import { NORMA_CFG } from '../config' -import { S } from '../state' -import { - activeView, - captureActiveView, - closeActiveView, - nameForSelection, - profilerState, - renderProfilerNetworkList, - renderProfilerResults, - renderViewBar, - restoreView, - uniqueViewName, - views, -} from '../profiler' -import { applyGroupVisibility, buildGroupLegend, buildLegend, getUsedTypes } from '../hulls' -import { applyTypeVisibility } from '../parallel_edges' -import { b64urlEncode, normaApiLoad } from './wiring' -import { compareState, renderCompareList } from '../label_colors' -import { currentTab } from '../wiring' -import { cy } from '../cy' -import { dataVersion, setHistoryBaseline, updateUndoButtons } from '../demo_downloads' -import { - downloadText, - libEntry, - libSelection, - normaLibrary, - plural, - renderLibraryLists, - setStatus, -} from '../layouts/controls' -import { refreshEnrichmentChoices, toast } from '../enrichment' -import { refreshLibraryView } from '../library' -import { renderGroupingSelect, updateContextInfo } from '../recording' -import { updateEmptyState } from '../welcome' - -/* ---------- the API tab's tester ---------- */ -const API_EXAMPLE = { - name: 'API example', - edges: [ - { source: 'TP53', target: 'MDM2', type: 'binding', weight: 0.9 }, - { source: 'MDM2', target: 'TP53', type: 'ubiquitination', directed: true }, - { source: 'TP53', target: 'CDKN1A', type: 'expression', directed: true }, - { source: 'CDKN1A', target: 'CDK2', type: 'inhibition', directed: true }, - { source: 'CDK2', target: 'CCNE1', type: 'binding' }, - { source: 'ATM', target: 'TP53', type: 'phosphorylation', directed: true }, - { source: 'ATM', target: 'CHEK2', type: 'phosphorylation', directed: true }, - { source: 'CHEK2', target: 'TP53', type: 'phosphorylation', directed: true }, - ], - groups: [ - { name: 'DNA damage sensing', members: ['ATM', 'CHEK2'], color: '#8b5cf6' }, - { - name: 'p53 core', - members: ['TP53', 'MDM2'], - color: '#f59e0b', - description: 'p53 and its E3 ligase', - }, - { name: 'Cell cycle', members: ['CDKN1A', 'CDK2', 'CCNE1', 'TP53'] }, - ], - expression: { TP53: 1.8, MDM2: 1.2, CDKN1A: 2.6, CDK2: -1.1, CCNE1: -0.7, ATM: 0.3, CHEK2: 0.4 }, - settings: { - edgeDirection: 'data', - edgeCurveStyle: 'bezier', - showGroupHulls: true, - hullStyle: 'bubble', - legendShow: true, - layout: 'fr', - }, -} - -function apiPayloadFromBox() { - const text = document.getElementById('apiPayload').value - try { - return JSON.parse(text) - } catch (e) { - throw new Error(`The payload is not valid JSON: ${e.message}`) - } -} - -function apiBase() { - return location.href.replace(/[?#].*$/, '') -} - -/* ============================================================ - SITE INFORMATION (licence, access, privacy, contact) - Shown on the Welcome page, in About and as an optional notice, from - the settings (see Help: Running NORMA locally or on a server). - ============================================================ */ -function siteLink(url, text) { - return url - ? `${escapeHtml(text)}` - : escapeHtml(text) -} - -export function renderSiteInfo() { - const S = NORMA_CFG.site - const hosted = NORMA_CFG.mode === 'hosted' - const contact = S.contactEmail - ? `${escapeHtml(S.contactEmail)}` - : siteLink(S.contactUrl, S.contactName || 'contact') - const foot = document.getElementById('siteFooter') - if (foot) { - foot.innerHTML = [ - `Free and open to all users. No login, registration or e-mail address is needed.`, - `No cookies and no tracking; your data stays in your browser (privacy).`, - `Licence: ${siteLink(S.licenceUrl, S.licenceName || 'MIT License')}`, - `${siteLink(S.sourceUrl, 'Source code')}`, - `Contact: ${contact}`, - `Programmatic access: Python template`, - S.privacyUrl ? `${siteLink(S.privacyUrl, 'Privacy policy')}` : '', - S.imprintUrl ? `${siteLink(S.imprintUrl, 'Imprint')}` : '', - ] - .filter(Boolean) - .join('·') - } - const box = document.getElementById('aboutSiteInfo') - if (box) { - const browsers = - Array.isArray(S.testedBrowsers) && S.testedBrowsers.length ? S.testedBrowsers.join(', ') : '' - box.innerHTML = ` -
  • Running here: ${hosted ? `a public NORMA server${NORMA_CFG.publicUrl ? ` at ${siteLink(NORMA_CFG.publicUrl, NORMA_CFG.publicUrl)}` : ''}` : NORMA_CFG.served ? 'a NORMA copy on this computer or network' : 'a NORMA file opened on this computer'}${S.institution ? `, provided by ${escapeHtml(S.institution)}` : ''}. NORMA ${escapeHtml(NORMA_CFG.version || '3.0')}.
  • -
  • Contact: ${contact}${S.contactEmail && S.contactUrl ? ` · ${siteLink(S.contactUrl, 'issue tracker')}` : ''}.
  • -
  • Maintenance: ${S.maintainedUntil ? `this server is maintained at least until ${escapeHtml(S.maintainedUntil)}.` : 'NORMA is maintained by the Pavlopoulos Lab; the source code stays available in its public repository.'}
  • -
  • Browsers: NORMA is made for current versions of Chrome, Edge, Firefox and Safari on Windows, macOS and Linux, and needs no plug-ins or installation.${browsers ? ` Tested on this server with ${escapeHtml(browsers)}.` : ''}
  • ` - } - if (S.notice) { - const bar = document.getElementById('siteNotice') - bar.querySelector('span').textContent = S.notice - bar.hidden = false - } - document.querySelectorAll('[data-download-template]').forEach((a) => - a.addEventListener('click', (e) => { - e.preventDefault() - downloadApiTemplate() - }) - ) - const wf = document.querySelector('.welcome-foot') - if (wf) - wf.textContent = `NORMA ${NORMA_CFG.version || '3.0'} runs in your browser: your files stay on this computer, except what you send to online databases or Arena3D yourself. Networks of up to ${MAX_NETWORK_NODES.toLocaleString('en-US')} nodes.` - // REST API availability - const restBtn = document.getElementById('btnApiServer') - const setRest = (on) => { - restBtn.disabled = !on - restBtn.title = on - ? '' - : 'The REST API needs NORMA to run with server.py (with the API turned on).' - } - if (NORMA_CFG.features.restApi === false) setRest(false) - else if (!NORMA_CFG.served) setRest(false) - else if (NORMA_CFG.features.restApi !== true) { - fetch('api/health') - .then((r) => (r.ok ? r.json() : null)) - .then((j) => setRest(!!(j && j.status === 'ok'))) - .catch(() => setRest(false)) - } -} - -function downloadApiTemplate() { - const el = { textContent: apiClientTemplate } - let text = el ? el.textContent : '' - const base = - NORMA_CFG.publicUrl || - (NORMA_CFG.served - ? location.href.replace(/[?#].*$/, '').replace(/norma\.html$|index\.html$/, '') - : '') - if (base) - text = text.replace('DEFAULT_SERVER = "http://localhost:8000/"', `DEFAULT_SERVER = "${base}"`) - downloadText('norma_api_client.py', text) -} - -/* ============================================================ - DELETING FILES - A network, annotation or expression file can be deleted completely: - it leaves Files, every view that shows it is updated (views left with - no network become empty), undo can't bring it back, and results of - the Network Profiler and Network Comparison that used it are removed. - ============================================================ */ -const KIND_WORD = { network: 'network', annotation: 'annotation', colors: 'expression file' } - -const SEL_FIELD = { annotation: 'annotation', colors: 'colors' } - -function libViewUses(lv, kind, id) { - if (!lv) return false - if (kind === 'network') - return String(lv.nets || '') - .split('|') - .includes(id) - return lv[SEL_FIELD[kind]] === id -} - -// views that show a file (the current one as it is now, others as saved) -function viewsShowing(kind, id) { - return views.filter((v) => - libViewUses(v.id === S.activeViewId ? S.currentLibView : v.state && v.state.libView, kind, id) - ) -} - -export let nextLoadPositions = null - -const KIND_TITLE = { network: 'network file', annotation: 'grouping', colors: 'expression file' } - -// items: [{ kind, id }] files; opts.view: also offer to remove this view -export function askDelete(items, opts = {}) { - const files = items - .map(({ kind, id }) => ({ kind, id, entry: libEntry(kind, id) })) - .filter((x) => x.entry) - const choices = [ - ...(opts.view ? [{ type: 'view', view: opts.view }] : []), - ...files.map((f) => ({ type: 'file', ...f })), - ] - if (!choices.length) return - const dlg = document.getElementById('deleteDialog') - const box = document.getElementById('deleteChoices') - const single = choices.length === 1 - const label = (c) => - c.type === 'view' ? `the view "${c.view.name}"` : `the ${KIND_TITLE[c.kind]} "${c.entry.name}"` - document.getElementById('deleteTitle').textContent = single - ? `Are you sure you want to completely remove ${label(choices[0])}?` - : opts.all - ? `Are you sure you want to completely remove all ${plural(files.length, 'file')}?` - : 'Are you sure you want to completely remove these?' - box.innerHTML = '' - box.hidden = single || opts.all - if (!box.hidden) { - choices.forEach((c, i) => { - const row = document.createElement('label') - const cb = document.createElement('input') - cb.type = 'checkbox' - cb.checked = true - cb.dataset.index = i - const others = - c.type === 'file' - ? viewsShowing(c.kind, c.id).filter((v) => !opts.view || v.id !== opts.view.id) - : [] - const note = - c.type === 'view' - ? 'closes the view; its display settings and positions are lost' - : others.length - ? `also shown in ${others.map((v) => `"${v.name}"`).join(', ')}, which will be updated` - : 'removed from Files and every menu' - const text = document.createElement('span') - text.innerHTML = `${escapeHtml( - label(c) - .replace(/^the /, '') - .replace(/^./, (ch) => ch.toUpperCase()) - )}${escapeHtml(note)}` - row.append(cb, text) - box.appendChild(row) - cb.addEventListener('change', update) - }) - } - const confirm = document.getElementById('btnDeleteConfirm') - function picked() { - if (box.hidden) return choices - return [...box.querySelectorAll('input')] - .filter((x) => x.checked) - .map((x) => choices[+x.dataset.index]) - } - function update() { - const p = picked() - const pf = p.filter((c) => c.type === 'file') - const shown = new Map() - pf.forEach(({ kind, id }) => viewsShowing(kind, id).forEach((v) => shown.set(v.id, v))) - if (opts.view && p.some((c) => c.type === 'view')) shown.delete(opts.view.id) - const lines = [] - if (pf.length) - lines.push( - `${pf.length === 1 ? 'The file' : 'The files'} will be removed from Files, from the Grouping menu and every other list, together with everything NORMA read from ${pf.length === 1 ? 'it' : 'them'}.` - ) - if (shown.size) { - lines.push( - `${shown.size === 1 ? 'This view shows' : 'These views show'} ${pf.length === 1 ? 'it' : 'them'} and will be updated: ${[...shown.values()].map((v) => `"${v.name}"`).join(', ')}. ${pf.some((c) => c.kind === 'network') ? 'A view left without networks becomes empty; other views keep their remaining networks and node positions.' : 'They keep their networks and node positions but lose these groups or colors.'}` - ) - } - if (p.some((c) => c.type === 'view')) - lines.push( - views.length === 1 - ? 'This is the only view; a new empty view takes its place.' - : 'The view is closed and the previous view is shown.' - ) - lines.push('This cannot be undone. Your files on disk are not touched.') - document.getElementById('deleteText').innerHTML = lines - .map((t) => `

    ${escapeHtml(t)}

    `) - .join('') - confirm.disabled = !p.length - confirm.textContent = p.length > 1 ? `Yes, remove ${p.length}` : 'Yes, remove' - } - update() - confirm.onclick = () => { - const p = picked() - if (!p.length) return - closeDeleteDialog() - const pf = p.filter((c) => c.type === 'file').map(({ kind, id }) => ({ kind, id })) - const closeView = p.some((c) => c.type === 'view') && opts.view - if (pf.length) deleteLibEntries(pf) - if (closeView) { - if (S.activeViewId !== closeView.id && views.some((v) => v.id === closeView.id)) { - captureActiveView() - S.activeViewId = closeView.id - } - const name = closeView.name - closeActiveView() - renderLibraryLists() - renderGroupingSelect() - updateContextInfo() - updateEmptyState() - toast(`Removed the view "${name}"${pf.length ? ` and ${plural(pf.length, 'file')}` : ''}.`) - } - } - if (typeof dlg.showModal === 'function') dlg.showModal() - else dlg.setAttribute('open', '') - document.getElementById('btnDeleteCancel').focus() -} - -function closeDeleteDialog() { - const dlg = document.getElementById('deleteDialog') - if (typeof dlg.close === 'function' && dlg.open) dlg.close() - else dlg.removeAttribute('open') -} - -export function deleteLibEntries(items) { - const entries = items - .map(({ kind, id }) => ({ kind, id, entry: libEntry(kind, id) })) - .filter((x) => x.entry) - if (!entries.length) return - captureActiveView() - const affected = new Set() - const lostNetwork = new Set() - entries.forEach(({ kind, id }) => { - views.forEach((v) => { - const sel = v.selection || (v.selection = { networks: [], annotation: '', colors: '' }) - if (kind === 'network') sel.networks = (sel.networks || []).filter((x) => x !== id) - else if (sel[SEL_FIELD[kind]] === id) sel[SEL_FIELD[kind]] = '' - const lv = v.state && v.state.libView - if (libViewUses(lv, kind, id)) { - affected.add(v) - if (kind === 'network') { - lv.nets = String(lv.nets) - .split('|') - .filter((x) => x !== id) - .join('|') - lostNetwork.add(v) - } else { - lv[SEL_FIELD[kind]] = '' - } - } - }) - normaLibrary[kind] = normaLibrary[kind].filter((e) => e.id !== id) - // groupings fetched for a deleted network stay usable on their own - if (kind === 'network') - normaLibrary.annotation.forEach((e) => { - if (e.forNetwork === id) delete e.forNetwork - }) - }) - affected.forEach((v) => { - v.history = { undo: [], redo: [], committed: null } // no way back to deleted data - const lv = v.state.libView - if (!lv.nets) { - v.data = null - if (v.autoName) v.name = uniqueViewName('Untitled view') - v.state = { config: v.state.config || { ...S.DEFAULT_VIEW_CONFIG } } - v.selection = { networks: [], annotation: '', colors: '' } - v.needsRefresh = null - } else { - // the saved picture still holds the deleted data: drop it now and - // rebuild the view from its remaining files when it is shown - v.data = null - v.needsRefresh = { reload: true } - } - }) - // drop analysis results that used deleted networks or changed views - const deletedNets = new Set(entries.filter((e) => e.kind === 'network').map((e) => e.id)) - const staleView = (id) => [...affected].some((v) => v.id === id) - let droppedResults = 0 - if (profilerState.results.length) { - const keep = profilerState.results.filter( - (r) => - !( - deletedNets.has(r.sourceValue) || - (r.sourceValue === 'view' && affected.has(activeView())) - ) - ) - droppedResults += profilerState.results.length - keep.length - if (keep.length !== profilerState.results.length) { - profilerState.results = keep - if (keep.length) renderProfilerResults(keep) - else { - document.getElementById('profResults').innerHTML = '' - document.getElementById('btnProfileTsv').disabled = true - } - } - } - if (compareState.results) { - const used = compareState.results.some((r) => - (r.sourceValue || '').startsWith('lib:') - ? deletedNets.has(r.sourceValue.slice(4)) - : staleView((r.sourceValue || '').slice(5)) - ) - if (used) { - droppedResults += 1 - compareState.results = null - document.getElementById('cmpResults').innerHTML = '' - ;['btnCompareTsv', 'btnCompareNet'].forEach((b) => { - document.getElementById(b).disabled = true - }) - document.getElementById('cmpArena').hidden = true - } - } - // show the current view again - const act = activeView() - S.historySuspended++ - try { - restoreView(act) - } finally { - S.historySuspended-- - } - S.dataCache = { version: dataVersion, data: act.data } - setHistoryBaseline() - renderViewBar() - updateUndoButtons() - renderLibraryLists() - renderGroupingSelect() - updateContextInfo() - updateEmptyState() - if (typeof refreshEnrichmentChoices === 'function') refreshEnrichmentChoices() - if (currentTab === 'profiler') renderProfilerNetworkList() - if (currentTab === 'compare') renderCompareList() - const names = entries.map((e) => `"${e.entry.name}"`) - const notes = [ - { - level: 'ok', - text: `Deleted ${entries.length === 1 ? names[0] : plural(entries.length, 'file')}${affected.size ? `; updated ${plural(affected.size, 'view')} that showed ${entries.length === 1 ? 'it' : 'them'}` : ''}.`, - }, - ] - if (droppedResults) - notes.push({ - level: 'ok', - text: 'Profiler or comparison results that used the deleted data were removed.', - }) - setStatus('normaStatus', notes) - toast(notes[0].text) -} - -// Brings a view up to date after files it showed were deleted: rebuilds it -// from its remaining files with its saved node positions, group colors and -// ticked groups and channels. -export function refreshAfterDelete(v) { - v.needsRefresh = null - const st = v.state || {} - const lv = st.libView - if (!lv) return - libSelection.networks = new Set( - String(lv.nets) - .split('|') - .filter((id) => libEntry('network', id)) - ) - libSelection.annotation = - lv.annotation && libEntry('annotation', lv.annotation) ? lv.annotation : '' - libSelection.colors = lv.colors && libEntry('colors', lv.colors) ? lv.colors : '' - if (!libSelection.networks.size) return - nextLoadPositions = st.positions && Object.keys(st.positions).length ? st.positions : null - S.currentLibView = null - const status = document.getElementById('normaStatus').innerHTML - try { - refreshLibraryView() - } finally { - nextLoadPositions = null - } - document.getElementById('normaStatus').innerHTML = status - // keep what the view looked like - if (st.groupColors) { - Object.entries(st.groupColors).forEach(([g, c]) => { - if (S.nodeColorMap[g]) { - const sc = sanitizeColor(c) - if (sc) S.nodeColorMap[g] = sc - } - }) - } - const used = new Set(getUsedGroups()) - if (Array.isArray(st.activeGroups)) { - const was = new Set(st.activeGroups) - used.forEach((g) => { - if (g in (st.groupColors || {}) && !was.has(g)) S.activeGroups.delete(g) - }) - } - if (Array.isArray(st.activeTypes)) { - const was = new Set(st.activeTypes) - getUsedTypes().forEach((t) => { - if (!was.has(t) && (st.typeColors || {})[t]) S.activeTypes.delete(t) - }) - } - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - if (typeof applyGroupVisibility === 'function') applyGroupVisibility() - if (typeof applyTypeVisibility === 'function') applyTypeVisibility() - buildGroupLegend() - buildLegend(getUsedTypes()) - if (st.positions && typeof st.zoom === 'number') cy.viewport({ zoom: st.zoom, pan: st.pan }) - v.selection = { - networks: [...libSelection.networks], - annotation: libSelection.annotation, - colors: libSelection.colors, - } - if (v.autoName) { - const base = nameForSelection() - if (base && base !== v.name) v.name = uniqueViewName(base) - renderViewBar() - } - captureActiveView() -} - -// page wiring, run by main.ts in the original order -export function init() { - ;(function setupApiTab() { - const box = document.getElementById('apiPayload') - box.value = JSON.stringify(API_EXAMPLE, null, 2) - document.getElementById('btnApiReset').addEventListener('click', () => { - box.value = JSON.stringify(API_EXAMPLE, null, 2) - }) - document.getElementById('btnApiLoad').addEventListener('click', async () => { - try { - await normaApiLoad(apiPayloadFromBox(), 'the API tab') - } catch (err) { - setStatus('apiStatus', [{ level: 'error', text: err.message }]) - } - }) - document.getElementById('btnApiLink').addEventListener('click', async () => { - try { - const url = `${apiBase()}#json=${b64urlEncode(JSON.stringify(apiPayloadFromBox()))}` - document.getElementById('apiResultUrl').value = url - const note = - url.length > 8000 - ? ' This link is long; browsers and chat programs may cut it. For larger networks use the server (Send to server) or a data URL.' - : '' - setStatus('apiStatus', [ - { - level: url.length > 8000 ? 'warn' : 'ok', - text: `Made a link of ${url.length.toLocaleString()} characters that opens this payload.${note}`, - }, - ]) - try { - await navigator.clipboard.writeText(url) - } catch (e) {} - } catch (err) { - setStatus('apiStatus', [{ level: 'error', text: err.message }]) - } - }) - document.getElementById('btnApiServer').addEventListener('click', async () => { - try { - const body = JSON.stringify(apiPayloadFromBox()) - const r = await fetch('api/external', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body, - }) - const text = await r.text() - let j - try { - j = JSON.parse(text) - } catch (e) { - throw new Error( - 'This page is not served by server.py, so the REST API is not available here. Run NORMA with server.py, or use Make a link.' - ) - } - if (!r.ok) throw new Error(j.message || `The server answered ${r.status}.`) - document.getElementById('apiResultUrl').value = j.url - setStatus('apiStatus', [ - { - level: 'ok', - text: `The server stored the payload (token ${j.token}, kept for ${j.expiresInHours} hours).`, - action: { label: 'Open link', run: () => window.open(j.url, '_blank') }, - }, - ]) - } catch (err) { - setStatus('apiStatus', [ - { - level: 'error', - text: err.message.includes('fetch') - ? 'This page is not served by server.py, so the REST API is not available here.' - : err.message, - }, - ]) - } - }) - document.getElementById('btnApiCopy').addEventListener('click', async () => { - const v = document.getElementById('apiResultUrl').value - if (!v) return - try { - await navigator.clipboard.writeText(v) - toast('Copied the link.') - } catch (e) { - document.getElementById('apiResultUrl').select() - } - }) - // show the address this copy of NORMA answers on - const served = /^https?:$/.test(location.protocol) - const base = served - ? apiBase().replace(/norma\.html$|index\.html$/, '') - : 'http://localhost:8000/' - document.querySelectorAll('[data-api-base]').forEach((el) => { - el.textContent = base - }) - if (!served) { - document.querySelector('.api-base').innerHTML = - 'This copy of NORMA was opened from a file, so the examples use http://localhost:8000/, where server.py runs by default. Links with #json= and postMessage work from a file too.' - } - document.querySelectorAll('.api-code').forEach((pre) => { - const btn = document.createElement('button') - btn.type = 'button' - btn.className = 'api-copy' - btn.textContent = 'Copy' - btn.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(pre.innerText) - btn.textContent = 'Copied' - setTimeout(() => { - btn.textContent = 'Copy' - }, 1200) - } catch (e) {} - }) - pre.parentElement.insertBefore(btn, pre) - }) - })() - - document.getElementById('siteNoticeClose').addEventListener('click', () => { - document.getElementById('siteNotice').hidden = true - }) - - document.getElementById('btnDeleteCancel').addEventListener('click', closeDeleteDialog) -} diff --git a/frontend/src/api/wiring.ts b/frontend/src/api/wiring.ts deleted file mode 100644 index df69f03..0000000 --- a/frontend/src/api/wiring.ts +++ /dev/null @@ -1,453 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { - REACTOME_SPECIES, - dbCancel, - filterGoModels, - goAnnotateView, - goFetchModel, - goLoadModels, - intactFetch, - isSessionObject, - loadSession, - ndexFetch, - ndexSearch, - omnipathFetch, - reactomeFetch, - reactomeSearch, - toast, -} from '../enrichment' -import { UNGROUPED, getUsedGroups } from '../network_state' -import { - addNormaEntry, - libSelection, - plural, - renderLibraryLists, - setLayoutMode, - setStatus, -} from '../layouts/controls' -import { applyConfig, loadData } from '../hulls' -import { applyTheme } from '../themes' -import { cy } from '../cy' -import { openInNewView } from '../profiler' -import { refreshLibraryView } from '../library' -import { runLayout } from '../layouts/run' -import { switchTab } from '../wiring' - -/* ============================================================ - NORMA API - Other applications can open NORMA with their own networks and groups: - 1. REST (with server.py): POST a payload to /api/external; the answer - { token, url } holds a link that opens NORMA with the data loaded - (norma.html?session=TOKEN). - 2. Links: norma.html?data=URL (a payload file), or - norma.html?network=URL&annotation=URL&expression=URL (NORMA files), - or norma.html#json=BASE64URL (a small payload inside the link). - 3. postMessage: a page that opens NORMA in a window or iframe sends - { type: 'norma:load', payload, requestId } and gets back - { type: 'norma:loaded', ok, summary | error, requestId }. - Every route ends in normaApiLoad(payload). - ============================================================ */ -const NORMA_API_VERSION = '1.0' - -const API_MAX_BYTES = 50 * 1024 * 1024 - -function apiTextOf(v) { - if (v == null) return [] - return (Array.isArray(v) ? v : [v]) - .map((x) => (typeof x === 'string' ? { text: x } : x)) - .filter((x) => x && typeof x.text === 'string') -} - -function apiSafe(s) { - return String(s ?? '') - .replace(/[\t\r\n]+/g, ' ') - .trim() -} - -function apiNode(s) { - return apiSafe(s).replace(/,/g, ';') -} - -// { edges: [...] } -> NORMA network text -function apiEdgesText(edges) { - if (!Array.isArray(edges) || !edges.length) - throw new Error('"edges" must be a non-empty list of { source, target } objects.') - const weighted = edges.some((e) => typeof e.weight === 'number') - const typed = edges.some((e) => e.type) - const directed = edges.some((e) => e.directed) - const head = [ - 'Source', - 'Target', - ...(weighted ? ['Weight'] : []), - ...(typed ? ['Type'] : []), - ...(directed ? ['Direction'] : []), - ] - const rows = edges.map((e, i) => { - if (!e || e.source == null || e.target == null) - throw new Error(`Edge ${i + 1} needs a source and a target.`) - return [ - apiNode(e.source), - apiNode(e.target), - ...(weighted ? [typeof e.weight === 'number' ? e.weight : 1] : []), - ...(typed ? [apiSafe(e.type || 'link')] : []), - ...(directed ? [e.directed ? 'directed' : 'undirected'] : []), - ].join('\t') - }) - return [head.join('\t'), ...rows].join('\n') + '\n' -} - -// groups as { name: [members] } or [{ name, members, color, description }] -function apiGroupsText(groups) { - const list = Array.isArray(groups) - ? groups - : Object.entries(groups || {}).map(([name, members]) => ({ name, members })) - const meta = {} - const rows = list.map((g, i) => { - if (!g || !Array.isArray(g.members)) throw new Error(`Group ${i + 1} needs a "members" list.`) - const name = apiSafe(g.name || `Group ${i + 1}`) - const m = {} - if (g.color) m.color = String(g.color) - if (g.description) m.description = String(g.description) - if (Object.keys(m).length) meta[name] = m - return `${name}\t${g.members.map(apiNode).join(',')}` - }) - return { text: rows.join('\n') + '\n', meta } -} - -// expression as { node: '#color' } or { node: number } or { node: { col: number } } -function apiExpressionText(expr) { - const entries = Object.entries(expr || {}) - if (!entries.length) return null - const first = entries[0][1] - if (first && typeof first === 'object') { - const cols = [...new Set(entries.flatMap(([, v]) => Object.keys(v || {})))] - return ( - [ - 'Node\t' + cols.join('\t'), - ...entries.map(([n, v]) => - [apiNode(n), ...cols.map((c) => (v && v[c] != null ? v[c] : 'NA'))].join('\t') - ), - ].join('\n') + '\n' - ) - } - if (typeof first === 'number') - return ( - ['Node\tvalue', ...entries.map(([n, v]) => `${apiNode(n)}\t${v == null ? 'NA' : v}`)].join( - '\n' - ) + '\n' - ) - return entries.map(([n, v]) => `${apiNode(n)}\t${v}`).join('\n') + '\n' -} - -export async function normaApiLoad(payload, origin = 'API') { - if (typeof payload === 'string') { - if (payload.length > API_MAX_BYTES) throw new Error('The payload is larger than 50 MB.') - try { - payload = JSON.parse(payload) - } catch (e) { - throw new Error('The payload is not valid JSON.') - } - } - if (!payload || typeof payload !== 'object' || Array.isArray(payload)) - throw new Error('The payload must be a JSON object.') - // a whole session - if (isSessionObject(payload) || isSessionObject(payload.session)) { - const r = loadSession(isSessionObject(payload) ? payload : payload.session) - switchTab('network') - return { kind: 'session', views: r.views, files: r.files } - } - const name = apiSafe(payload.name) || 'API network' - const settings = - payload.settings && typeof payload.settings === 'object' ? payload.settings : null - const files = payload.files || {} - const netTexts = [...apiTextOf(files.network)] - if (payload.edges) netTexts.push({ text: apiEdgesText(payload.edges), name }) - let summary - if (netTexts.length) { - const directed = payload.directed === true - const nets = netTexts.map((t, i) => - addNormaEntry( - 'network', - t.name || (netTexts.length > 1 ? `${name} ${i + 1}` : name), - t.text, - '', - null, - { directed } - ) - ) - // node attributes - if (Array.isArray(payload.nodes)) { - const attrs = {} - payload.nodes.forEach((n) => { - if (n && n.id != null) { - const { id, ...rest } = n - attrs[apiNode(id)] = rest - } - }) - nets.forEach((e) => { - e.nodeAttrs = attrs - }) - } - const anns = [] - apiTextOf(files.annotation).forEach((t, i) => - anns.push( - addNormaEntry('annotation', t.name || `${name}: grouping ${i + 1}`, t.text, '', null, {}) - ) - ) - if (payload.groups) { - const g = apiGroupsText(payload.groups) - const e = addNormaEntry('annotation', `${name}: groups`, g.text, '', null, {}) - e.groupMeta = g.meta - anns.push(e) - } - ;(Array.isArray(payload.annotations) ? payload.annotations : []).forEach((a, i) => { - const g = apiGroupsText(a.groups || {}) - const e = addNormaEntry( - 'annotation', - `${name}: ${apiSafe(a.name) || `grouping ${i + 1}`}`, - g.text, - '', - null, - {} - ) - e.groupMeta = g.meta - anns.push(e) - }) - let col = null - const exprText = - apiTextOf(files.expression)[0] || - (payload.expression ? { text: apiExpressionText(payload.expression) } : null) - if (exprText && exprText.text) - col = addNormaEntry( - 'colors', - `${name}: ${payload.expression && typeof Object.values(payload.expression)[0] !== 'string' ? 'values' : 'colors'}`, - exprText.text, - '', - null, - {} - ) - openInNewView(name, () => { - libSelection.networks = new Set(nets.map((e) => e.id)) - libSelection.annotation = anns[0] ? anns[0].id : '' - libSelection.colors = col ? col.id : '' - renderLibraryLists() - refreshLibraryView() - }) - summary = { - kind: 'files', - view: name, - networks: nets.length, - groupings: anns.length, - expression: !!col, - notes: [...nets, ...anns, ...(col ? [col] : [])].flatMap((e) => e.parsed.notes || []), - } - } else if (payload.network || Array.isArray(payload.nodes)) { - const data = payload.network || payload - if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) - throw new Error('A NORMA view needs "nodes" and "edges" lists (see the JSON format in Help).') - openInNewView(name, () => loadData(data)) - summary = { kind: 'view', view: name } - } else { - throw new Error( - 'Nothing to show: give "edges", "files.network", "network" (a NORMA view) or a session.' - ) - } - if (settings) { - applyConfig(settings) - if (settings.theme) applyTheme(settings.theme) - if (settings.layout) { - setLayoutMode('connections') - document.getElementById('layoutSelect').value = settings.layout - runLayout(settings.layout) - } - } - switchTab(payload.tab === '3d' ? 'network3d' : 'network') - summary.nodes = cy.nodes().length - summary.edges = cy.edges().length - summary.groups = getUsedGroups().filter((g) => g !== UNGROUPED).length - setStatus('apiStatus', [ - { - level: 'ok', - text: `${origin}: opened "${name}" with ${plural(summary.nodes, 'node')}, ${plural(summary.edges, 'edge')} and ${plural(summary.groups, 'group')}.`, - }, - ]) - toast(`Opened "${name}" from ${origin}.`) - return summary -} - -export function b64urlEncode(text) { - const bytes = new TextEncoder().encode(text) - let bin = '' - bytes.forEach((b) => { - bin += String.fromCharCode(b) - }) - return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') -} - -function b64urlDecode(s) { - const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((s.length + 3) % 4)) - return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0))) -} - -async function apiFetchText(url) { - const r = await fetch(url) - if (!r.ok) throw new Error(`${url} answered ${r.status}.`) - return r.text() -} - -// what the address asks for -export async function apiFromLocation() { - const q = new URLSearchParams(location.search) - const hash = new URLSearchParams(location.hash.replace(/^#/, '')) - try { - if (q.get('example')) { - const sel = document.getElementById('sampleSelect') - const key = q.get('example') - if (![...sel.options].some((o) => o.value === key)) - throw new Error(`there is no example called "${key}".`) - sel.value = key - document.getElementById('btnSample').click() - switchTab(q.get('tab') === '3d' ? 'network3d' : 'network') - return { kind: 'example', example: key } - } - if (q.get('session')) { - const token = q.get('session') - const r = await fetch(`api/session/${encodeURIComponent(token)}`) - if (!r.ok) - throw new Error( - r.status === 404 - ? 'This link has expired or does not exist on this server.' - : `The server answered ${r.status}.` - ) - return await normaApiLoad(await r.text(), 'a shared link') - } - if (hash.get('json')) { - let text - try { - text = b64urlDecode(hash.get('json')) - } catch (e) { - throw new Error( - 'the data part of the link (after #json=) is damaged; it may have been cut when the link was copied.' - ) - } - return await normaApiLoad(text, 'the link') - } - if (q.get('data')) return await normaApiLoad(await apiFetchText(q.get('data')), q.get('data')) - if (q.get('network')) { - const files = { network: await apiFetchText(q.get('network')) } - if (q.get('annotation')) files.annotation = await apiFetchText(q.get('annotation')) - if (q.get('expression')) files.expression = await apiFetchText(q.get('expression')) - const settings = {} - if (q.get('layout')) settings.layout = q.get('layout') - if (q.get('theme')) settings.theme = q.get('theme') - return await normaApiLoad( - { name: q.get('name') || 'Linked network', files, settings, tab: q.get('tab') || '' }, - 'the link' - ) - } - } catch (err) { - switchTab('api') - setStatus('apiStatus', [ - { level: 'error', text: `The data in this link could not be opened: ${err.message}` }, - ]) - } - return null -} - -export function apiAnnounceReady() { - const msg = { type: 'norma:ready', version: NORMA_API_VERSION } - try { - if (window.opener) window.opener.postMessage(msg, '*') - } catch (e) {} - try { - if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*') - } catch (e) {} -} - -// page wiring, run by main.ts in the original order -export function init() { - /* ---------- wiring ---------- */ - ;(function setupDbImporters() { - const rs = document.getElementById('reactomeSpecies') - REACTOME_SPECIES.forEach((s) => rs.add(new Option(s, s))) - const bind = (id, fn) => document.getElementById(id).addEventListener('click', fn) - bind('btnReactomeSearch', reactomeSearch) - bind('btnReactomeFetch', reactomeFetch) - bind('btnOmnipathFetch', omnipathFetch) - bind('btnNdexSearch', ndexSearch) - bind('btnNdexFetch', ndexFetch) - bind('btnIntactFetch', intactFetch) - bind('btnGoModels', goLoadModels) - bind('btnGoModelFetch', goFetchModel) - bind('btnGoAnnotate', goAnnotateView) - document.getElementById('goModelFilter').addEventListener('input', filterGoModels) - const sc = document.getElementById('intactScore') - sc.addEventListener('input', () => { - document.getElementById('intactScoreValue').textContent = parseFloat(sc.value).toFixed(2) - }) - ;['reactome', 'omnipath', 'ndex', 'intact', 'go'].forEach((k) => - bind(`${k}Cancel`, () => dbCancel(k)) - ) - document.getElementById('reactomeQuery').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault() - reactomeSearch() - } - }) - document.getElementById('ndexQuery').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault() - ndexSearch() - } - }) - // collapsible importer sections - // every sidebar section opens and closes from its title - document.querySelectorAll('.section.collapsible > h3').forEach((h) => { - const toggle = () => { - h.parentElement.classList.toggle('collapsed') - h.setAttribute('aria-expanded', String(!h.parentElement.classList.contains('collapsed'))) - } - h.addEventListener('click', (e) => { - if (!e.target.closest('button, a, input, select')) toggle() - }) - h.addEventListener('keydown', (e) => { - if (e.target !== h || (e.key !== 'Enter' && e.key !== ' ')) return - e.preventDefault() - toggle() - }) - }) - document - .querySelectorAll('.section.collapsible.collapsed > h3') - .forEach((h) => h.setAttribute('aria-expanded', 'false')) - })() - - // postMessage - window.addEventListener('message', async (e) => { - const m = e.data - if (!m || typeof m !== 'object' || typeof m.type !== 'string' || !m.type.startsWith('norma:')) - return - const reply = (obj) => { - try { - ;(e.source || window.parent).postMessage( - { ...obj, requestId: m.requestId }, - e.origin === 'null' ? '*' : e.origin - ) - } catch (err) {} - } - if (m.type === 'norma:ping') { - reply({ type: 'norma:ready', version: NORMA_API_VERSION }) - return - } - if (m.type !== 'norma:load') return - try { - const summary = await normaApiLoad( - m.payload, - e.origin && e.origin !== 'null' ? e.origin : 'another page' - ) - reply({ type: 'norma:loaded', ok: true, summary }) - } catch (err) { - reply({ type: 'norma:loaded', ok: false, error: err.message }) - } - }) -} diff --git a/frontend/src/arena3d.test.ts b/frontend/src/arena3d.test.ts deleted file mode 100644 index 69e78fb..0000000 --- a/frontend/src/arena3d.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, test } from 'vitest' -import { - benjaminiHochberg, - convertArena3dNetwork, - hypergeomUpper, - isArena3dNetworkText, -} from './arena3d' - -const ARENA = [ - 'SourceNode\tSourceLayer\tTargetNode\tTargetLayer\tWeight\tChannel', - 'A\tL1\tB\tL1\t2\tppi', - 'B\tL1\tC\tL2\t\tgenetic', - 'C\tL2\tD\tL2\t1\tppi', - '', -].join('\n') - -test('recognises Arena3D network files', () => { - expect(isArena3dNetworkText(ARENA)).toBe(true) - expect(isArena3dNetworkText('Source\tTarget\nA\tB\n')).toBe(false) - expect(isArena3dNetworkText('')).toBe(false) -}) - -test('converts layers to groups and keeps weights and channels', () => { - const r = convertArena3dNetwork(ARENA) - expect(r.layers).toBe(2) - expect(r.network.split('\n').filter(Boolean)).toEqual([ - 'Source\tTarget\tWeight\tType', - 'A\tB\t2\tppi', - 'B\tC\t1\tgenetic', - 'C\tD\t1\tppi', - ]) - expect(r.annotation).toBe('L1\tA,B\nL2\tC,D\n') -}) - -test('hypergeometric upper tail', () => { - // urn: 10 balls, 4 white, draw 3: P(X >= 3) = C(4,3)/C(10,3) = 4/120 - expect(hypergeomUpper(3, 10, 4, 3)).toBeCloseTo(4 / 120, 10) - expect(hypergeomUpper(0, 10, 4, 3)).toBeCloseTo(1, 10) - expect(hypergeomUpper(5, 10, 4, 3)).toBe(0) -}) - -test('Benjamini-Hochberg keeps order and is monotone', () => { - // ranks: 0.01, 0.03, 0.04, 0.5 -> q = min over higher ranks of p * n / rank - const q = benjaminiHochberg([0.01, 0.04, 0.03, 0.5]) - expect(q.map((x) => +x.toFixed(4))).toEqual([0.04, 0.0533, 0.0533, 0.5]) - expect(benjaminiHochberg([])).toEqual([]) -}) diff --git a/frontend/src/arena3d.ts b/frontend/src/arena3d.ts deleted file mode 100644 index 8e7d5b3..0000000 --- a/frontend/src/arena3d.ts +++ /dev/null @@ -1,285 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { - UNGROUPED, - effectiveGroupsFor, - escapeHtml, - getUsedGroups, - groupLabel, - refreshNodeVisual, - sanitizeColor, -} from './network_state' -import { activeView } from './profiler' -import { applyGroupVisibility, buildGroupLegend } from './hulls' -import { cy } from './cy' -import { downloadText, fileStem, normaCols, normaLines, plural } from './layouts/controls' -import { groupAnalysisState } from './enrichment' -import { partitionStats } from './welcome' -import { shownEdges, shownNodes } from './metrics' -import { simpleGraph } from './wiring' - -/* ---------- reading Arena3D network files ---------- */ -// SourceNode, SourceLayer, TargetNode, TargetLayer, Weight, Channel: -// becomes a NORMA network (one channel per Channel value when there are -// several) and an annotation with one group per layer. -export function isArena3dNetworkText(text) { - const first = normaLines(text).find((l) => l.trim()) - if (!first) return false - const h = normaCols(first).map((c) => c.trim().toLowerCase()) - return ( - h[0] === 'sourcenode' && - h[1] === 'sourcelayer' && - h[2] === 'targetnode' && - h[3] === 'targetlayer' - ) -} - -export function convertArena3dNetwork(text) { - const lines = normaLines(text).filter((l) => l.trim()) - const head = normaCols(lines[0]).map((c) => c.trim().toLowerCase()) - const wCol = head.indexOf('weight'), - cCol = head.indexOf('channel') - const members = new Map() - const channels = new Set() - const rows = [] - lines.slice(1).forEach((line) => { - const c = normaCols(line) - if (c.length < 4) return - const [sn, sl, tn, tl] = c.map((x) => x.trim()) - ;[ - [sn, sl], - [tn, tl], - ].forEach(([n, l]) => { - if (!members.has(l)) members.set(l, new Set()) - members.get(l).add(n) - }) - const ch = cCol >= 0 ? (c[cCol] || '').trim() : '' - if (ch) channels.add(ch) - rows.push([sn, tn, wCol >= 0 ? (c[wCol] || '').trim() : '', ch]) - }) - const typed = channels.size > 1 - const weighted = wCol >= 0 - const header = ['Source', 'Target', ...(weighted ? ['Weight'] : []), ...(typed ? ['Type'] : [])] - const network = - [ - header.join('\t'), - ...rows.map((r) => - [r[0], r[1], ...(weighted ? [r[2] || '1'] : []), ...(typed ? [r[3] || '1'] : [])].join('\t') - ), - ].join('\n') + '\n' - const annotation = [...members].map(([l, set]) => `${l}\t${[...set].join(',')}`).join('\n') + '\n' - return { network, annotation, layers: members.size } -} - -/* ============================================================ - GROUP ANALYSIS (Network Profiler page, current view) - - per-group statistics and the modularity of the grouping - - the group network: one node per group, edges weighted by the - connections between groups, opened as a new view - - enrichment: hypergeometric test of annotation terms in selected - nodes or in each group of another annotation, with - Benjamini-Hochberg FDR - All of it uses the shown part of the view (ticked groups and channels). - ============================================================ */ - -// log n! with a cached table -const LOG_FACT = [0] - -function logFact(n) { - for (let i = LOG_FACT.length; i <= n; i++) LOG_FACT[i] = LOG_FACT[i - 1] + Math.log(i) - return LOG_FACT[n] -} - -function logChoose(n, k) { - return k < 0 || k > n ? -Infinity : logFact(n) - logFact(k) - logFact(n - k) -} - -// P(X >= k) for X ~ Hypergeometric(N population, K successes, n draws) -export function hypergeomUpper(k, N, K, n) { - const hi = Math.min(K, n) - if (k > hi) return 0 - const lo = Math.max(k, 0) - const denom = logChoose(N, n) - const logs = [] - for (let x = lo; x <= hi; x++) logs.push(logChoose(K, x) + logChoose(N - K, n - x) - denom) - const m = Math.max(...logs) - const p = Math.exp(m) * logs.reduce((s, l) => s + Math.exp(l - m), 0) - return Math.min(1, p) -} - -export function benjaminiHochberg(ps) { - const idx = ps.map((p, i) => [p, i]).sort((a, b) => a[0] - b[0]) - const q = new Array(ps.length) - let min = 1 - for (let r = idx.length - 1; r >= 0; r--) { - const [p, i] = idx[r] - min = Math.min(min, (p * idx.length) / (r + 1)) - q[i] = min - } - return q -} - -// the shown part of the view as a simple undirected graph -function shownSimpleGraph() { - const nodes = shownNodes() - const ids = nodes.map((n) => n.id()) - const idSet = new Set(ids) - const pairs = [] - shownEdges(true).forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s !== t && idSet.has(s) && idSet.has(t)) pairs.push([s, t]) - }) - return { nodes, ids, graph: simpleGraph(ids, pairs) } -} - -function groupStatistics() { - const { nodes, ids, graph } = shownSimpleGraph() - const index = new Map(ids.map((id, i) => [id, i])) - const groupsOf = ids.map((id) => effectiveGroupsFor(cy.$id(id)).filter((g) => g !== UNGROUPED)) - const order = getUsedGroups().filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - const members = new Map(order.map((g) => [g, []])) - groupsOf.forEach((gs, i) => - gs.forEach((g) => { - if (members.has(g)) members.get(g).push(i) - }) - ) - const m = graph.m, - twoM = 2 * m - const deg = graph.adj.map((a) => a.length) - const rows = [] - members.forEach((list, g) => { - if (!list.length) return - const inG = new Set(list) - let internal2 = 0, - boundary = 0, - vol = 0 - list.forEach((i) => { - vol += deg[i] - graph.adj[i].forEach((j) => { - if (inG.has(j)) internal2++ - else boundary++ - }) - }) - const internal = internal2 / 2 - const k = list.length - const pairsIn = (k * (k - 1)) / 2 - const volRest = twoM - vol - rows.push({ - group: g, - label: groupLabel(g), - size: k, - internal, - boundary, - density: pairsIn ? internal / pairsIn : NaN, - avgInternalDegree: k ? internal2 / k : NaN, - conductance: Math.min(vol, volRest) > 0 ? boundary / Math.min(vol, volRest) : NaN, - share: twoM ? internal / m - Math.pow(vol / twoM, 2) : NaN, // modularity term - }) - }) - // modularity of the grouping: nodes in several groups count for their first group, nodes in none are singletons - const raw = new Int32Array(ids.length) - const gIndex = new Map(order.map((g, i) => [g, i])) - groupsOf.forEach((gs, i) => { - raw[i] = gs.length ? gIndex.get(gs[0]) : order.length + i - }) - const part = partitionStats(graph, raw) - const overlapping = groupsOf.filter((gs) => gs.length > 1).length - const ungrouped = groupsOf.filter((gs) => !gs.length).length - // how much denser groups are inside than the network as a whole - const netDensity = ids.length > 1 ? m / ((ids.length * (ids.length - 1)) / 2) : NaN - return { - rows, - modularity: part.modularity, - overlapping, - ungrouped, - nodes: ids.length, - edges: m, - netDensity, - } -} - -export function renderGroupStats() { - const root = document.getElementById('gaStats') - if (!cy.nodes().length) { - root.innerHTML = '

    Show a network first.

    ' - return - } - const st = groupStatistics() - if (!st.rows.length) { - root.innerHTML = '

    The current view has no ticked groups.

    ' - return - } - const f = (v, d = 3) => (Number.isFinite(v) ? v.toFixed(d) : '—') - const notes = [] - if (st.overlapping) - notes.push( - `${plural(st.overlapping, 'node')} in several groups ${st.overlapping === 1 ? 'counts' : 'count'} for ${st.overlapping === 1 ? 'its' : 'their'} first group` - ) - if (st.ungrouped) - notes.push( - `${plural(st.ungrouped, 'node')} in no group ${st.ungrouped === 1 ? 'counts as a group of its own' : 'count as groups of their own'}` - ) - root.innerHTML = ` -

    Modularity of the grouping: ${f(st.modularity)} on ${plural(st.nodes, 'shown node')} and ${plural(st.edges, 'edge')} (network density ${f(st.netDensity, 4)})${notes.length ? `; ${notes.join(', ')}` : ''}. Values above about 0.3 mean the groups follow the network's structure.

    -
    - - ${st.rows - .map( - (r) => ` - - - - ` - ) - .join('')} -
    GroupNodesEdges insideEdges leavingDensity insideMean degree insideConductance ↓Modularity share
    ${escapeHtml(r.label)}${r.size}${r.internal}${r.boundary}${f(r.density)}${f(r.avgInternalDegree, 2)}${f(r.conductance)}${f(r.share, 4)}
    ` - document.getElementById('btnGaStatsTsv').disabled = false - groupAnalysisState.stats = st -} - -export function groupStatsTsv() { - const st = groupAnalysisState.stats - if (!st) return - const f = (v) => (Number.isFinite(v) ? String(Math.round(v * 1e6) / 1e6) : '') - const lines = [ - `# modularity of the grouping\t${f(st.modularity)}`, - [ - 'Group', - 'Nodes', - 'Edges inside', - 'Edges leaving', - 'Density inside', - 'Mean degree inside', - 'Conductance', - 'Modularity share', - ].join('\t'), - ...st.rows.map((r) => - [ - r.label, - r.size, - r.internal, - r.boundary, - f(r.density), - f(r.avgInternalDegree), - f(r.conductance), - f(r.share), - ].join('\t') - ), - ] - const v = activeView() - downloadText(`group-statistics${v ? '-' + fileStem(v.name) : ''}.tsv`, lines.join('\n') + '\n') -} - -// Group colors given with generated data win over the palette that a -// data config re-applies. -export function applyGroupColorOverrides(colors) { - Object.entries(colors || {}).forEach(([g, c]) => { - const sc = sanitizeColor(c) - if (sc) S.nodeColorMap[g] = sc - }) - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - buildGroupLegend() - applyGroupVisibility() -} diff --git a/frontend/src/benchmark.ts b/frontend/src/benchmark.ts deleted file mode 100644 index 88cc5dc..0000000 --- a/frontend/src/benchmark.ts +++ /dev/null @@ -1,274 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { activeView } from './profiler' -import { - applyLocalLayoutsAfter, - blockGroupLayout, - collectLayoutGraph, - strategyGravity, - strategySuperNodes, - strategyVirtualNodes, -} from './layouts/input' -import { - computeSubLayout, - computeSubLayoutAsync, - makeLayoutProgress, - normalizeSpacing, - targetNodeSpacing, -} from './metrics' -import { - downloadText, - fileStem, - nextPaint, - plural, - setLayoutMode, - setStatus, -} from './layouts/controls' -import { escapeHtml } from './network_state' -import { groupSeparation } from './clustering/mapping' -import { runActiveLayout } from './layouts/run' -import { switchTab } from './wiring' - -/* ---------- layout benchmark (Network Profiler) ---------- */ -const BENCH_LAYOUTS = [ - ['conn', 'cose', 'Force-directed (cose)'], - ['conn', 'fr', 'Force-directed, weighted'], - ['conn', 'kk', 'Kamada–Kawai'], - ['conn', 'stress', 'Stress majorization'], - ['conn', 'circle', 'Circle'], - ['conn', 'concentric', 'Concentric'], - ['conn', 'breadthfirst', 'Hierarchical'], - ['conn', 'grid', 'Grid'], - ['conn', 'random', 'Random (baseline)'], - ['groups', 'fr', 'Groups: force-directed, weighted'], - ['groups', 'cose', 'Groups: force-directed (cose)'], - ['groups', 'circle', 'Groups: circle'], - ['groups', 'grid', 'Groups: grid'], - ['groups', 'breadthfirst', 'Groups: hierarchical'], - ['groups', 'concentric', 'Groups: concentric'], - ['groups', 'virtual', 'NORMA-2.0 strategy 1: virtual nodes'], - ['groups', 'gravity', 'NORMA-2.0 strategy 2: group gravity'], - ['groups', 'supernodes', 'NORMA-2.0 strategy 3: super nodes'], -] - -const benchState = { rows: [], running: false, cancel: false } - -async function benchmarkPositions(kind, name, graph) { - const spacing = targetNodeSpacing() - if (kind === 'conn') { - const pos = ['fr', 'kk', 'stress'].includes(name) - ? await computeSubLayoutAsync(graph.nodeIds, graph.edges, name) - : computeSubLayout(graph.nodeIds, graph.edges, name) - return pos ? normalizeSpacing(pos, spacing) : null - } - const algorithm = document.getElementById('strategyAlgorithm').value - const force = parseFloat(document.getElementById('groupForce').value) || 10 - const clusterScale = parseFloat(document.getElementById('groupClusterRadius').value) || 1 - let localName = document.getElementById('localGroupLayout').value - const keepAllowed = name === 'virtual' || name === 'gravity' - if (localName === 'keep' && !keepAllowed) localName = 'circle' - if (name === 'virtual') - return applyLocalLayoutsAfter( - graph, - await strategyVirtualNodes(graph, algorithm), - localName, - force, - clusterScale - ) - if (name === 'gravity') - return applyLocalLayoutsAfter( - graph, - await strategyGravity(graph, algorithm, force), - localName, - force, - clusterScale - ) - if (name === 'supernodes') - return strategySuperNodes(graph, algorithm, localName, force, clusterScale) - return blockGroupLayout(graph, name, localName, force, clusterScale) -} - -export async function runLayoutBenchmark() { - if (benchState.running) { - benchState.cancel = true - return - } - const graph = collectLayoutGraph() - const btn = document.getElementById('btnBench') - if (graph.nodeIds.length < 3) { - setStatus('benchStatus', [ - { level: 'error', text: 'Show a network with at least three nodes first.' }, - ]) - return - } - if (graph.groupMembers.size < 2) { - setStatus('benchStatus', [ - { - level: 'error', - text: 'The current view needs at least two ticked groups to measure separation.', - }, - ]) - return - } - const repeats = parseInt(document.getElementById('benchRepeats').value, 10) || 1 - benchState.running = true - benchState.cancel = false - btn.textContent = 'Stop' - const rows = [] - const started = performance.now() - try { - for (const [li, [kind, name, label]] of BENCH_LAYOUTS.entries()) { - if (benchState.cancel) break - const text = `Running ${label} (${li + 1} of ${BENCH_LAYOUTS.length})…` - const show = (f) => setStatus('benchStatus', [{ level: 'busy', text, progress: f }]) - show(li / BENCH_LAYOUTS.length) - await nextPaint() - const runs = [] - for (let k = 0; k < repeats; k++) { - const t0 = performance.now() - let pos - S.layoutProgress = makeLayoutProgress((f) => - show((li + (k + f) / repeats) / BENCH_LAYOUTS.length) - ) - try { - pos = await benchmarkPositions(kind, name, graph) - } catch (err) { - pos = null - } finally { - S.layoutProgress = null - } - show((li + (k + 1) / repeats) / BENCH_LAYOUTS.length) - await nextPaint() - const ms = performance.now() - t0 - if (!pos) continue - runs.push({ ms, ...groupSeparation(pos, graph.groupsOf) }) - } - const agg = (key) => { - const vals = runs.map((r) => r[key]).filter(Number.isFinite) - if (!vals.length) return { mean: NaN, sd: NaN } - const mean = vals.reduce((s, v) => s + v, 0) / vals.length - const sd = - vals.length > 1 - ? Math.sqrt(vals.reduce((s, v) => s + (v - mean) ** 2, 0) / (vals.length - 1)) - : NaN - return { mean, sd } - } - rows.push({ - kind, - name, - label, - runs: runs.length, - ms: agg('ms'), - silhouette: agg('silhouette'), - intruders: agg('intruders'), - overlap: agg('overlap'), - }) - benchState.rows = rows - renderBenchmark(graph) - } - const secs = ((performance.now() - started) / 1000).toFixed(1) - setStatus('benchStatus', [ - { - level: benchState.cancel ? 'warn' : 'ok', - text: `${benchState.cancel ? 'Stopped after' : 'Benchmarked'} ${plural(rows.length, 'layout')} on ${plural(graph.nodeIds.length, 'node')} and ${plural(graph.groupMembers.size, 'group')} in ${secs} s. The view itself was not changed.`, - }, - ]) - document.getElementById('btnBenchTsv').disabled = !rows.length - } finally { - benchState.running = false - btn.textContent = 'Benchmark layouts' - } -} - -function renderBenchmark(graph) { - const root = document.getElementById('benchResults') - const rows = benchState.rows - if (!rows.length) { - root.innerHTML = '' - return - } - const best = (key) => { - const vals = rows.map((r) => r[key].mean).filter(Number.isFinite) - if (!vals.length) return null - return key === 'silhouette' ? Math.max(...vals) : Math.min(...vals) - } - const bS = best('silhouette'), - bI = best('intruders'), - bO = best('overlap') - const cell = (v, fmt, isBest) => - `${Number.isFinite(v.mean) ? fmt(v.mean) + (Number.isFinite(v.sd) ? ` ± ${fmt(v.sd)}` : '') : '—'}` - const pct = (v) => `${(v * 100).toFixed(1)}%` - const num = (v) => v.toFixed(3) - const ms = (v) => (v < 1000 ? `${Math.round(v)} ms` : `${(v / 1000).toFixed(1)} s`) - root.innerHTML = `
    - - ${rows - .map( - (r, i) => ` - - ${cell(r.ms, ms, false)} - ${cell(r.silhouette, num, r.silhouette.mean === bS)} - ${cell(r.intruders, pct, r.intruders.mean === bI)} - ${cell(r.overlap, pct, r.overlap.mean === bO)} - - ` - ) - .join('')}
    LayoutTimeSilhouette ↑Inside other outlines ↓Outline overlap ↓
    ${escapeHtml(r.label)}
    ` - root - .querySelectorAll('.bench-use') - .forEach((b) => b.addEventListener('click', () => useBenchLayout(rows[+b.dataset.row]))) -} - -function useBenchLayout(row) { - if (row.kind === 'conn') { - setLayoutMode('connections') - document.getElementById('layoutSelect').value = row.name - } else { - setLayoutMode('groups') - document.getElementById('groupArrangement').value = row.name - document - .getElementById('groupArrangement') - .dispatchEvent(new Event('change', { bubbles: true })) - } - switchTab('network') - runActiveLayout() -} - -export function benchmarkTsv() { - const rows = benchState.rows - if (!rows.length) return - const f = (v, d = 4) => (Number.isFinite(v) ? v.toFixed(d) : '') - const lines = [ - [ - 'Layout', - 'Runs', - 'Time ms (mean)', - 'Time ms (sd)', - 'Silhouette (mean)', - 'Silhouette (sd)', - 'Inside other outlines (mean)', - 'Inside other outlines (sd)', - 'Outline overlap (mean)', - 'Outline overlap (sd)', - ].join('\t'), - ] - rows.forEach((r) => - lines.push( - [ - r.label, - r.runs, - f(r.ms.mean, 1), - f(r.ms.sd, 1), - f(r.silhouette.mean), - f(r.silhouette.sd), - f(r.intruders.mean), - f(r.intruders.sd), - f(r.overlap.mean), - f(r.overlap.sd), - ].join('\t') - ) - ) - const v = typeof activeView === 'function' && activeView() - downloadText(`layout-benchmark${v ? '-' + fileStem(v.name) : ''}.tsv`, lines.join('\n') + '\n') -} diff --git a/frontend/src/clustering/clustering.test.ts b/frontend/src/clustering/clustering.test.ts deleted file mode 100644 index 23f6252..0000000 --- a/frontend/src/clustering/clustering.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { runCommunityAlgorithm } from './mcl' - -// Two 4-cliques joined by a single edge: every algorithm must find the two cliques. -function twoCliques() { - const edges: [number, number][] = [] - for (const base of [0, 4]) - for (let i = 0; i < 4; i++) for (let j = i + 1; j < 4; j++) edges.push([base + i, base + j]) - edges.push([3, 4]) - const adj: number[][] = Array.from({ length: 8 }, () => []) - for (const [a, b] of edges) { - adj[a].push(b) - adj[b].push(a) - } - return { n: 8, m: edges.length, adj } -} - -describe.each(['louvain', 'leiden', 'lpa', 'walktrap', 'mcl'])('%s', (algo) => { - test('separates two cliques', () => { - const r = runCommunityAlgorithm(twoCliques(), algo) - expect(r.count).toBe(2) - const m = Array.from(r.membership as Int32Array) - expect(new Set(m.slice(0, 4)).size).toBe(1) - expect(new Set(m.slice(4)).size).toBe(1) - expect(m[0]).not.toBe(m[7]) - expect(r.sizes).toEqual([4, 4]) - // 13 edges; each clique: 12/26 - (13/26)^2 - expect(r.modularity).toBeCloseTo(2 * (12 / 26 - (13 / 26) ** 2), 6) - }) - - test('is deterministic', () => { - const a = runCommunityAlgorithm(twoCliques(), algo) - const b = runCommunityAlgorithm(twoCliques(), algo) - expect(Array.from(a.membership as Int32Array)).toEqual(Array.from(b.membership as Int32Array)) - }) -}) - -test('a graph without edges is one community per node', () => { - const r = runCommunityAlgorithm({ n: 3, m: 0, adj: [[], [], []] }, 'leiden') - expect(r.count).toBe(3) - expect(Number.isNaN(r.modularity)).toBe(true) -}) - -test('unknown algorithm throws', () => { - expect(() => runCommunityAlgorithm(twoCliques(), 'nope')).toThrow(/Unknown community algorithm/) -}) diff --git a/frontend/src/clustering/label_propagation.ts b/frontend/src/clustering/label_propagation.ts deleted file mode 100644 index a4541c5..0000000 --- a/frontend/src/clustering/label_propagation.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { mulberry32 } from '../sample_data' -import { partitionStats, splitDisconnected } from '../welcome' - -/* ---------- label propagation ---------- */ -export function labelPropagation(g, opts = {}) { - const rand = mulberry32(opts.seed ?? 7) - const n = g.n - const label = Int32Array.from({ length: n }, (_, i) => i) - const order = Array.from({ length: n }, (_, i) => i) - for (let it = 0; it < 200; it++) { - for (let i = n - 1; i > 0; i--) { - const j = Math.floor(rand() * (i + 1)) - ;[order[i], order[j]] = [order[j], order[i]] - } - let changed = false - for (const v of order) { - const a = g.adj[v] - if (!a.length) continue - const counts = new Map() - let best = 0 - for (let k = 0; k < a.length; k++) { - const c = (counts.get(label[a[k]]) || 0) + 1 - counts.set(label[a[k]], c) - if (c > best) best = c - } - const top = [] - counts.forEach((c, l) => { - if (c === best) top.push(l) - }) - if (top.includes(label[v])) continue // already a most frequent label - label[v] = top[Math.floor(rand() * top.length)] - changed = true - } - if (!changed) break - } - return partitionStats(g, splitDisconnected(g, label)) -} diff --git a/frontend/src/clustering/leiden.ts b/frontend/src/clustering/leiden.ts deleted file mode 100644 index 4e01099..0000000 --- a/frontend/src/clustering/leiden.ts +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { mulberry32 } from '../sample_data' -import { partitionStats } from '../welcome' - -/* ---------- Leiden ---------- */ -export function leidenCommunities(g, opts = {}) { - const gamma = opts.resolution ?? 1 - const theta = opts.theta ?? 0.01 - const rand = mulberry32(opts.seed ?? 42) - const n = g.n - if (!g.m) - return partitionStats( - g, - Int32Array.from({ length: n }, (_, i) => i) - ) - // weighted graph: nbr[i] = Map(j -> w), kw[i] = weighted degree - let nbr = g.adj.map((a) => { - const mp = new Map() - a.forEach((v) => mp.set(v, 1)) - return mp - }) - let N = n - const twoM = 2 * g.m - let nodeOf = Int32Array.from({ length: n }, (_, i) => i) // original node -> aggregate node - let part = Int32Array.from({ length: n }, (_, i) => i) // aggregate node -> community - - const shuffle = (arr) => { - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(rand() * (i + 1)) - ;[arr[i], arr[j]] = [arr[j], arr[i]] - } - return arr - } - - for (let level = 0; level < 50; level++) { - const kw = new Float64Array(N) - nbr.forEach((mp, i) => { - let s = 0 - mp.forEach((w, j) => { - s += j === i ? 2 * w : w - }) - kw[i] = s - }) - // --- move nodes fast - const tot = new Float64Array(N) - for (let i = 0; i < N; i++) tot[part[i]] += kw[i] - const queue = shuffle(Array.from({ length: N }, (_, i) => i)) - const inQueue = new Uint8Array(N).fill(1) - let head = 0 - let changed = false - while (head < queue.length) { - const v = queue[head++] - inQueue[v] = 0 - const cv = part[v] - const links = new Map() - nbr[v].forEach((w, u) => { - if (u !== v) { - const cu = part[u] - links.set(cu, (links.get(cu) || 0) + w) - } - }) - tot[cv] -= kw[v] - let best = cv - let bestGain = (links.get(cv) || 0) - (gamma * kw[v] * tot[cv]) / twoM - links.forEach((w, c) => { - const gain = w - (gamma * kw[v] * tot[c]) / twoM - if (gain > bestGain + 1e-12) { - bestGain = gain - best = c - } - }) - tot[best] += kw[v] - if (best !== cv) { - part[v] = best - changed = true - nbr[v].forEach((w, u) => { - if (u !== v && part[u] !== best && !inQueue[u]) { - inQueue[u] = 1 - queue.push(u) - } - }) - } - } - // communities of the current level - const commMembers = new Map() - for (let i = 0; i < N; i++) { - if (!commMembers.has(part[i])) commMembers.set(part[i], []) - commMembers.get(part[i]).push(i) - } - if (commMembers.size === N) break // every community is a single node: done - // --- refinement: well-connected sub-communities inside each community - const refined = Int32Array.from({ length: N }, (_, i) => i) - const rTot = Float64Array.from(kw) - const rSingle = new Uint8Array(N).fill(1) - commMembers.forEach((members, c) => { - if (members.length === 1) return - const totC = members.reduce((s, i) => s + kw[i], 0) - const inC = new Set(members) - // links from each node to the rest of its community - const kIn = new Map() - members.forEach((v) => { - let s = 0 - nbr[v].forEach((w, u) => { - if (u !== v && inC.has(u)) s += w - }) - kIn.set(v, s) - }) - // links from each refined community to the rest of C (start: singletons) - const extC = new Map(members.map((v) => [v, kIn.get(v)])) - shuffle([...members]).forEach((v) => { - if (!rSingle[v]) return - const kv = kw[v] - if (kIn.get(v) < (gamma * kv * (totC - kv)) / twoM) return // v not well connected - const links = new Map() - nbr[v].forEach((w, u) => { - if (u !== v && inC.has(u)) { - const r = refined[u] - links.set(r, (links.get(r) || 0) + w) - } - }) - const own = refined[v] - const candidates = [] - let maxGain = 0 - links.forEach((w, r) => { - if (r === own) return - const totR = rTot[r] - // only well-connected refined communities are eligible - if (extC.get(r) < (gamma * totR * (totC - totR)) / twoM) return - const gain = w - (gamma * kv * totR) / twoM - if (gain >= 0) { - candidates.push([r, gain]) - if (gain > maxGain) maxGain = gain - } - }) - if (!candidates.length) return - // choose randomly, favouring larger gains (theta controls randomness) - const weights = candidates.map(([, gain]) => Math.exp((gain - maxGain) / theta)) - let pick = rand() * weights.reduce((s, w) => s + w, 0) - let chosen = candidates[candidates.length - 1][0] - for (let i = 0; i < candidates.length; i++) { - pick -= weights[i] - if (pick <= 0) { - chosen = candidates[i][0] - break - } - } - // move v from its singleton into `chosen` - const linksToChosen = links.get(chosen) || 0 - extC.set(chosen, extC.get(chosen) + kIn.get(v) - 2 * linksToChosen) - extC.delete(own) - rTot[chosen] += kv - rTot[own] -= kv - refined[v] = chosen - rSingle[v] = 0 - rSingle[chosen] = 0 // refined ids are node indices, so this is the community's first node - }) - }) - // --- aggregate on the refined partition; each aggregate node keeps its - // (non-refined) community as its starting community - const rmap = new Map() - for (let i = 0; i < N; i++) if (!rmap.has(refined[i])) rmap.set(refined[i], rmap.size) - const nextN = rmap.size - if (nextN === N && !changed) break - const agg = Array.from({ length: nextN }, () => new Map()) - const nextPart = new Int32Array(nextN) - for (let i = 0; i < N; i++) { - const ri = rmap.get(refined[i]) - nextPart[ri] = part[i] - nbr[i].forEach((w, j) => { - const rj = rmap.get(refined[j]) - // self-loops are stored once with their full weight - if (ri === rj) { - if (j >= i) agg[ri].set(ri, (agg[ri].get(ri) || 0) + (i === j ? w : w)) - } else agg[ri].set(rj, (agg[ri].get(rj) || 0) + w) - }) - } - nodeOf = nodeOf.map((x) => rmap.get(refined[x])) - // community ids of the aggregate start partition - const cmap = new Map() - for (let i = 0; i < nextN; i++) { - if (!cmap.has(nextPart[i])) cmap.set(nextPart[i], cmap.size) - nextPart[i] = cmap.get(nextPart[i]) - } - nbr = agg - part = nextPart - N = nextN - if (nextN === cmap.size) break // aggregation changes nothing more - } - const raw = new Int32Array(n) - for (let i = 0; i < n; i++) raw[i] = part[nodeOf[i]] - return partitionStats(g, raw) -} diff --git a/frontend/src/clustering/mapping.ts b/frontend/src/clustering/mapping.ts deleted file mode 100644 index eda41c6..0000000 --- a/frontend/src/clustering/mapping.ts +++ /dev/null @@ -1,814 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EDGE_TYPES } from '../palette' -import { S } from '../state' -import { - UNGROUPED, - effectiveGroupsFor, - formatAttrValue, - getUsedGroups, - groupLabel, - nodeFillMode, - refreshNodeVisual, -} from '../network_state' -import { VALUE_SCALES, VALUE_TRANSFORMS } from './mcl' -import { applyNodeSizing, shownNodes } from '../metrics' -import { convexHull, displayGroups, getUsedTypes, legendExtra } from '../hulls' -import { currentTheme } from '../themes' -import { cy } from '../cy' -import { mulberry32 } from '../sample_data' -import { plural } from '../layouts/controls' -import { rgbOf } from '../view3d/state' -import { stopFramePlay, updateFrameControls } from '../enrichment' -import { updateContextInfo } from '../recording' - -/* ---------- mapping ---------- */ -export function viewHasValues() { - return cy.nodes().some((n) => n.data('values')) -} - -export function valueColumns() { - const cols = new Set() - cy.nodes().forEach((n) => { - const v = n.data('values') - if (v) Object.keys(v).forEach((k) => cols.add(k)) - }) - return [...cols] -} - -function valueSettings() { - const scale = - VALUE_SCALES[document.getElementById('valueScale').value] || VALUE_SCALES['div-rdbu'] - return { - column: document.getElementById('valueColumn').value, - transform: - VALUE_TRANSFORMS[document.getElementById('valueTransform').value] || VALUE_TRANSFORMS.none, - scaleKey: document.getElementById('valueScale').value, - scale, - center: parseFloat(document.getElementById('valueCenter').value) || 0, - range: document.getElementById('valueRange').value, - min: parseFloat(document.getElementById('valueMin').value), - max: parseFloat(document.getElementById('valueMax').value), - missing: document.getElementById('valueMissing').value || '#c8c8c8', - } -} - -function nodeValue(node, st) { - const v = node.data('values') - if (!v || v[st.column] === null || v[st.column] === undefined) return null - const t = st.transform.fn(v[st.column]) - return Number.isFinite(t) ? t : null -} - -// Range of the scale: [lo, hi], plus the center for diverging scales. -function valueDomain(st) { - const vals = [] - const cols = document.getElementById('valueSameRange').checked ? valueColumns() : [st.column] - cy.nodes().forEach((n) => { - if (n.hasClass('hidden-group')) return - cols.forEach((c) => { - const x = nodeValue(n, { ...st, column: c }) - if (x !== null) vals.push(x) - }) - }) - if (!vals.length) return { lo: 0, hi: 1, center: st.center, count: 0 } - let lo = Math.min(...vals), - hi = Math.max(...vals) - if ( - st.range === 'custom' && - Number.isFinite(st.min) && - Number.isFinite(st.max) && - st.max > st.min - ) { - lo = st.min - hi = st.max - } else if (st.scale.diverging && st.range === 'symmetric') { - const m = Math.max(Math.abs(lo - st.center), Math.abs(hi - st.center)) || 1 - lo = st.center - m - hi = st.center + m - } - if (hi === lo) { - lo -= 1 - hi += 1 - } - return { - lo, - hi, - center: st.center, - count: vals.length, - dataMin: Math.min(...vals), - dataMax: Math.max(...vals), - } -} - -function interpolateStops(stops, t) { - t = Math.max(0, Math.min(1, t)) - const pos = t * (stops.length - 1) - const i = Math.min(stops.length - 2, Math.floor(pos)) - const f = pos - i - const a = rgbOf(stops[i]), - b = rgbOf(stops[i + 1]) - const h = (x) => Math.round(x).toString(16).padStart(2, '0') - return ( - '#' + h(a[0] + (b[0] - a[0]) * f) + h(a[1] + (b[1] - a[1]) * f) + h(a[2] + (b[2] - a[2]) * f) - ) -} - -function valueToColor(x, st, dom) { - if (x === null) return st.missing - let t - if (st.scale.diverging) { - const c = Math.min(Math.max(dom.center, dom.lo), dom.hi) - t = x < c ? (0.5 * (x - dom.lo)) / (c - dom.lo || 1) : 0.5 + (0.5 * (x - c)) / (dom.hi - c || 1) - } else { - t = (x - dom.lo) / (dom.hi - dom.lo) - } - return interpolateStops(st.scale.stops, t) -} - -// Size by value: distance from the center on diverging scales, the value itself otherwise. -export function valueForSize(node) { - const st = valueSettings() - const x = nodeValue(node, st) - if (x === null) return 0 - return st.scale.diverging ? Math.abs(x - st.center) : x -} - -export function applyValueColors() { - const has = viewHasValues() - document.getElementById('valueControls').hidden = !has - const fillSel = document.getElementById('nodeFillSelect') - fillSel.querySelector('option[value="values"]').disabled = !has - if (!has && fillSel.value === 'values') fillSel.value = 'groups' - if (!has) { - scheduleLegend() - if (typeof updateFrameControls === 'function') { - stopFramePlay() - updateFrameControls() - } - return - } - // keep the column list current - const colSel = document.getElementById('valueColumn') - const cols = valueColumns() - const current = colSel.value - if ([...colSel.options].map((o) => o.value).join('\u0000') !== cols.join('\u0000')) { - colSel.innerHTML = '' - cols.forEach((c) => colSel.add(new Option(c, c))) - } - if (!cols.includes(colSel.value)) colSel.value = cols.includes(current) ? current : cols[0] - const st = valueSettings() - document.getElementById('valueCenterRow').hidden = !st.scale.diverging - document.getElementById('valueCustomRow').hidden = st.range !== 'custom' - const symOpt = document.getElementById('valueRange').querySelector('option[value="symmetric"]') - symOpt.disabled = !st.scale.diverging - if (!st.scale.diverging && st.range === 'symmetric') - document.getElementById('valueRange').value = 'data' - const dom = valueDomain(st) - cy.batch(() => { - cy.nodes().forEach((n) => { - const c = valueToColor(nodeValue(n, st), st, dom) - if (n.data('valueColor') !== c) n.data('valueColor', c) - }) - if (nodeFillMode() === 'values') cy.nodes().forEach((n) => refreshNodeVisual(n)) - }) - const fmt = (v) => formatAttrValue(Math.round(v * 1000) / 1000) - const own = cy - .nodes() - .filter((n) => !n.hasClass('hidden-group') && nodeValue(n, st) !== null) - .map((n) => nodeValue(n, st)) - document.getElementById('valueSummary').textContent = own.length - ? `${plural(own.length, 'shown node')} with a value, from ${fmt(Math.min(...own))} to ${fmt(Math.max(...own))}${st.transform.fn === VALUE_TRANSFORMS.none.fn ? '' : ' after the transform'}${valueColumns().length > 1 && document.getElementById('valueSameRange').checked ? '; the scale covers all columns' : ''}.` - : 'No shown node has a value in this column.' - if (document.getElementById('sizeMetric').value === 'value') applyNodeSizing() - scheduleLegend() - updateContextInfo() - if (typeof updateFrameControls === 'function') updateFrameControls() -} - -/* ============================================================ - LEGEND - One layout, drawn three ways: on screen (inline SVG), into exported - raster images (canvas) and into exported SVG files. - ============================================================ */ -const LEGEND_W = 230 - -function legendOptions() { - return { - show: document.getElementById('legendShow').checked, - scale: document.getElementById('legendScale').checked, - groups: document.getElementById('legendGroups').checked, - shapes: document.getElementById('legendShapes').checked, - channels: document.getElementById('legendChannels').checked, - title: document.getElementById('legendTitle').value.trim(), - } -} - -export function legendModel() { - const o = legendOptions() - const model = { title: o.title, sections: [], scale: null } - if (legendExtra) - legendExtra.forEach((sec) => - model.sections.push({ - title: sec.title, - items: sec.items.map((it) => ({ - kind: it.kind || 'glyph', - label: it.label, - color: it.color, - shape: 'ellipse', - })), - }) - ) - if ( - o.scale && - viewHasValues() && - (nodeFillMode() === 'values' || document.getElementById('sizeMetric').value === 'value') - ) { - const st = valueSettings() - const dom = valueDomain(st) - const missing = cy.nodes().some((n) => !n.hasClass('hidden-group') && nodeValue(n, st) === null) - model.scale = { - title: st.transform.title(st.column), - stops: st.scale.stops, - diverging: st.scale.diverging, - lo: dom.lo, - hi: dom.hi, - center: Math.min(Math.max(dom.center, dom.lo), dom.hi), - missing: missing ? st.missing : null, - colored: nodeFillMode() === 'values', - sized: document.getElementById('sizeMetric').value === 'value', - } - } - if (o.groups) { - const groups = displayGroups(getUsedGroups()).filter( - (g) => S.activeGroups.has(g) && g !== UNGROUPED - ) - const showing = - nodeFillMode() === 'groups' || - document.getElementById('showGroupHulls').checked || - Object.keys(S.groupShapes).length - if (groups.length && showing) { - const items = groups.slice(0, 40).map((g) => ({ - kind: 'glyph', - label: groupLabel(g), - color: S.nodeColorMap[g] || '#888888', - shape: o.shapes && S.groupShapes[g] ? S.groupShapes[g] : 'ellipse', - })) - if (groups.length > 40) - items.push({ kind: 'note', label: `and ${groups.length - 40} more groups` }) - model.sections.push({ title: legendExtra ? 'Nodes (in which networks)' : 'Groups', items }) - } - } - if (o.channels) { - const types = getUsedTypes().filter((t) => S.activeTypes.has(t)) - if (types.length > 1) { - const items = types.slice(0, 30).map((t) => ({ - kind: 'line', - label: (EDGE_TYPES[t] || { label: t }).label, - color: (EDGE_TYPES[t] || { color: '#888' }).color, - })) - if (types.length > 30) - items.push({ kind: 'note', label: `and ${types.length - 30} more channels` }) - model.sections.push({ - title: legendExtra ? 'Edges (in which networks)' : 'Edge channels', - items, - }) - } - } - return model -} - -export function legendIsEmpty(m) { - return !m.scale && !m.sections.length -} - -function niceTicks(lo, hi, count = 5) { - const span = hi - lo - const step0 = span / (count - 1) - const mag = Math.pow(10, Math.floor(Math.log10(step0))) - const norm = step0 / mag - const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag - const ticks = [] - for (let v = Math.ceil(lo / step - 1e-9) * step; v <= hi + 1e-9; v += step) - ticks.push(Math.abs(v) < step * 1e-9 ? 0 : v) - return ticks -} - -function tickLabel(v) { - const a = Math.abs(v) - if (a !== 0 && (a >= 1e5 || a < 1e-3)) return v.toExponential(1) - return String(Math.round(v * 1000) / 1000) -} - -// Layout in unit pixels: a list of drawing operations. -export function layoutLegend(m) { - const ops = [] - const pad = 12, - w = LEGEND_W - let y = pad - const text = (str, x, yy, size, opts = {}) => - ops.push({ t: 'text', str, x, y: yy, size, ...opts }) - if (m.title) { - text(m.title, pad, y + 12, 14, { bold: true }) - y += 24 - } - if (m.scale) { - const s = m.scale - text(s.title, pad, y + 11, 12, { bold: true }) - y += 18 - const barW = w - 2 * pad, - barH = 12 - ops.push({ - t: 'grad', - x: pad, - y, - w: barW, - h: barH, - stops: s.diverging - ? divergingStopsFor(s) - : s.stops.map((c, i) => [i / (s.stops.length - 1), c]), - }) - ops.push({ t: 'rect', x: pad, y, w: barW, h: barH, stroke: true }) - const ticks = niceTicks(s.lo, s.hi) - ticks.forEach((v) => { - const tx = pad + ((v - s.lo) / (s.hi - s.lo)) * barW - ops.push({ t: 'line', x1: tx, y1: y + barH, x2: tx, y2: y + barH + 4, muted: true }) - text(tickLabel(v), tx, y + barH + 15, 10, { anchor: 'middle', muted: true }) - }) - y += barH + 22 - const notes = [] - if (s.colored && s.sized) notes.push('node color and size') - else if (s.sized) notes.push('node size') - if (notes.length) { - text(`Shown as ${notes.join(', ')}`, pad, y + 8, 10, { muted: true }) - y += 14 - } - if (s.missing) { - ops.push({ t: 'glyph', shape: 'ellipse', x: pad + 6, y: y + 7, r: 6, fill: s.missing }) - text('no value', pad + 18, y + 11, 11, {}) - y += 18 - } - y += 6 - } - m.sections.forEach((sec) => { - text(sec.title, pad, y + 11, 12, { bold: true }) - y += 18 - sec.items.forEach((it) => { - if (it.kind === 'glyph') - ops.push({ t: 'glyph', shape: it.shape, x: pad + 7, y: y + 7, r: 6.5, fill: it.color }) - else if (it.kind === 'line') - ops.push({ - t: 'line', - x1: pad, - y1: y + 7, - x2: pad + 16, - y2: y + 7, - color: it.color, - width: 3, - }) - const label = it.label.length > 30 ? it.label.slice(0, 29) + '…' : it.label - text(label, it.kind === 'note' ? pad : pad + 22, y + 11, 11, { - muted: it.kind === 'note', - full: it.label, - }) - y += 17 - }) - y += 6 - }) - return { w, h: y + pad - 6, ops } -} - -function divergingStopsFor(s) { - // put the middle stop at the center value - const c = (s.center - s.lo) / (s.hi - s.lo) - const n = s.stops.length, - mid = (n - 1) / 2 - return s.stops.map((col, i) => [i <= mid ? (c * i) / mid : c + ((1 - c) * (i - mid)) / mid, col]) -} - -function legendSvg(layout, scale, colors, x0 = 0, y0 = 0) { - const f = (v) => (Math.round(v * 100) / 100).toString() - const esc = (v) => - String(v).replace( - /[&<>"]/g, - (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] - ) - const id = 'lg' + Math.random().toString(36).slice(2, 8) - let defs = '', - body = '' - let gi = 0 - layout.ops.forEach((op) => { - if (op.t === 'text') { - body += `${op.full && op.full !== op.str ? `${esc(op.full)}` : ''}${esc(op.str)}` - } else if (op.t === 'grad') { - const gid = `${id}g${gi++}` - defs += `${op.stops.map(([o, c]) => ``).join('')}` - body += `` - } else if (op.t === 'rect') { - body += `` - } else if (op.t === 'line') { - body += `` - } else if (op.t === 'glyph') { - body += glyphSvg(op.shape, op.x, op.y, op.r, op.fill, colors.text) - } - }) - return `${defs ? `${defs}` : ''}${body}` -} - -function glyphSvg(shape, x, y, r, fill, stroke) { - const f = (v) => (Math.round(v * 100) / 100).toString() - const def = shape !== 'ellipse' && cy.renderer().nodeShapes[shape] - if (def && def.points) { - const pts = [] - for (let i = 0; i < def.points.length; i += 2) - pts.push(`${f(x + def.points[i] * r)},${f(y + def.points[i + 1] * r)}`) - return `` - } - return `` -} - -function drawLegendOnCanvas(ctx, layout, scale, colors, x0, y0, bg) { - ctx.save() - ctx.translate(x0, y0) - ctx.scale(scale, scale) - if (bg) { - ctx.fillStyle = bg - ctx.fillRect(0, 0, layout.w, layout.h) - } - layout.ops.forEach((op) => { - if (op.t === 'text') { - ctx.font = `${op.bold ? '700 ' : ''}${op.size}px Inter, Helvetica, Arial, sans-serif` - ctx.fillStyle = op.muted ? colors.muted : colors.text - ctx.textAlign = op.anchor === 'middle' ? 'center' : 'left' - ctx.textBaseline = 'alphabetic' - ctx.fillText(op.str, op.x, op.y) - } else if (op.t === 'grad') { - const g = ctx.createLinearGradient(op.x, 0, op.x + op.w, 0) - op.stops.forEach(([o, c]) => g.addColorStop(Math.max(0, Math.min(1, o)), c)) - ctx.fillStyle = g - ctx.fillRect(op.x, op.y, op.w, op.h) - } else if (op.t === 'rect') { - ctx.strokeStyle = colors.line - ctx.lineWidth = 1 - ctx.strokeRect(op.x, op.y, op.w, op.h) - } else if (op.t === 'line') { - ctx.strokeStyle = op.color || colors.muted - ctx.lineWidth = op.width || 1 - ctx.lineCap = 'round' - ctx.beginPath() - ctx.moveTo(op.x1, op.y1) - ctx.lineTo(op.x2, op.y2) - ctx.stroke() - } else if (op.t === 'glyph') { - ctx.fillStyle = op.fill - ctx.strokeStyle = colors.text - ctx.globalAlpha = 1 - ctx.lineWidth = 0.8 - const def = op.shape !== 'ellipse' && cy.renderer().nodeShapes[op.shape] - ctx.beginPath() - if (def && def.points) { - for (let i = 0; i < def.points.length; i += 2) { - const px = op.x + def.points[i] * op.r, - py = op.y + def.points[i + 1] * op.r - i ? ctx.lineTo(px, py) : ctx.moveTo(px, py) - } - ctx.closePath() - } else ctx.arc(op.x, op.y, op.r, 0, Math.PI * 2) - ctx.fill() - ctx.globalAlpha = 0.35 - ctx.stroke() - ctx.globalAlpha = 1 - } - }) - ctx.restore() -} - -function legendColorsFor(bg) { - // readable text on the export background (or the theme on screen) - const dark = bg - ? (() => { - const [r, g, b] = rgbOf(bg) - return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 < 0.45 - })() - : isDarkTheme() - return dark - ? { text: '#e5e7eb', muted: '#9ca3af', line: '#6b7280' } - : { text: '#111827', muted: '#6b7280', line: '#9ca3af' } -} - -function isDarkTheme() { - const [r, g, b] = rgbOf(currentTheme.bg) - return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 < 0.45 -} - -// On-screen legend -let legendQueued = false - -export function scheduleLegend() { - if (legendQueued) return - legendQueued = true - requestAnimationFrame(() => { - legendQueued = false - renderLegendPanel() - }) -} - -function renderLegendPanel() { - const el = document.getElementById('legendPanel') - if (!el) return - const o = legendOptions() - const m = o.show ? legendModel() : null - if (!m || legendIsEmpty(m) || !cy.nodes().length) { - el.hidden = true - return - } - const layout = layoutLegend(m) - const colors = { text: currentTheme.text, muted: currentTheme.muted, line: currentTheme.line } - el.innerHTML = `${legendSvg(layout, 1, colors)}` - el.hidden = false -} - -// Adds the legend to the right of an exported picture. -export function exportLegendLayout(o) { - if (!o.legend) return null - const m = legendModel() - return legendIsEmpty(m) ? null : layoutLegend(m) -} - -export function composeCanvasWithLegend(canvas, layout, scale, bg) { - if (!layout) return canvas - const lw = Math.round(layout.w * scale), - lh = Math.round(layout.h * scale) - const out = document.createElement('canvas') - out.width = canvas.width + lw - out.height = Math.max(canvas.height, lh) - const ctx = out.getContext('2d') - if (bg) { - ctx.fillStyle = bg - ctx.fillRect(0, 0, out.width, out.height) - } - ctx.drawImage(canvas, 0, 0) - drawLegendOnCanvas(ctx, layout, scale, legendColorsFor(bg || '#ffffff'), canvas.width, 0, null) - return out -} - -export function composeSvgWithLegend(svgText, layout, scale, bg) { - if (!layout) return svgText - const body = svgText.replace(/^<\?xml[^>]*>\s*/, '') - const wm = /]*\swidth="([\d.]+)"/.exec(body), - hm = /]*\sheight="([\d.]+)"/.exec(body) - const W = wm ? parseFloat(wm[1]) : 1000, - H = hm ? parseFloat(hm[1]) : 800 - const lw = layout.w * scale, - lh = layout.h * scale - const totalW = Math.ceil(W + lw), - totalH = Math.ceil(Math.max(H, lh)) - const inner = body.replace(/\n\n` + - (bg ? `\n` : '') + - inner + - '\n' + - `${legendSvg(layout, scale, legendColorsFor(bg || '#ffffff'), W, 0)}\n` - ) -} - -/* ============================================================ - GROUP SEPARATION - How well a layout separates the ticked groups: - silhouette mean silhouette width of nodes in exactly one group, - from their positions (-1..1, higher = better separated) - intruders share of shown nodes lying inside the convex outline of - a group they don't belong to (lower = better) - overlap share of the area covered by group outlines that is - covered by two or more of them (lower = better) - Scores are computed after every layout and by the layout benchmark. - ============================================================ */ -const SEPARATION_SAMPLE = 1500 - -export function groupSeparation(positions, groupsOf) { - const ids = Object.keys(groupsOf).filter((id) => positions[id]) - const out = { - nodes: ids.length, - groups: 0, - silhouette: NaN, - intruders: NaN, - overlap: NaN, - silhouetteNodes: 0, - } - const members = new Map() - ids.forEach((id) => - groupsOf[id].forEach((g) => { - if (!members.has(g)) members.set(g, []) - members.get(g).push(id) - }) - ) - out.groups = members.size - if (members.size < 2) return out - - // --- silhouette (nodes in exactly one group) - const single = ids.filter((id) => groupsOf[id].length === 1) - const byGroup = new Map() - single.forEach((id) => { - const g = groupsOf[id][0] - if (!byGroup.has(g)) byGroup.set(g, []) - byGroup.get(g).push(id) - }) - if (byGroup.size >= 2) { - const rand = mulberry32(99) - let sample = single - if (single.length > SEPARATION_SAMPLE) { - sample = [...single] - for (let i = sample.length - 1; i > 0; i--) { - const j = Math.floor(rand() * (i + 1)) - ;[sample[i], sample[j]] = [sample[j], sample[i]] - } - sample = sample.slice(0, SEPARATION_SAMPLE) - } - // reference points per group (all single-group nodes, capped per group), as flat arrays - const refs = new Map() - byGroup.forEach((list, g) => { - let pts = list - if (list.length > 400) { - pts = [] - const stepK = list.length / 400 - for (let k = 0; k < 400; k++) pts.push(list[Math.floor(k * stepK)]) - } - const xs = new Float64Array(pts.length), - ys = new Float64Array(pts.length) - pts.forEach((id, k) => { - xs[k] = positions[id].x - ys[k] = positions[id].y - }) - refs.set(g, { xs, ys, ids: new Set(pts) }) - }) - const groupKeys = [...refs.keys()] - let total = 0 - sample.forEach((id) => { - const g = groupsOf[id][0] - const px = positions[id].x, - py = positions[id].y - const own = refs.get(g) - let a = 0, - na = 0 - for (let k = 0; k < own.xs.length; k++) { - const dx = px - own.xs[k], - dy = py - own.ys[k] - const d = Math.sqrt(dx * dx + dy * dy) - if (d === 0 && own.ids.has(id)) continue // the node itself - a += d - na++ - } - if (!na) return // a group of one: silhouette 0 - a /= na - let b = Infinity - for (const h of groupKeys) { - if (h === g) continue - const r = refs.get(h) - let sum = 0 - for (let k = 0; k < r.xs.length; k++) { - const dx = px - r.xs[k], - dy = py - r.ys[k] - sum += Math.sqrt(dx * dx + dy * dy) - } - const mean = sum / r.xs.length - if (mean < b) b = mean - } - const m = Math.max(a, b) - total += m > 0 ? (b - a) / m : 0 - }) - out.silhouette = total / sample.length - out.silhouetteNodes = sample.length - } - - // --- outlines: convex hulls of groups with three or more members - const hulls = [] - members.forEach((list, g) => { - if (list.length < 3) return - const hull = convexHull(list.map((id) => positions[id])) - if (hull.length < 3) return - const xs = hull.map((p) => p.x), - ys = hull.map((p) => p.y) - hulls.push({ - g, - hull, - set: new Set(list), - x1: Math.min(...xs), - x2: Math.max(...xs), - y1: Math.min(...ys), - y2: Math.max(...ys), - }) - }) - if (!hulls.length) return out - const inside = (hull, x, y) => { - // convex polygon in either orientation - let sign = 0 - for (let i = 0; i < hull.length; i++) { - const a = hull[i], - b = hull[(i + 1) % hull.length] - const cr = (b.x - a.x) * (y - a.y) - (b.y - a.y) * (x - a.x) - if (Math.abs(cr) < 1e-9) continue - const s = cr > 0 ? 1 : -1 - if (!sign) sign = s - else if (s !== sign) return false - } - return true - } - let intr = 0 - ids.forEach((id) => { - const p = positions[id] - for (const h of hulls) { - if (h.set.has(id) || p.x < h.x1 || p.x > h.x2 || p.y < h.y1 || p.y > h.y2) continue - if (inside(h.hull, p.x, p.y)) { - intr++ - break - } - } - }) - out.intruders = intr / ids.length - - // --- overlap by rasterising the outlines - const X1 = Math.min(...hulls.map((h) => h.x1)), - X2 = Math.max(...hulls.map((h) => h.x2)) - const Y1 = Math.min(...hulls.map((h) => h.y1)), - Y2 = Math.max(...hulls.map((h) => h.y2)) - const G = 160 - const sx = (X2 - X1) / G || 1, - sy = (Y2 - Y1) / G || 1 - const cover = new Uint16Array(G * G) - hulls.forEach((h) => { - const i1 = Math.max(0, Math.floor((h.x1 - X1) / sx)), - i2 = Math.min(G - 1, Math.floor((h.x2 - X1) / sx)) - const j1 = Math.max(0, Math.floor((h.y1 - Y1) / sy)), - j2 = Math.min(G - 1, Math.floor((h.y2 - Y1) / sy)) - for (let j = j1; j <= j2; j++) { - const y = Y1 + (j + 0.5) * sy - for (let i = i1; i <= i2; i++) { - if (inside(h.hull, X1 + (i + 0.5) * sx, y)) cover[j * G + i]++ - } - } - }) - let any = 0, - multi = 0 - for (let k = 0; k < cover.length; k++) { - if (cover[k]) any++ - if (cover[k] > 1) multi++ - } - out.overlap = any ? multi / any : 0 - return out -} - -// Current 2D positions and ticked groups of the shown nodes. -function currentSeparationInput() { - const positions = {}, - groupsOf = {} - shownNodes().forEach((n) => { - positions[n.id()] = n.position() - groupsOf[n.id()] = effectiveGroupsFor(n).filter((g) => g !== UNGROUPED) - }) - return { positions, groupsOf } -} - -function formatSeparation(r) { - const pct = (v) => (Number.isFinite(v) ? `${(v * 100).toFixed(v < 0.1 ? 1 : 0)}%` : '—') - return { - silhouette: Number.isFinite(r.silhouette) ? r.silhouette.toFixed(2) : '—', - intruders: pct(r.intruders), - overlap: pct(r.overlap), - } -} - -let separationTimer = null - -export function scheduleSeparation() { - clearTimeout(separationTimer) - separationTimer = setTimeout(updateSeparationBox, 350) -} - -function updateSeparationBox() { - const box = document.getElementById('sepValues') - if (!box) return - if (!cy.nodes().length) { - box.innerHTML = 'No network shown.' - return - } - const { positions, groupsOf } = currentSeparationInput() - const r = groupSeparation(positions, groupsOf) - if (r.groups < 2) { - box.innerHTML = 'Needs at least two ticked groups.' - return - } - const f = formatSeparation(r) - const bar = Number.isFinite(r.silhouette) - ? `` - : '' - box.innerHTML = ` -
    Silhouette${f.silhouette}${bar}
    -
    Inside other outlines${f.intruders}
    -
    Outline overlap${f.overlap}
    ` -} - -// page wiring, run by main.ts in the original order -export function init() { - cy.on('layoutstop dragfree', scheduleSeparation) -} diff --git a/frontend/src/clustering/mcl.ts b/frontend/src/clustering/mcl.ts deleted file mode 100644 index 21071bc..0000000 --- a/frontend/src/clustering/mcl.ts +++ /dev/null @@ -1,257 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { labelPropagation } from './label_propagation' -import { leidenCommunities } from './leiden' -import { listSample, normaCols, normaLines, plural } from '../layouts/controls' -import { louvain } from '../wiring' -import { partitionStats, splitDisconnected } from '../welcome' -import { walktrap } from './walktrap' - -/* ---------- Markov clustering ---------- */ -function markovClustering(g, opts = {}) { - const inflation = opts.inflation ?? 2 - const n = g.n - const keep = opts.keep ?? 60 // largest entries kept per column - const eps = 1e-5 - // column-stochastic matrix with self-loops; cols[j] = Map(i -> value) - let cols = new Array(n) - for (let j = 0; j < n; j++) { - const a = g.adj[j] - const w = 1 / (a.length + 1) - const m = new Map([[j, w]]) - for (let k = 0; k < a.length; k++) m.set(a[k], w) - cols[j] = m - } - const prune = (m) => { - let entries = [...m].filter(([, v]) => v > eps) - if (entries.length > keep) { - entries.sort((x, y) => y[1] - x[1]) - entries = entries.slice(0, keep) - } - let s = 0 - entries.forEach((e) => { - s += e[1] - }) - return new Map(entries.map(([i, v]) => [i, v / s])) - } - for (let it = 0; it < 100; it++) { - // expansion: M * M, column by column - const next = new Array(n) - let delta = 0 - for (let j = 0; j < n; j++) { - const out = new Map() - cols[j].forEach((vkj, k) => { - cols[k].forEach((vik, i) => { - out.set(i, (out.get(i) || 0) + vik * vkj) - }) - }) - // inflation - out.forEach((v, i) => out.set(i, Math.pow(v, inflation))) - const pruned = prune(out) - pruned.forEach((v, i) => { - delta = Math.max(delta, Math.abs(v - (cols[j].get(i) || 0))) - }) - next[j] = pruned - } - cols = next - if (delta < 1e-6) break - } - // each node joins its attractor (strongest row in its column); attractors - // that point at each other form one cluster - const parent = Int32Array.from({ length: n }, (_, i) => i) - const find = (x) => { - while (parent[x] !== x) { - parent[x] = parent[parent[x]] - x = parent[x] - } - return x - } - const union = (a, b) => { - const ra = find(a), - rb = find(b) - if (ra !== rb) parent[ra] = rb - } - for (let j = 0; j < n; j++) { - let best = j, - bestV = -1 - cols[j].forEach((v, i) => { - if (v > bestV + 1e-12) { - bestV = v - best = i - } - }) - union(j, best) - } - const raw = new Int32Array(n) - for (let i = 0; i < n; i++) raw[i] = find(i) - return partitionStats(g, splitDisconnected(g, raw)) -} - -export function runCommunityAlgorithm(g, algo, params = {}) { - if (algo === 'louvain') return louvain(g, params.resolution ?? 1) - if (algo === 'leiden') return leidenCommunities(g, { resolution: params.resolution ?? 1 }) - if (algo === 'lpa') return labelPropagation(g) - if (algo === 'walktrap') return walktrap(g, { steps: params.steps ?? 4 }) - if (algo === 'mcl') return markovClustering(g, { inflation: params.inflation ?? 2 }) - throw new Error('Unknown community algorithm: ' + algo) -} - -/* ============================================================ - NUMERIC NODE VALUES - Expression files may hold numbers (log2 fold changes, p-values, ...) - in one or more named columns instead of colors. Each node then carries - data('values') = { column: number | null }, and the chosen column is - mapped to a diverging or sequential color scale and, optionally, to - node size. - ============================================================ */ -export const VALUE_SCALES = { - 'div-rdbu': { - label: 'Blue – white – red', - diverging: true, - stops: ['#2166ac', '#67a9cf', '#f7f7f7', '#ef8a62', '#b2182b'], - }, - 'div-puor': { - label: 'Purple – white – orange', - diverging: true, - stops: ['#5e3c99', '#b2abd2', '#f7f7f7', '#fdb863', '#e66101'], - }, - 'div-prgn': { - label: 'Purple – white – green', - diverging: true, - stops: ['#762a83', '#af8dc3', '#f7f7f7', '#7fbf7b', '#1b7837'], - }, - 'div-brbg': { - label: 'Brown – white – teal', - diverging: true, - stops: ['#8c510a', '#d8b365', '#f5f5f5', '#5ab4ac', '#01665e'], - }, - 'seq-viridis': { - label: 'Viridis', - diverging: false, - stops: ['#440154', '#3b528b', '#21918c', '#5ec962', '#fde725'], - }, - 'seq-magma': { - label: 'Magma', - diverging: false, - stops: ['#000004', '#51127c', '#b73779', '#fc8961', '#fcfdbf'], - }, - 'seq-blues': { - label: 'Light to dark blue', - diverging: false, - stops: ['#f7fbff', '#c6dbef', '#6baed6', '#2171b5', '#08306b'], - }, - 'seq-reds': { - label: 'Light to dark red', - diverging: false, - stops: ['#fff5f0', '#fcbba1', '#fb6a4a', '#cb181d', '#67000d'], - }, -} - -export const VALUE_TRANSFORMS = { - none: { label: 'as they are', fn: (v) => v, title: (c) => c }, - neglog10: { - label: '−log10 (for p-values)', - fn: (v) => (v > 0 ? -Math.log10(v) : null), - title: (c) => `−log10(${c})`, - }, - abs: { label: 'absolute values', fn: (v) => Math.abs(v), title: (c) => `|${c}|` }, -} - -const NUMERIC_MISSING = new Set([ - '', - 'na', - 'n/a', - 'nan', - 'null', - 'none', - '-', - '.', - '?', - 'inf', - '-inf', -]) - -export function parseNumber(s) { - const t = String(s ?? '').trim() - if (NUMERIC_MISSING.has(t.toLowerCase())) return null - // allow decimal commas when there is no dot - const x = Number(/^[-+]?\d+,\d+(e[-+]?\d+)?$/i.test(t) ? t.replace(',', '.') : t) - return Number.isFinite(x) ? x : undefined // undefined: not a number at all -} - -// Does a table look numeric (node name, then numbers)? Used by detection and parsing. -export function numericShare(lines) { - let rows = 0, - numeric = 0 - lines.forEach((line) => { - const cols = normaCols(line) - if (cols.length < 2) return - rows++ - if ( - cols.slice(1).every((c) => parseNumber(c) !== undefined) && - cols.slice(1).some((c) => parseNumber(c) !== null) - ) - numeric++ - }) - return rows ? numeric / rows : 0 -} - -export function parseNumericValues(text) { - const lines = normaLines(text).filter((l) => l.trim()) - let header = null - const first = normaCols(lines[0] || '') - if (first.length >= 2 && first.slice(1).some((c) => parseNumber(c) === undefined)) header = first - const body = header ? lines.slice(1) : lines - const width = Math.max(...body.map((l) => normaCols(l).length), header ? header.length : 2) - const names = [] - for (let c = 1; c < width; c++) { - const raw = header && header[c] ? header[c].trim() : width === 2 ? 'value' : `value ${c}` - let name = raw || `value ${c}` - while (names.includes(name)) name += '′' - names.push(name) - } - const values = new Map() - const bad = [], - repeated = [] - body.forEach((line, i) => { - const cols = normaCols(line) - const node = (cols[0] || '').trim() - if (!node) return - const row = {} - let any = false - names.forEach((name, k) => { - const v = parseNumber(cols[k + 1]) - if (v === undefined) { - bad.push(i + 1 + (header ? 1 : 0)) - row[name] = null - } else { - row[name] = v - if (v !== null) any = true - } - }) - if (values.has(node)) repeated.push(node) - if (any || !values.has(node)) values.set(node, row) - }) - if (!values.size) - throw new Error( - 'No numeric values were found. Each line should be a node name, a tab, then one or more numbers.' - ) - const notes = [] - if (header) notes.push(`Read the first line as column names: ${names.join(', ')}.`) - if (bad.length) - notes.push( - `Read ${plural(bad.length, 'value')} that ${bad.length === 1 ? "isn't a number" : "aren't numbers"} as missing: line ${listSample([...new Set(bad)])}.` - ) - if (repeated.length) - notes.push( - `Kept the last line for ${plural(repeated.length, 'repeated node')}: ${listSample(repeated)}.` - ) - return { - colors: new Map(), - numeric: true, - columns: names, - values, - notes, - summary: `${plural(values.size, 'node')}, ${names.length === 1 ? `values (${names[0]})` : `${names.length} value columns`}`, - } -} diff --git a/frontend/src/clustering/values.test.ts b/frontend/src/clustering/values.test.ts deleted file mode 100644 index b4f9a5c..0000000 --- a/frontend/src/clustering/values.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect, test } from 'vitest' -import { numericShare, parseNumber, parseNumericValues } from './mcl' - -test('parseNumber: numbers, decimal commas, missing markers, junk', () => { - expect(parseNumber('1.5')).toBe(1.5) - expect(parseNumber('-2,5')).toBe(-2.5) - expect(parseNumber(' 3e2 ')).toBe(300) - expect(parseNumber('NA')).toBeNull() - expect(parseNumber('')).toBeNull() - expect(parseNumber('abc')).toBeUndefined() -}) - -test('numericShare counts rows whose value columns are all numbers', () => { - expect(numericShare(['A\t1', 'B\t2', 'C\tred'])).toBeCloseTo(2 / 3) - expect(numericShare(['A'])).toBe(0) -}) - -test('parseNumericValues reads a header, names columns and reports bad values', () => { - const r = parseNumericValues('node\tlog2fc\tpval\nA\t1.5\t0.01\nB\tx\t0.2\nB\t-1\t0.3\n') - expect(r.values.get('A')).toEqual({ log2fc: 1.5, pval: 0.01 }) - expect(r.values.get('B')).toEqual({ log2fc: -1, pval: 0.3 }) - expect(r.notes.join(' ')).toMatch(/column names: log2fc, pval/) - expect(r.notes.join(' ')).toMatch(/isn't a number/) - expect(r.notes.join(' ')).toMatch(/repeated node/) -}) - -test('parseNumericValues without a header names the single column "value"', () => { - const r = parseNumericValues('A\t1\nB\t2\n') - expect(r.values.get('B')).toEqual({ value: 2 }) -}) - -test('parseNumericValues throws when nothing is numeric', () => { - expect(() => parseNumericValues('A\tred\n')).toThrow(/No numeric values/) -}) diff --git a/frontend/src/clustering/walktrap.ts b/frontend/src/clustering/walktrap.ts deleted file mode 100644 index 4657c8d..0000000 --- a/frontend/src/clustering/walktrap.ts +++ /dev/null @@ -1,198 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { WALKTRAP_NODE_LIMIT, partitionStats } from '../welcome' - -/* ---------- Walktrap ---------- */ -export function walktrap(g, opts = {}) { - const t = opts.steps ?? 4 - const n = g.n - if (n > WALKTRAP_NODE_LIMIT) - throw new Error( - `Walktrap is limited to ${WALKTRAP_NODE_LIMIT.toLocaleString('en-US')} nodes in the browser.` - ) - if (!g.m) - return partitionStats( - g, - Int32Array.from({ length: n }, (_, i) => i) - ) - // random walk with a self-loop on every node (as in the original method) - const deg = g.adj.map((a) => a.length + 1) - const step = (vec) => { - const out = new Map() - vec.forEach((p, u) => { - const share = p / deg[u] - out.set(u, (out.get(u) || 0) + share) - const a = g.adj[u] - for (let k = 0; k < a.length; k++) out.set(a[k], (out.get(a[k]) || 0) + share) - }) - return out - } - // P^t for every node (sparse) - const P = new Array(n) - for (let i = 0; i < n; i++) { - let v = new Map([[i, 1]]) - for (let s = 0; s < t; s++) v = step(v) - // negligible probabilities are dropped to keep memory in check - v.forEach((p, k) => { - if (p < 1e-7) v.delete(k) - }) - P[i] = v - } - const dist2 = (a, b) => { - // sum_k (a_k - b_k)^2 / d_k - let s = 0 - a.forEach((x, k) => { - const y = b.get(k) || 0 - s += ((x - y) * (x - y)) / deg[k] - }) - b.forEach((y, k) => { - if (!a.has(k)) s += (y * y) / deg[k] - }) - return s - } - const size = new Int32Array(2 * n).fill(0) - for (let i = 0; i < n; i++) size[i] = 1 - const vec = P // community vectors, extended as merges happen - const nbrs = Array.from({ length: n }, (_, i) => new Map()) // community -> Map(neighbour -> delta sigma) - const alive = new Uint8Array(2 * n) - for (let i = 0; i < n; i++) alive[i] = 1 - // min-heap of [delta, a, b] - const heap = [] - const push = (item) => { - heap.push(item) - let i = heap.length - 1 - while (i > 0) { - const p = (i - 1) >> 1 - if (heap[p][0] <= heap[i][0]) break - ;[heap[p], heap[i]] = [heap[i], heap[p]] - i = p - } - } - const pop = () => { - const top = heap[0] - const last = heap.pop() - if (heap.length) { - heap[0] = last - let i = 0 - for (;;) { - const l = 2 * i + 1, - r = l + 1 - let m = i - if (l < heap.length && heap[l][0] < heap[m][0]) m = l - if (r < heap.length && heap[r][0] < heap[m][0]) m = r - if (m === i) break - ;[heap[m], heap[i]] = [heap[i], heap[m]] - i = m - } - } - return top - } - const deltaSigma = (a, b) => - (((size[a] * size[b]) / (size[a] + size[b])) * dist2(vec[a], vec[b])) / n - for (let u = 0; u < n; u++) { - g.adj[u].forEach((v) => { - if (v <= u) return - const d = deltaSigma(u, v) - nbrs[u].set(v, d) - nbrs[v].set(u, d) - push([d, u, v]) - }) - } - // modularity bookkeeping on the original graph - const twoM = 2 * g.m - const inW = new Float64Array(2 * n), - totW = new Float64Array(2 * n) - for (let i = 0; i < n; i++) totW[i] = g.adj[i].length - const between = Array.from({ length: n }, (_, u) => { - const mp = new Map() - g.adj[u].forEach((v) => mp.set(v, (mp.get(v) || 0) + 1)) - return mp - }) - let q = 0 - for (let i = 0; i < n; i++) q -= Math.pow(totW[i] / twoM, 2) - const merges = [] - let bestQ = q, - bestStep = 0 - let next = n - while (heap.length) { - const [d, a, b] = pop() - if (!alive[a] || !alive[b] || nbrs[a].get(b) !== d) continue - const c = next++ - alive[a] = 0 - alive[b] = 0 - alive[c] = 1 - size[c] = size[a] + size[b] - const va = vec[a], - vb = vec[b] - const vc = new Map() - va.forEach((x, k) => vc.set(k, (x * size[a]) / size[c])) - vb.forEach((x, k) => vc.set(k, (vc.get(k) || 0) + (x * size[b]) / size[c])) - vec[c] = vc - vec[a] = null - vec[b] = null - // modularity change - const eab = between[a].get(b) || 0 - q += (2 * eab) / twoM - (2 * totW[a] * totW[b]) / (twoM * twoM) - inW[c] = inW[a] + inW[b] + 2 * eab - totW[c] = totW[a] + totW[b] - const bc = new Map() - ;[a, b].forEach((x) => - between[x].forEach((w, y) => { - if (y !== a && y !== b) bc.set(y, (bc.get(y) || 0) + w) - }) - ) - between[c] = bc - bc.forEach((w, y) => { - between[y].delete(a) - between[y].delete(b) - between[y].set(c, w) - }) - merges.push([a, b, c]) - if (q > bestQ + 1e-12) { - bestQ = q - bestStep = merges.length - } - // new distances to neighbouring communities (Lance-Williams where possible) - const nc = new Map() - const neighbours = new Set([...nbrs[a].keys(), ...nbrs[b].keys()]) - neighbours.delete(a) - neighbours.delete(b) - neighbours.forEach((x) => { - let dnew - if (nbrs[a].has(x) && nbrs[b].has(x)) { - dnew = - ((size[a] + size[x]) * nbrs[a].get(x) + - (size[b] + size[x]) * nbrs[b].get(x) - - size[x] * d) / - (size[a] + size[b] + size[x]) - } else { - dnew = deltaSigma(c, x) - } - nc.set(x, dnew) - nbrs[x].delete(a) - nbrs[x].delete(b) - nbrs[x].set(c, dnew) - push([dnew, Math.min(c, x), Math.max(c, x)]) - }) - nbrs[c] = nc - nbrs[a] = new Map() - nbrs[b] = new Map() - } - // replay merges up to the step with the highest modularity - const parent = Int32Array.from({ length: 2 * n }, (_, i) => i) - const find = (x) => { - while (parent[x] !== x) { - parent[x] = parent[parent[x]] - x = parent[x] - } - return x - } - for (let s = 0; s < bestStep; s++) { - const [a, b, c] = merges[s] - parent[a] = c - parent[b] = c - } - const raw = new Int32Array(n) - for (let i = 0; i < n; i++) raw[i] = find(i) - return partitionStats(g, raw) -} diff --git a/frontend/src/clustering/wiring.ts b/frontend/src/clustering/wiring.ts deleted file mode 100644 index f1091ed..0000000 --- a/frontend/src/clustering/wiring.ts +++ /dev/null @@ -1,377 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { VALUE_SCALES } from './mcl' -import { applyValueColors, scheduleLegend, scheduleSeparation } from './mapping' -import { benchmarkTsv, runLayoutBenchmark } from '../benchmark' -import { buildGroupLegend } from '../hulls' -import { cy, onCommitStyle } from '../cy' - -/* ---------- wiring: values, legend, separation, benchmark ---------- */ -export function updateValueScalePreview() { - const sc = VALUE_SCALES[document.getElementById('valueScale').value] || VALUE_SCALES['div-rdbu'] - document.getElementById('valueScalePreview').style.background = - `linear-gradient(90deg, ${sc.stops.join(', ')})` -} - -/* ============================================================ - BUBBLE SETS - Group contours that hug their members (after Collins, Penn and - Carpendale, "Bubble Sets", IEEE TVCG 2009). For each group an energy - field is built on a grid: positive around member nodes and along - "virtual edges" that tie the members together (a minimum spanning - tree, so the contour stays one piece), negative around nodes that are - not members, so the contour bends away from them. The outline is the - iso-line of that field (marching squares), smoothed. - Works in any coordinate system: pass positions and radii in the units - the result should be drawn in. - ============================================================ */ -const BUBBLE_THRESHOLD = 0.45 - -// members / others: [{ x, y, r }]; opts: { margin, edgeRadius, avoid, cell } -export function bubbleContour(members, others, opts) { - if (!members.length) return [] - const margin = opts.margin, - edgeR = opts.edgeRadius, - avoid = opts.avoid - let x1 = Infinity, - y1 = Infinity, - x2 = -Infinity, - y2 = -Infinity, - maxR = 0 - members.forEach((p) => { - x1 = Math.min(x1, p.x - p.r) - y1 = Math.min(y1, p.y - p.r) - x2 = Math.max(x2, p.x + p.r) - y2 = Math.max(y2, p.y + p.r) - maxR = Math.max(maxR, p.r) - }) - const pad = margin * 1.6 + 2 - x1 -= pad - y1 -= pad - x2 += pad - y2 += pad - // grid resolution: fine enough for the margin, capped for speed - let cell = opts.cell || Math.max(margin / 3, 0.5) - const maxCells = opts.maxCells || 250000 - while (((x2 - x1) / cell) * ((y2 - y1) / cell) > maxCells) cell *= 1.25 - const nx = Math.ceil((x2 - x1) / cell) + 1, - ny = Math.ceil((y2 - y1) / cell) + 1 - const F = new Float32Array(nx * ny) - - // positive energy around members: 1 inside the node, fading to 0 at `margin` - const splatDisc = (p, reach, weight) => { - const R = p.r + reach - const i0 = Math.max(0, Math.floor((p.x - R - x1) / cell)), - i1 = Math.min(nx - 1, Math.ceil((p.x + R - x1) / cell)) - const j0 = Math.max(0, Math.floor((p.y - R - y1) / cell)), - j1 = Math.min(ny - 1, Math.ceil((p.y + R - y1) / cell)) - for (let j = j0; j <= j1; j++) { - const gy = y1 + j * cell, - dy = gy - p.y - for (let i = i0; i <= i1; i++) { - const gx = x1 + i * cell, - dx = gx - p.x - const d = Math.sqrt(dx * dx + dy * dy) - p.r - if (d >= reach) continue - const t = d <= 0 ? 1 : 1 - d / reach - F[j * nx + i] += weight * t * t - } - } - } - members.forEach((p) => splatDisc(p, margin, 1)) - - // virtual edges: a minimum spanning tree over the members (Prim) - const m = members.length - const segs = [] - if (m > 1 && m <= 2500) { - const inTree = new Uint8Array(m), - best = new Float64Array(m).fill(Infinity), - from = new Int32Array(m).fill(-1) - best[0] = 0 - for (let k = 0; k < m; k++) { - let u = -1, - bu = Infinity - for (let v = 0; v < m; v++) - if (!inTree[v] && best[v] < bu) { - bu = best[v] - u = v - } - if (u < 0) break - inTree[u] = 1 - if (from[u] >= 0) segs.push([members[from[u]], members[u]]) - const pu = members[u] - for (let v = 0; v < m; v++) { - if (inTree[v]) continue - const d = Math.hypot(members[v].x - pu.x, members[v].y - pu.y) - if (d < best[v]) { - best[v] = d - from[v] = u - } - } - } - } else if (opts.links) { - opts.links.forEach(([a, b]) => segs.push([a, b])) - } - segs.forEach(([a, b]) => { - const R = edgeR - const i0 = Math.max(0, Math.floor((Math.min(a.x, b.x) - R - x1) / cell)), - i1 = Math.min(nx - 1, Math.ceil((Math.max(a.x, b.x) + R - x1) / cell)) - const j0 = Math.max(0, Math.floor((Math.min(a.y, b.y) - R - y1) / cell)), - j1 = Math.min(ny - 1, Math.ceil((Math.max(a.y, b.y) + R - y1) / cell)) - const vx = b.x - a.x, - vy = b.y - a.y, - L2 = vx * vx + vy * vy || 1 - for (let j = j0; j <= j1; j++) { - const gy = y1 + j * cell - for (let i = i0; i <= i1; i++) { - const gx = x1 + i * cell - const t = Math.max(0, Math.min(1, ((gx - a.x) * vx + (gy - a.y) * vy) / L2)) - const d = Math.hypot(gx - (a.x + t * vx), gy - (a.y + t * vy)) - if (d >= R) continue - const s = 1 - d / R - // take the stronger of node and edge energy so tree links don't bloat the nodes - const idx = j * nx + i - F[idx] = Math.max(F[idx], s * s) - } - } - }) - - // negative energy around non-members, strongest at their rim - others.forEach((p) => { - if ( - p.x + p.r + avoid < x1 || - p.x - p.r - avoid > x2 || - p.y + p.r + avoid < y1 || - p.y - p.r - avoid > y2 - ) - return - const R = p.r + avoid - const i0 = Math.max(0, Math.floor((p.x - R - x1) / cell)), - i1 = Math.min(nx - 1, Math.ceil((p.x + R - x1) / cell)) - const j0 = Math.max(0, Math.floor((p.y - R - y1) / cell)), - j1 = Math.min(ny - 1, Math.ceil((p.y + R - y1) / cell)) - for (let j = j0; j <= j1; j++) { - const gy = y1 + j * cell, - dy = gy - p.y - for (let i = i0; i <= i1; i++) { - const gx = x1 + i * cell, - dx = gx - p.x - const d = Math.sqrt(dx * dx + dy * dy) - p.r - if (d >= avoid) continue - const t = d <= 0 ? 1 : 1 - d / avoid - F[j * nx + i] -= 0.9 * t * t - } - } - }) - // members always stay inside their own contour - members.forEach((p) => { - const i = Math.round((p.x - x1) / cell), - j = Math.round((p.y - y1) / cell) - for (let b = -1; b <= 1; b++) - for (let a = -1; a <= 1; a++) { - const ii = i + a, - jj = j + b - if (ii >= 0 && jj >= 0 && ii < nx && jj < ny) F[jj * nx + ii] = Math.max(F[jj * nx + ii], 1) - } - }) - - return marchingSquares(F, nx, ny, x1, y1, cell, BUBBLE_THRESHOLD) -} - -// Iso-lines of a grid as closed loops of points. -function marchingSquares(F, nx, ny, x0, y0, cell, T) { - // edge ids: horizontal edge (i, j)->(i+1, j) = 2 * (j * nx + i); vertical = +1 - const point = new Map() - const edgePoint = (i, j, horizontal) => { - const key = 2 * (j * nx + i) + (horizontal ? 0 : 1) - let p = point.get(key) - if (p) return key - const a = F[j * nx + i] - const b = horizontal ? F[j * nx + i + 1] : F[(j + 1) * nx + i] - const t = Math.abs(b - a) < 1e-9 ? 0.5 : (T - a) / (b - a) - p = horizontal - ? { x: x0 + (i + t) * cell, y: y0 + j * cell } - : { x: x0 + i * cell, y: y0 + (j + t) * cell } - point.set(key, p) - return key - } - const next = new Map() // directed segments: from edge -> to edge - for (let j = 0; j < ny - 1; j++) { - for (let i = 0; i < nx - 1; i++) { - const vtl = F[j * nx + i], - vtr = F[j * nx + i + 1], - vbr = F[(j + 1) * nx + i + 1], - vbl = F[(j + 1) * nx + i] - const tl = vtl >= T, - tr = vtr >= T, - br = vbr >= T, - bl = vbl >= T - const code = (tl ? 8 : 0) | (tr ? 4 : 0) | (br ? 2 : 0) | (bl ? 1 : 0) - if (code === 0 || code === 15) continue - const cx0 = x0 + i * cell, - cy0 = y0 + j * cell, - cx1 = cx0 + cell, - cy1 = cy0 + cell - // orient a segment with a reference corner whose side is known: the - // inside always ends up on the same side of the walking direction - const link = (a, b, px, py, inside) => { - const pa = point.get(a), - pb = point.get(b) - const cross = (pb.x - pa.x) * (py - pa.y) - (pb.y - pa.y) * (px - pa.x) - if (cross > 0 === inside) next.set(a, b) - else next.set(b, a) - } - const top = () => edgePoint(i, j, true), - bottom = () => edgePoint(i, j + 1, true) - const left = () => edgePoint(i, j, false), - right = () => edgePoint(i + 1, j, false) - const TL = (s) => (s ? link(left(), top(), cx0, cy0, tl) : null) - switch (code) { - case 1: - case 14: - link(left(), bottom(), cx0, cy1, bl) - break - case 2: - case 13: - link(bottom(), right(), cx1, cy1, br) - break - case 3: - case 12: - link(left(), right(), cx0, cy0, tl) - break - case 4: - case 11: - link(right(), top(), cx1, cy0, tr) - break - case 6: - case 9: - link(bottom(), top(), cx0, cy0, tl) - break - case 7: - case 8: - link(left(), top(), cx0, cy0, tl) - break - case 5: - case 10: { - const centre = (vtl + vtr + vbr + vbl) / 4 >= T - // saddle: the two corners that differ from the centre are cut off - if (centre === tl) { - link(left(), bottom(), cx0, cy1, bl) - link(top(), right(), cx1, cy0, tr) - } else { - link(left(), top(), cx0, cy0, tl) - link(bottom(), right(), cx1, cy1, br) - } - break - } - } - } - } - const loops = [] - const used = new Set() - next.forEach((_, start) => { - if (used.has(start)) return - const loop = [] - let cur = start - while (cur !== undefined && !used.has(cur)) { - used.add(cur) - loop.push(point.get(cur)) - cur = next.get(cur) - } - if (loop.length >= 3) { - loop.closed = cur === start - loops.push(loop) - } - }) - return loops -} - -// Smooth closed path (quadratic curves through edge midpoints), simplified first. -export function bubblePathD(loops, map) { - const f = (v) => Math.round(v * 100) / 100 - let d = '' - loops.forEach((loop) => { - // drop nearly collinear points to keep paths small - const pts = [] - for (let i = 0; i < loop.length; i++) { - const p = map ? map(loop[i]) : loop[i] - const last = pts[pts.length - 1] - if (!last || Math.hypot(p.x - last.x, p.y - last.y) > 0.8) pts.push(p) - } - if (pts.length < 3) return - const n = pts.length - const mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }) - const m0 = mid(pts[n - 1], pts[0]) - d += `M${f(m0.x)},${f(m0.y)}` - for (let i = 0; i < n; i++) { - const p = pts[i], - q = mid(p, pts[(i + 1) % n]) - d += `Q${f(p.x)},${f(p.y)} ${f(q.x)},${f(q.y)}` - } - d += 'Z' - }) - return d -} - -// page wiring, run by main.ts in the original order -export function init() { - ;[ - 'valueColumn', - 'valueTransform', - 'valueScale', - 'valueCenter', - 'valueRange', - 'valueMin', - 'valueMax', - 'valueMissing', - ].forEach((id) => { - const el = document.getElementById(id) - const run = () => { - updateValueScalePreview() - applyValueColors() - } - el.addEventListener('change', run) - el.addEventListener('input', run) - }) - - updateValueScalePreview() - - ;[ - 'legendShow', - 'legendScale', - 'legendGroups', - 'legendShapes', - 'legendChannels', - 'legendTitle', - ].forEach((id) => { - document.getElementById(id).addEventListener('input', scheduleLegend) - document.getElementById(id).addEventListener('change', scheduleLegend) - }) - - document.getElementById('sizeMetric').addEventListener('change', scheduleLegend) - - document.getElementById('showGroupHulls').addEventListener('change', scheduleLegend) - - document.getElementById('groupSortSelect').addEventListener('change', () => { - buildGroupLegend() - scheduleLegend() - }) - - document.getElementById('spreadSlider').addEventListener('input', scheduleSeparation) - - document.getElementById('btnBench').addEventListener('click', runLayoutBenchmark) - - document.getElementById('btnBenchTsv').addEventListener('click', benchmarkTsv) - - { - onCommitStyle(scheduleLegend) - } - - cy.on('data', () => { - if ( - !document.getElementById('legendPanel').hidden || - document.getElementById('legendShow').checked - ) - scheduleLegend() - }) -} diff --git a/frontend/src/config.test.ts b/frontend/src/config.test.ts deleted file mode 100644 index 8c7909f..0000000 --- a/frontend/src/config.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from 'vitest' - -test('NORMA_CFG merges window.NORMA_CONFIG over the defaults', async () => { - ;(window as unknown as { NORMA_CONFIG: unknown }).NORMA_CONFIG = { - site: { name: 'Test NORMA' }, - app: { maxNodes: 10 }, - features: { relays: { string: false } }, - } - const { NORMA_CFG } = await import('./config') - expect(NORMA_CFG.site.name).toBe('Test NORMA') - expect(NORMA_CFG.site.contactName).toBe('Pavlopoulos Lab') - expect(NORMA_CFG.app.maxNodes).toBe(10) - expect(NORMA_CFG.app.theme).toBe('white') - expect(NORMA_CFG.features.relays).toEqual({ string: false, arena3d: true, databases: true }) - expect((NORMA_CFG as { fromServer?: boolean }).fromServer).toBe(true) -}) diff --git a/frontend/src/config.ts b/frontend/src/config.ts deleted file mode 100644 index 535ff06..0000000 --- a/frontend/src/config.ts +++ /dev/null @@ -1,57 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import cytoscape from 'cytoscape' - -/* ============================================================ - SETTINGS - window.NORMA_CONFIG comes from server.py (/norma-config.js) or from a - norma-config.js file next to norma.html; everything has a default, so - NORMA also runs with no settings at all. - ============================================================ */ -export const NORMA_CFG = (() => { - const d = { - mode: 'local', - version: '3.0', - publicUrl: '', - features: { - restApi: null, - relays: { string: true, arena3d: true, databases: true }, - apiTtlHours: 24, - }, - site: { - name: 'NORMA', - institution: '', - contactName: 'Pavlopoulos Lab', - contactEmail: '', - contactUrl: 'https://github.com/PavlopoulosLab/NORMA/issues', - sourceUrl: 'https://github.com/PavlopoulosLab/NORMA', - licenceName: 'MIT License', - licenceUrl: 'https://opensource.org/licenses/MIT', - privacyUrl: '', - imprintUrl: '', - maintainedUntil: '', - notice: '', - testedBrowsers: [], - }, - app: { maxNodes: 10000, theme: 'white', startTab: 'welcome', cdnFallback: true }, - } - const c = (typeof window.NORMA_CONFIG === 'object' && window.NORMA_CONFIG) || {} - const merge = (a, b) => { - Object.keys(b || {}).forEach((k) => { - if ( - b[k] && - typeof b[k] === 'object' && - !Array.isArray(b[k]) && - a[k] && - typeof a[k] === 'object' - ) - merge(a[k], b[k]) - else if (b[k] !== undefined && b[k] !== null) a[k] = b[k] - }) - return a - } - merge(d, c) - d.served = /^https?:$/.test(location.protocol) - d.fromServer = d.served && !!c.features // server.py writes "features" - return d -})() diff --git a/frontend/src/contours.ts b/frontend/src/contours.ts deleted file mode 100644 index 52fc57a..0000000 --- a/frontend/src/contours.ts +++ /dev/null @@ -1,551 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EDGE_TYPES } from './palette' -import { NORMA_CFG } from './config' -import { S } from './state' -import { - UNGROUPED, - effectiveGroupsFor, - getUsedGroups, - groupLabel, - nodeFillMode, -} from './network_state' -import { activeView } from './profiler' -import { bubbleContour, bubblePathD } from './clustering/wiring' -import { buildCompareArena3dModel } from './enrichment' -import { cy } from './cy' -import { dataVersion } from './demo_downloads' -import { downloadText, fileStem, plural, setStatus } from './layouts/controls' -import { drawGroupHulls, hexToRgba, hullCanvasCss } from './hulls' -import { edgeIsDirected } from './export/dialog' -import { resolveStringRoute } from './string/requests' -import { rgbOf } from './view3d/state' -import { shownEdges, shownNodes } from './metrics' - -/* ---------- 2D: cached contours in model coordinates ---------- */ -var bubbleCache = { key: '', paths: new Map(), computedAt: 0, timer: null } - -function bubbleSettings(zoom) { - // screen-pixel margins turned into model units at this zoom - const mScale = Math.max(0.15, Math.min(1, zoom)) - return { - margin: (36 * mScale) / zoom, // contour about 12 px outside the nodes - edgeRadius: (24 * mScale) / zoom, // links about 8 px wide on each side - avoid: (14 * mScale) / zoom, - } -} - -function bubbleKey(nodes, zoom) { - let h = 0 - nodes.forEach((n, i) => { - const p = n.position() - h = - (h * 31 + Math.round(p.x * 2) * 7 + Math.round(p.y * 2) * 13 + Math.round(n.width()) + i) % - 1000000007 - }) - const zb = Math.round(Math.log2(zoom) * 4) - return `${dataVersion}|${zb}|${h}|${nodes.length}|${[...S.activeGroups].join('\u0001')}` -} - -// Contours for every ticked group, as SVG path data in model coordinates. -export function bubblePathsModel(force) { - const shown = cy.nodes().filter((n) => !n.hasClass('hidden-group')) - const zoom = cy.zoom() - const key = bubbleKey(shown, zoom) - if (!force && key === bubbleCache.key) return bubbleCache.paths - // while things move, recompute at most every 120 ms (large networks: only - // once they stop) and redraw when they stop - const now = performance.now() - const big = shown.length > 1500 - const sameData = bubbleCache.key.split('|')[0] === String(dataVersion) - if (!force && bubbleCache.key && sameData && (big || now - bubbleCache.computedAt < 120)) { - clearTimeout(bubbleCache.timer) - // redraw inside an animation frame: a canvas drawn from a bare timer is - // not always shown until something else repaints (seen in Chrome on macOS) - bubbleCache.timer = setTimeout( - () => - requestAnimationFrame(() => { - bubbleCache.computedAt = 0 - bubbleCache.settled = true - drawGroupHulls() - }), - big ? 300 : 140 - ) - if (!(big && bubbleCache.settled)) return bubbleCache.paths - } - bubbleCache.settled = false - const zb = Math.pow(2, Math.round(Math.log2(zoom) * 4) / 4) - const opts = bubbleSettings(zb) - const all = shown.map((n) => ({ - id: n.id(), - x: n.position().x, - y: n.position().y, - r: n.width() / 2, - groups: effectiveGroupsFor(n), - })) - const paths = new Map() - getUsedGroups() - .filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - .forEach((g) => { - const members = all.filter((p) => p.groups.includes(g)) - if (!members.length) return - const others = all.filter((p) => !p.groups.includes(g)) - const loops = bubbleContour(members, others, opts) - paths.set(g, bubblePathD(loops)) - }) - bubbleCache.key = key - bubbleCache.paths = paths - bubbleCache.computedAt = now - return paths -} - -export function drawBubbleSets(ctx, opacity) { - const paths = bubblePathsModel(false) - const z = cy.zoom(), - pan = cy.pan() - // the scale the canvas was actually sized with, not a pixel ratio that - // may have changed since - const dpr = hullCanvasCss.dpr || 1 - ctx.save() - ctx.setTransform(z * dpr, 0, 0, z * dpr, pan.x * dpr, pan.y * dpr) - paths.forEach((d, g) => { - if (!d) return - const color = S.nodeColorMap[g] || '#888888' - const path = new Path2D(d) - ctx.fillStyle = hexToRgba(color, opacity) - ctx.fill(path, 'evenodd') - ctx.lineWidth = 1.6 / z - ctx.strokeStyle = hexToRgba(color, Math.min(1, opacity + 0.45)) - ctx.stroke(path) - }) - ctx.restore() -} - -/* ============================================================ - ARENA3D EXPORT - Arena3D (https://arena3d.org) shows multilayer networks in 3D. Each - ticked group of the current view becomes one layer; nodes keep their - 2D positions inside their layer, edges inside a group stay in its layer - and edges between groups run between layers. A node in several groups - appears once in each of its layers. - Three outputs: - - Arena3D JSON (Arena3D's own export format) - - Arena3D network file (SourceNode, SourceLayer, TargetNode, - TargetLayer, Weight, Channel) - - Open in Arena3D: the JSON is POSTed to /api/external, - which answers { token, url }; the url is opened in a new tab. - ============================================================ */ -export const ARENA3D_LAYER_SPACING = 960 - -export const ARENA3D_LAYER_WIDTH = 947 - -const ARENA3D_COPY_CHANNEL = 'same node' - -export const ARENA3D_MAX_LAYERS = 20 - -function arena3dBase() { - const raw = (document.getElementById('arenaAddress').value || 'https://arena3d.org') - .trim() - .replace(/\/+$/, '') - return /^https?:\/\//i.test(raw) ? raw : 'https://' + raw -} - -export function hexColor(c) { - const [r, g, b] = rgbOf(c) - const h = (x) => - Math.max(0, Math.min(255, Math.round(x))) - .toString(16) - .padStart(2, '0') - return '#' + h(r) + h(g) + h(b) -} - -// Arena3D layer names can't contain underscores (node ids are name_layer). -export function arenaSafe(s) { - return ( - String(s) - .replace(/_/g, '-') - .replace(/[\t\r\n]+/g, ' ') - .trim() || '-' - ) -} - -function buildArena3dModel() { - const connectCopies = document.getElementById('arenaCopies').checked - const colorMode = document.getElementById('arenaNodeColors').value - const nodes = shownNodes() - const edges = shownEdges(true).filter((e) => e.data('source') !== e.data('target')) - if (!nodes.length) throw new Error('Show a network first.') - - // layers: ticked groups in legend order, then one for nodes in no group - const groupOrder = getUsedGroups().filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - const layersOf = new Map() - let ungrouped = false - nodes.forEach((n) => { - let gs = effectiveGroupsFor(n).filter((g) => g !== UNGROUPED) - if (!gs.length) { - gs = [UNGROUPED] - ungrouped = true - } - layersOf.set(n.id(), gs) - }) - let layerKeys = [...groupOrder.filter((g) => nodes.some((n) => layersOf.get(n.id()).includes(g)))] - if (ungrouped) layerKeys.push(UNGROUPED) - // Arena3D takes up to 20 layers: keep the first 20 (group list order) - let droppedLayers = 0, - droppedNodes = 0 - if (layerKeys.length > ARENA3D_MAX_LAYERS) { - droppedLayers = layerKeys.length - ARENA3D_MAX_LAYERS - layerKeys = layerKeys.slice(0, ARENA3D_MAX_LAYERS) - const kept = new Set(layerKeys) - ;[...layersOf].forEach(([id, gs]) => { - const left = gs.filter((g) => kept.has(g)) - if (left.length) layersOf.set(id, left) - else { - layersOf.delete(id) - droppedNodes++ - } - }) - } - const usedNames = new Set() - const layerName = new Map( - layerKeys.map((g) => { - let name = arenaSafe(g === UNGROUPED ? 'No group' : groupLabel(g)) - while (usedNames.has(name)) name += '′' - usedNames.add(name) - return [g, name] - }) - ) - const nodeName = (id) => arenaSafe(id) - - // positions: each layer's nodes are fitted into the layer's square - const layers = layerKeys.map((g, i) => ({ - key: g, - name: layerName.get(g), - position_x: String((i - (layerKeys.length - 1) / 2) * ARENA3D_LAYER_SPACING), - position_y: '0', - position_z: '0', - last_layer_scale: '1', - rotation_x: '0', - rotation_y: '0', - rotation_z: '0', - floor_current_color: g === UNGROUPED ? '#777777' : hexColor(S.nodeColorMap[g] || '#777777'), - geometry_parameters_width: String(ARENA3D_LAYER_WIDTH), - })) - const outNodes = [] - const half = ARENA3D_LAYER_WIDTH * 0.42 - layers.forEach((layer) => { - const members = nodes.filter( - (n) => layersOf.has(n.id()) && layersOf.get(n.id()).includes(layer.key) - ) - const xs = members.map((n) => n.position('x')), - ys = members.map((n) => n.position('y')) - const cx = (Math.min(...xs) + Math.max(...xs)) / 2, - cy0 = (Math.min(...ys) + Math.max(...ys)) / 2 - const span = Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)) || 1 - const k = members.length > 1 ? (2 * half) / span : 0 - members.forEach((n) => { - // pie nodes (several groups) take the color of the layer they are drawn in - const multi = nodeFillMode() === 'groups' && effectiveGroupsFor(n).length > 1 - const color = - (colorMode === 'group' || multi) && layer.key !== UNGROUPED - ? S.nodeColorMap[layer.key] || '#777777' - : n.style('background-color') - const attrs = n.data('attrs') || {} - outNodes.push({ - name: nodeName(n.id()), - layer: layer.name, - position_x: '0', - position_y: String(-(n.position('y') - cy0) * k), - position_z: String((n.position('x') - cx) * k), - scale: String(Math.round(Math.max(0.3, Math.min(3, (n.width() || 42) / 42)) * 100) / 100), - color: hexColor(color), - url: - typeof attrs.url === 'string' - ? attrs.url - : attrs.stringId - ? `https://string-db.org/network/${attrs.stringId}` - : '', - descr: typeof attrs.description === 'string' ? attrs.description : '', - }) - }) - }) - - // edges - const weights = edges.map((e) => e.data('weight')).filter((w) => typeof w === 'number') - const weighted = weights.length > 0 - const wMin = weighted ? Math.min(...weights) : 0, - wMax = weighted ? Math.max(...weights) : 1 - const opacityOf = (w) => { - if (!weighted || typeof w !== 'number' || wMax === wMin) return 1 - return Math.round((0.2 + (0.8 * (w - wMin)) / (wMax - wMin)) * 1000) / 1000 - } - const multiChannel = new Set(edges.map((e) => e.data('type'))).size > 1 - const outEdges = [] - const rows = [] - const seen = new Set() - let interLayer = 0 - edges.forEach((e) => { - const s = e.data('source'), - t = e.data('target') - const ls = layersOf.get(s), - lt = layersOf.get(t) - if (!ls || !lt) return - const shared = ls.filter((g) => lt.includes(g)) - const pairs = shared.length ? shared.map((g) => [g, g]) : [[ls[0], lt[0]]] - if (!shared.length) interLayer++ - const channelLabel = (EDGE_TYPES[e.data('type')] || { label: e.data('type') }).label - const channel = multiChannel ? arenaSafe(channelLabel) : '' - pairs.forEach(([gs, gt]) => { - const src = `${nodeName(s)}_${layerName.get(gs)}`, - trg = `${nodeName(t)}_${layerName.get(gt)}` - const key = [src, trg, channel].join('\u0000') - if (seen.has(key)) return - seen.add(key) - outEdges.push({ - src, - trg, - opacity: String(opacityOf(e.data('weight'))), - color: hexColor(e.style('line-color') || e.data('color')), - channel, - }) - rows.push([ - nodeName(s), - layerName.get(gs), - nodeName(t), - layerName.get(gt), - typeof e.data('weight') === 'number' ? e.data('weight') : 1, - channel || '1', - ]) - }) - }) - // optional edges linking the copies of a node in different layers - let copyEdges = 0 - if (connectCopies) { - layersOf.forEach((gs, id) => { - for (let i = 0; i + 1 < gs.length; i++) { - const src = `${nodeName(id)}_${layerName.get(gs[i])}`, - trg = `${nodeName(id)}_${layerName.get(gs[i + 1])}` - outEdges.push({ - src, - trg, - opacity: '0.6', - color: '#FFFFFF', - channel: multiChannel ? ARENA3D_COPY_CHANNEL : '', - }) - rows.push([ - nodeName(id), - layerName.get(gs[i]), - nodeName(id), - layerName.get(gs[i + 1]), - 1, - multiChannel ? ARENA3D_COPY_CHANNEL : '1', - ]) - copyEdges++ - } - }) - } - const json = { - scene: { - position_x: '0', - position_y: '0', - scale: String( - layers.length > 4 ? Math.round(((0.6561 * 4) / layers.length) * 1e4) / 1e4 : 0.6561 - ), - color: '#000000', - rotation_x: '0.261799387799149', - rotation_y: '0.261799387799149', - rotation_z: '0.0872664625997165', - }, - layers: layers.map(({ key, ...rest }) => rest), - nodes: outNodes, - edges: outEdges, - universalLabelColor: '#FFFFFF', - direction: edges.some((e) => edgeIsDirected(e)), - edgeOpacityByWeight: weighted, - edgeWidthByWeight: false, - } - const multiLayerNodes = [...layersOf.values()].filter((gs) => gs.length > 1).length - return { - json, - rows, - stats: { - layers: layers.length, - nodes: outNodes.length, - edges: outEdges.length, - interLayer, - copyEdges, - multiLayerNodes, - distinctNodes: layersOf.size, - droppedLayers, - droppedNodes, - }, - } -} - -function arena3dNetworkText(rows) { - return ( - [ - 'SourceNode\tSourceLayer\tTargetNode\tTargetLayer\tWeight\tChannel', - ...rows.map((r) => r.join('\t')), - ].join('\n') + '\n' - ) -} - -function arena3dSummary(st) { - const parts = [ - `${plural(st.layers, 'layer')}, ${plural(st.nodes, 'node')} (${st.distinctNodes.toLocaleString()} distinct), ${plural(st.edges, 'edge')}`, - ] - if (st.interLayer) parts.push(`${plural(st.interLayer, 'edge')} between layers`) - if (st.multiLayerNodes) parts.push(`${plural(st.multiLayerNodes, 'node')} in several layers`) - if (st.droppedLayers) - parts.push( - `Arena3D takes up to ${ARENA3D_MAX_LAYERS} layers, so only the first ${ARENA3D_MAX_LAYERS} groups were exported (${plural(st.droppedLayers, 'group')} left out${st.droppedNodes ? `, with ${plural(st.droppedNodes, 'node')} that ${st.droppedNodes === 1 ? 'is' : 'are'} only in those` : ''})` - ) - return parts.join('; ') -} - -// The current view, or the Network Comparison (`src`), as an Arena3D network. -const ARENA3D_SOURCES = { - view: { - build: async () => buildArena3dModel(), - status: 'arenaStatus', - button: 'btnArenaOpen', - stem: () => { - const v = typeof activeView === 'function' && activeView() - return `arena3d-${v ? fileStem(v.name) : 'network'}` - }, - }, - compare: { - build: async () => buildCompareArena3dModel(), - status: 'cmpArenaStatus', - button: 'btnCmpArenaOpen', - stem: () => 'arena3d-comparison', - }, -} - -export async function exportArena3d(kind, src = 'view') { - const S = ARENA3D_SOURCES[src] - try { - setStatus(S.status, [{ level: 'busy', text: 'Preparing the Arena3D network…', progress: null }]) - const model = await S.build() - const stem = S.stem() - if (kind === 'json') { - downloadText(`${stem}.json`, JSON.stringify(model.json, null, 2)) - setStatus(S.status, [ - { level: 'ok', text: `Saved ${stem}.json: ${arena3dSummary(model.stats)}.` }, - ]) - } else { - downloadText(`${stem}.txt`, arena3dNetworkText(model.rows)) - setStatus(S.status, [ - { - level: 'ok', - text: `Saved ${stem}.txt (Arena3D network file): ${arena3dSummary(model.stats)}. Use Upload Network in Arena3D to open it.`, - }, - ]) - } - } catch (err) { - setStatus(S.status, [{ level: 'error', text: err.message }]) - } -} - -export async function openInArena3d(src = 'view') { - const S = ARENA3D_SOURCES[src] - // open the tab now, while the click still counts as a user action, so - // pop-up blockers let it through; it is pointed at Arena3D once the - // server answers - const tab = window.open('', '_blank') - if (tab) { - try { - tab.document.write( - 'Opening Arena3D…

    Sending the network to Arena3D…

    ' - ) - } catch (e) {} - } - const btn = document.getElementById(S.button) - btn.disabled = true - let model - try { - model = await S.build() - } catch (err) { - if (tab && !tab.closed) tab.close() - btn.disabled = false - setStatus(S.status, [{ level: 'error', text: err.message }]) - return - } - setStatus(S.status, [ - { level: 'busy', text: `Sending ${arena3dSummary(model.stats)} to Arena3D…`, progress: null }, - ]) - try { - const route = - NORMA_CFG.features.relays.arena3d === false ? 'direct' : await resolveStringRoute() - const body = JSON.stringify(model.json) - const url = - route === 'proxy' - ? `arena3d-api/external?upstream=${encodeURIComponent(arena3dBase())}` - : `${arena3dBase()}/api/external` - let response - try { - response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body, - }) - } catch (err) { - throw new Error( - route === 'proxy' - ? `The server relay could not be reached (${err.message}). Check that server.py is still running.` - : `Arena3D could not be reached (${err.message}). Browsers may block direct calls to other sites; run NORMA with server.py, or save the Arena3D JSON and load it in Arena3D yourself.` - ) - } - const text = await response.text() - if (!response.ok) { - if (route === 'proxy' && !/^\s*[\[{]/.test(text) && response.status !== 502) - throw new Error( - 'This server has no Arena3D relay. Start NORMA with server.py, or save the Arena3D JSON instead.' - ) - throw new Error(`Arena3D answered with an error (${response.status}): ${text.slice(0, 200)}`) - } - let answer - try { - answer = JSON.parse(text) - } catch (e) { - throw new Error('Arena3D sent an answer that is not JSON.') - } - if (!answer || !answer.url) throw new Error('Arena3D did not return a link to the network.') - if (tab && !tab.closed) tab.location.href = answer.url - else window.open(answer.url, '_blank') - setStatus(S.status, [ - { level: 'ok', text: `Opened in Arena3D: ${arena3dSummary(model.stats)}.` }, - { - level: 'ok', - text: 'If no new tab appeared, open the link:', - action: { label: 'Open Arena3D', run: () => window.open(answer.url, '_blank') }, - }, - ]) - } catch (err) { - if (tab && !tab.closed) tab.close() - setStatus(S.status, [ - { level: 'error', text: err.message }, - { - level: 'ok', - text: 'You can still save the Arena3D JSON and load it in Arena3D.', - action: { label: 'Save Arena3D JSON', run: () => exportArena3d('json', src) }, - }, - ]) - } finally { - btn.disabled = false - } -} - -// page wiring, run by main.ts in the original order -export function init() { - document.getElementById('btnArenaOpen').addEventListener('click', () => openInArena3d('view')) - - document - .getElementById('btnArenaJson') - .addEventListener('click', () => exportArena3d('json', 'view')) - - document - .getElementById('btnArenaTsv') - .addEventListener('click', () => exportArena3d('tsv', 'view')) -} diff --git a/frontend/src/cy.ts b/frontend/src/cy.ts deleted file mode 100644 index 1e71443..0000000 --- a/frontend/src/cy.ts +++ /dev/null @@ -1,181 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import cytoscape from 'cytoscape' -import { webglPreference } from './enrichment' - -/* ---------- cytoscape setup ---------- */ -/* ---------- stylesheet ---------- - BASE_STYLE is the fixed rule list. Settings change it only through - setStyle(selector, props), which merges into the rule for that selector. - The whole sheet is then rebuilt once per tick, so it never grows no - matter how often sliders move, and rule order (and therefore which rule - wins) stays fixed: node, node.dimmed, ..., edge, ..., edge.highlighted. */ -export const SELECTION_YELLOW = '#facc15' - -const BASE_STYLE = [ - { - selector: 'node', - style: { - 'background-color': 'data(color)', - 'pie-size': '100%', - // Cytoscape supports up to 16 pie slices per node - ...Object.fromEntries( - Array.from({ length: 16 }, (_, i) => [ - [`pie-${i + 1}-background-color`, `data(pieColor${i + 1})`], - [`pie-${i + 1}-background-size`, `data(pieSize${i + 1})`], - ]).flat() - ), - width: 'data(size)', - height: 'data(size)', - shape: 'data(shape)', - label: 'data(id)', - color: '#e7ebf5', - 'font-size': 'data(labelPx)', - 'font-family': 'Inter, sans-serif', - 'text-valign': 'center', - 'text-margin-y': 0, - 'text-outline-width': 3, - 'text-outline-color': '#0f1420', - 'border-width': 2, - 'border-color': 'rgba(255,255,255,0.35)', - 'overlay-opacity': 0, - }, - }, - { - selector: 'node.dimmed', - style: { opacity: 0.12 }, - }, - { - selector: 'node.highlighted', - style: { 'border-color': '#5fd3c4', 'border-width': 3 }, - }, - { - // several nodes can be selected (Shift/Ctrl/Cmd-click or Shift-drag); - // a thick border plus a soft halo keeps them visible at any zoom - selector: 'node:selected', - style: { - 'border-width': 5, - 'border-color': '#facc15', - 'underlay-color': '#facc15', - 'underlay-opacity': 0.35, - 'underlay-padding': 7, - 'underlay-shape': 'ellipse', - }, - }, - { - selector: 'edge', - style: { - 'curve-style': 'haystack', - 'haystack-radius': 0.4, - width: 2, - 'line-color': 'data(color)', - opacity: 0.85, - }, - }, - { - selector: 'edge.hidden-type', - style: { display: 'none' }, - }, - { - selector: 'edge.merged-hidden', - style: { display: 'none' }, - }, - { - selector: 'node.hidden-group', - style: { display: 'none' }, - }, - { - selector: 'edge.dimmed', - style: { opacity: 0.04 }, - }, - { - selector: 'edge.highlighted', - style: { opacity: 1, width: 3.2 }, - }, - { - selector: 'edge:selected', - style: { - 'underlay-color': '#facc15', - 'underlay-opacity': 0.45, - 'underlay-padding': 4, - opacity: 1, - }, - }, - { - selector: 'core', - style: { - 'selection-box-color': '#0f766e', - 'selection-box-opacity': 0.12, - 'selection-box-border-color': '#0f766e', - 'selection-box-border-width': 1, - 'active-bg-opacity': 0, - }, - }, -] - -const DYNAMIC_STYLE = {} - -let styleCommitQueued = false - -function buildStylesheet() { - const seen = new Set() - const sheet = BASE_STYLE.map((rule) => { - seen.add(rule.selector) - return { - selector: rule.selector, - style: { ...rule.style, ...(DYNAMIC_STYLE[rule.selector] || {}) }, - } - }) - Object.keys(DYNAMIC_STYLE).forEach((sel) => { - if (!seen.has(sel)) sheet.push({ selector: sel, style: { ...DYNAMIC_STYLE[sel] } }) - }) - return sheet -} - -// other modules (3D view, legend) react after every style commit -const styleHooks = [] - -export function onCommitStyle(fn) { - styleHooks.push(fn) -} - -function commitStyle() { - styleCommitQueued = false - cy.style().fromJson(buildStylesheet()).update() - styleHooks.forEach((fn) => fn()) -} - -export function setStyle(selector, props) { - DYNAMIC_STYLE[selector] = { ...(DYNAMIC_STYLE[selector] || {}), ...props } - if (!styleCommitQueued) { - styleCommitQueued = true - queueMicrotask(commitStyle) - } -} - -// WebGL drawing is chosen once, when the canvas is created (see Display → Performance). -export const WEBGL_ACTIVE = (() => { - try { - return webglPreference() - } catch (e) { - return false - } -})() - -export const cy = cytoscape({ - container: document.getElementById('cy'), - ...(WEBGL_ACTIVE ? { renderer: { name: 'canvas', webgl: true } } : {}), - elements: [], - // Effectively unlimited zoom. Cytoscape's own limits are 1e-50..1e50, - // but canvas drawing breaks down long before that; a millionfold either - // way is far past anything useful. Panning is kept in bounds by - // clampViewport() below, so the network can't be lost off-screen. - minZoom: 1e-6, - maxZoom: 1e6, - // with panning on, box selection needs Shift (or Ctrl/Cmd/Alt) while dragging - boxSelectionEnabled: true, - selectionType: 'single', - wheelSensitivity: 0.25, - style: BASE_STYLE, - layout: { name: 'grid' }, -}) diff --git a/frontend/src/demo_downloads.ts b/frontend/src/demo_downloads.ts deleted file mode 100644 index 6f5e205..0000000 --- a/frontend/src/demo_downloads.ts +++ /dev/null @@ -1,323 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EDGE_TYPES } from './palette' -import { S } from './state' -import { SAMPLE_GENERATORS } from './sample_data' -import { activeView, restoreView, snapshotData, spreadBase, viewSettings } from './profiler' -import { cy } from './cy' -import { downloadText, libSelection } from './layouts/controls' -import { escapeHtml } from './network_state' -import { getUsedTypes } from './hulls' - -/* ---------- built-in demo downloads (Help > Example files) ---------- */ -// Converts a demo in loadData's JSON shape to NORMA's text formats. -export function demoToNormaTexts(data) { - const weighted = data.edges.some((e) => typeof e.weight === 'number') - const typed = new Set(data.edges.map((e) => e.type || 'link')).size > 1 - const isDirected = (e) => (e.directed === undefined ? !!data.directed : !!e.directed) - const withDirection = data.edges.some(isDirected) - const header = ['Source', 'Target'] - if (weighted) header.push('Weight') - if (typed) header.push('Type') - if (withDirection) header.push('Direction') - const netLines = [header.join('\t')] - data.edges.forEach((e) => { - const row = [e.source, e.target] - if (weighted) row.push(String(typeof e.weight === 'number' ? e.weight : 1)) - if (typed) row.push(e.type || 'link') - if (withDirection) row.push(isDirected(e) ? 'directed' : 'undirected') - netLines.push(row.join('\t')) - }) - const members = new Map() - ;(data.groupOrder || []).forEach((g) => members.set(g, [])) - data.nodes.forEach((n) => { - const groups = n.groups && n.groups.length ? n.groups : n.group ? [n.group] : [] - groups.forEach((g) => { - if (!members.has(g)) members.set(g, []) - members.get(g).push(n.id) - }) - }) - const attrs = data.groupAttrs || {} - const annLines = [...members] - .filter(([, m]) => m.length) - .map(([g, m]) => `${(attrs[g] && attrs[g].label) || g}\t${m.join(',')}`) - return { network: netLines.join('\n') + '\n', annotation: annLines.join('\n') + '\n' } -} - -const DEMO_DOWNLOADS = [ - [ - 'trp', - 'E. coli trp operon, multi-edge', - 'Channels, weights, gene function and EC number attributes, group descriptions', - ], - ['modules50', '4 overlapping modules (50 nodes)', 'Attributes on nodes, edges and groups'], - ['modules100', '4 overlapping modules (100 nodes)', 'Attributes on nodes, edges and groups'], - [ - 'tiny', - 'Random network with edge labels (20 nodes)', - 'Each edge has an interaction attribute, shown as its label', - ], - [ - 'directed', - 'Random directed multi-edge network (40 nodes)', - 'Arrows from source to target, up to three channels per pair, some reciprocal edges; the network file has a Direction column', - ], - ['small', 'Random network (60 nodes)', ''], - ['medium', 'Random network (200 nodes)', ''], - ['large', 'Random network (800 nodes)', ''], - ['massive', 'Random network (5,000 nodes)', ''], -] - -function renderDemoDownloads() { - const root = document.getElementById('helpDemoFiles') - if (!root) return - const table = document.createElement('table') - table.className = 'help-table' - table.innerHTML = 'DemoDownload' - const body = document.createElement('tbody') - DEMO_DOWNLOADS.forEach(([key, title, note]) => { - const tr = document.createElement('tr') - const td1 = document.createElement('td') - td1.innerHTML = `${escapeHtml(title)}${note ? `${escapeHtml(note)}` : ''}` - const td2 = document.createElement('td') - td2.className = 'dl-buttons' - const make = (label, fileName, build) => { - const btn = document.createElement('button') - btn.type = 'button' - btn.className = 'dl-copy' - btn.textContent = label - btn.title = `Download ${fileName}` - btn.addEventListener('click', () => downloadText(fileName, build())) - td2.appendChild(btn) - } - const gen = () => SAMPLE_GENERATORS[key]() - make('Network', `demo-${key}-network.txt`, () => demoToNormaTexts(gen()).network) - make('Groups', `demo-${key}-annotation.txt`, () => demoToNormaTexts(gen()).annotation) - make('JSON', `demo-${key}.json`, () => JSON.stringify(gen(), null, 2)) - tr.append(td1, td2) - body.appendChild(tr) - }) - table.appendChild(body) - root.appendChild(table) -} - -/* ============================================================ - UNDO / REDO - Each view keeps its own history. An entry is the view's full visual - state (settings, node positions, ticked groups and channels, colors, - spread) plus a reference to its data; data snapshots are reused until - the data actually changes, so moving a node doesn't copy the network. - Recording: the first change after a quiet moment pushes the last - settled state onto the undo stack; further changes within the same - burst (slider drags, layout animations) join that entry, and the state - settles once nothing has changed for a moment. Entries that turn out to - change nothing are dropped. Zoom, pan, search and selection are not - part of the history, and neither is the theme. - ============================================================ */ -const HISTORY_LIMIT = 60 - -const HISTORY_SETTLE_MS = 450 - -export let historyTimer = null - -export let dataVersion = 0 - -export function bumpDataVersion() { - dataVersion++ -} - -function viewHistory() { - const v = activeView() - if (!v) return null - if (!v.history) v.history = { undo: [], redo: [], committed: null } - return v.history -} - -function currentDataSnapshot() { - if (S.dataCache.version !== dataVersion) - S.dataCache = { version: dataVersion, data: snapshotData() } - return S.dataCache.data -} - -function captureHistoryState() { - const positions = {} - cy.nodes().forEach((n) => { - const p = n.position() - positions[n.id()] = { x: p.x, y: p.y } - }) - const typeColors = {} - getUsedTypes().forEach((t) => { - if (EDGE_TYPES[t]) typeColors[t] = EDGE_TYPES[t].color - }) - return { - data: currentDataSnapshot(), - selection: { - networks: [...libSelection.networks], - annotation: libSelection.annotation, - colors: libSelection.colors, - }, - state: { - config: viewSettings(), - positions, - activeGroups: [...S.activeGroups], - activeTypes: [...S.activeTypes], - groupColors: { ...S.nodeColorMap }, - typeColors, - libView: S.currentLibView ? { ...S.currentLibView } : null, - spread: parseFloat(document.getElementById('spreadSlider').value) || 0, - // kept in memory only: lets Spread return to the exact positions after undo - spreadBase: spreadBase ? { ...spreadBase } : null, - groupFilter: document.getElementById('groupFilter').value, - }, - } -} - -function sameHistoryState(a, b) { - if (!a || !b || a.data !== b.data) return false - const pa = a.state.positions, - pb = b.state.positions - const ids = Object.keys(pa) - if (ids.length !== Object.keys(pb).length) return false - for (const id of ids) { - const p = pa[id], - q = pb[id] - if (!q || Math.abs(p.x - q.x) > 0.01 || Math.abs(p.y - q.y) > 0.01) return false - } - const strip = (e) => - JSON.stringify({ ...e.state, positions: null, spreadBase: null, sel: e.selection }) - return strip(a) === strip(b) -} - -function layoutStillRunning() { - return ( - S.cyLayoutsRunning > 0 || - document.getElementById('btnRunLayout').disabled || - cy.nodes().animated() - ) -} - -export function scheduleSettle() { - clearTimeout(historyTimer) - historyTimer = setTimeout(settleHistory, HISTORY_SETTLE_MS) -} - -function settleHistory() { - clearTimeout(historyTimer) - historyTimer = null - const h = viewHistory() - if (!h) { - S.historyGestureOpen = false - return - } - if (layoutStillRunning()) { - scheduleSettle() - return - } - const now = captureHistoryState() - if (S.historyGestureOpen && h.undo.length && sameHistoryState(h.undo[h.undo.length - 1], now)) { - h.undo.pop() - } - h.committed = now - S.historyGestureOpen = false - updateUndoButtons() -} - -// Settles right away (used before switching views and before undo/redo). -export function flushHistory() { - if (historyTimer || S.historyGestureOpen) { - clearTimeout(historyTimer) - historyTimer = null - const h = viewHistory() - if (h) { - const now = captureHistoryState() - if (S.historyGestureOpen && h.undo.length && sameHistoryState(h.undo[h.undo.length - 1], now)) - h.undo.pop() - h.committed = now - } - S.historyGestureOpen = false - } -} - -// Call after something the user did changed (or is about to change) the view. -export function noteChange() { - if (S.historySuspended) return - const h = viewHistory() - if (!h) return - if (!S.historyGestureOpen) { - if (h.committed) { - h.undo.push(h.committed) - if (h.undo.length > HISTORY_LIMIT) h.undo.shift() - h.redo.length = 0 - } - S.historyGestureOpen = true - } - scheduleSettle() - updateUndoButtons() -} - -// Records the current state as the starting point (no undo entry). -export function setHistoryBaseline() { - S.historyGestureOpen = false - scheduleSettle() -} - -function applyHistoryState(entry) { - S.historySuspended++ - try { - const reuse = entry.data === currentDataSnapshot() && cy.nodes().length > 0 - restoreView( - { data: entry.data, selection: entry.selection, state: { ...entry.state } }, - { reuseData: reuse } - ) - if (!reuse) S.dataCache = { version: dataVersion, data: entry.data } - } finally { - S.historySuspended-- - } -} - -export function undo() { - flushHistory() - const h = viewHistory() - if (!h || !h.undo.length) return - const target = h.undo.pop() - if (h.committed) h.redo.push(h.committed) - applyHistoryState(target) - h.committed = target - updateUndoButtons() -} - -export function redo() { - flushHistory() - const h = viewHistory() - if (!h || !h.redo.length) return - const target = h.redo.pop() - if (h.committed) h.undo.push(h.committed) - applyHistoryState(target) - h.committed = target - updateUndoButtons() -} - -export function updateUndoButtons() { - const h = viewHistory() - const u = document.getElementById('btnUndo'), - r = document.getElementById('btnRedo') - const canUndo = !!h && h.undo.length > 0 - const canRedo = !!h && h.redo.length > 0 && !S.historyGestureOpen - u.disabled = !canUndo - r.disabled = !canRedo - u.title = canUndo - ? `Undo (Ctrl+Z), ${h.undo.length} step${h.undo.length === 1 ? '' : 's'} available` - : 'Nothing to undo' - r.title = canRedo - ? `Redo (Ctrl+Shift+Z), ${h.redo.length} step${h.redo.length === 1 ? '' : 's'} available` - : 'Nothing to redo' -} - -// page wiring, run by main.ts in the original order -export function init() { - renderDemoDownloads() - - cy.on('layoutstart', () => { - S.cyLayoutsRunning++ - }) -} diff --git a/frontend/src/directed_stats.ts b/frontend/src/directed_stats.ts deleted file mode 100644 index 1ff1705..0000000 --- a/frontend/src/directed_stats.ts +++ /dev/null @@ -1,184 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { PROFILE_PATH_LIMIT, formatStat } from './wiring' -import { escapeHtml } from './network_state' - -/* ---------- directed statistics (Profiler and Compare) ---------- */ -export const PROFILE_DIRECTED_STATS = [ - { key: 'dirEdges', label: 'Directed edges', desc: 'Distinct source → target connections.' }, - { - key: 'reciprocity', - label: 'Reciprocity', - desc: 'Share of directed edges whose reverse edge also exists.', - }, - { key: 'maxIn', label: 'Maximum in-degree', desc: 'Most incoming neighbors of any node.' }, - { key: 'maxOut', label: 'Maximum out-degree', desc: 'Most outgoing neighbors of any node.' }, - { key: 'sources', label: 'Source nodes', desc: 'Nodes with outgoing but no incoming edges.' }, - { key: 'sinks', label: 'Sink nodes', desc: 'Nodes with incoming but no outgoing edges.' }, - { - key: 'scc', - label: 'Strongly connected components', - desc: 'Groups of nodes that can all reach each other along edge directions.', - }, - { - key: 'largestScc', - label: 'Largest strongly connected component', - desc: 'Nodes in the biggest such group, with its share of all nodes.', - }, - { - key: 'reachable', - label: 'Reachable node pairs', - desc: 'Share of ordered pairs (A, B) where B can be reached from A along edge directions.', - }, - { - key: 'dirAvgPath', - label: 'Average directed path length', - desc: 'Mean shortest path following edge directions, over reachable pairs.', - }, - { - key: 'dirDiameter', - label: 'Directed diameter', - desc: 'Longest shortest path following edge directions.', - }, -] - -// edges: [source, target, directed]; undirected edges can be walked both ways -export function profileDirected(ids, edges) { - const n = ids.length - const index = new Map(ids.map((id, i) => [id, i])) - const out = Array.from({ length: n }, () => new Set()) - const arcKeys = new Set() - edges.forEach(([s, t, d]) => { - const a = index.get(s), - b = index.get(t) - if (a === undefined || b === undefined || a === b) return - out[a].add(b) - if (d) arcKeys.add(a + ',' + b) - else out[b].add(a) - }) - const r = {} - r.dirEdges = arcKeys.size - let recip = 0 - arcKeys.forEach((k) => { - const [a, b] = k.split(',') - if (arcKeys.has(b + ',' + a)) recip++ - }) - r.reciprocity = arcKeys.size ? recip / arcKeys.size : NaN - const indeg = new Int32Array(n) - const adj = out.map((set) => Int32Array.from(set)) - adj.forEach((list) => - list.forEach((w) => { - indeg[w]++ - }) - ) - r.maxIn = n ? Math.max(...indeg) : NaN - r.maxOut = n ? Math.max(...adj.map((l) => l.length)) : NaN - r.sources = 0 - r.sinks = 0 - for (let v = 0; v < n; v++) { - if (indeg[v] === 0 && adj[v].length > 0) r.sources++ - if (adj[v].length === 0 && indeg[v] > 0) r.sinks++ - } - - // strongly connected components (iterative Tarjan) - const idx = new Int32Array(n).fill(-1), - low = new Int32Array(n), - onStack = new Uint8Array(n) - const stack = [], - sccSizes = [] - let counter = 0 - for (let root = 0; root < n; root++) { - if (idx[root] >= 0) continue - const call = [[root, 0]] - idx[root] = low[root] = counter++ - stack.push(root) - onStack[root] = 1 - while (call.length) { - const frame = call[call.length - 1] - const v = frame[0] - if (frame[1] < adj[v].length) { - const w = adj[v][frame[1]++] - if (idx[w] < 0) { - idx[w] = low[w] = counter++ - stack.push(w) - onStack[w] = 1 - call.push([w, 0]) - } else if (onStack[w]) low[v] = Math.min(low[v], idx[w]) - } else { - call.pop() - if (call.length) { - const u = call[call.length - 1][0] - low[u] = Math.min(low[u], low[v]) - } - if (low[v] === idx[v]) { - let size = 0, - w - do { - w = stack.pop() - onStack[w] = 0 - size++ - } while (w !== v) - sccSizes.push(size) - } - } - } - } - r.scc = sccSizes.length - const big = sccSizes.length ? Math.max(...sccSizes) : 0 - r.largestScc = n ? `${big.toLocaleString('en-US')} (${((100 * big) / n).toFixed(1)}%)` : '—' - - // directed shortest paths - if (n <= PROFILE_PATH_LIMIT) { - const dist = new Int32Array(n), - queue = new Int32Array(Math.max(1, n)) - let pairs = 0, - total = 0, - diameter = 0 - for (let s = 0; s < n; s++) { - dist.fill(-1) - dist[s] = 0 - let head = 0, - tail = 0 - queue[tail++] = s - while (head < tail) { - const v = queue[head++] - const nb = adj[v] - for (let k = 0; k < nb.length; k++) { - const w = nb[k] - if (dist[w] < 0) { - dist[w] = dist[v] + 1 - queue[tail++] = w - pairs++ - total += dist[w] - if (dist[w] > diameter) diameter = dist[w] - } - } - } - } - r.reachable = n > 1 ? `${((100 * pairs) / (n * (n - 1))).toFixed(1)}%` : '—' - r.dirAvgPath = pairs ? total / pairs : NaN - r.dirDiameter = pairs ? diameter : NaN - } else { - r.reachable = r.dirAvgPath = r.dirDiameter = 'skipped' - } - return r -} - -export function directedStatsTable(results) { - let html = - '
    ' + - results.map((r) => ``).join('') + - '' - PROFILE_DIRECTED_STATS.forEach((st) => { - html += - `` + - results - .map( - (r) => - `` - ) - .join('') + - '' - }) - return html + '
    Statistic${escapeHtml(r.name)}
    ${escapeHtml(st.label)}${escapeHtml(st.desc)}${escapeHtml(r.directed ? formatStat(r.directed[st.key]) : '—')}
    ' -} diff --git a/frontend/src/enrichment.ts b/frontend/src/enrichment.ts deleted file mode 100644 index 0eb4925..0000000 --- a/frontend/src/enrichment.ts +++ /dev/null @@ -1,3078 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import cytoscape from 'cytoscape' -import { - ARENA3D_LAYER_SPACING, - ARENA3D_LAYER_WIDTH, - ARENA3D_MAX_LAYERS, - arenaSafe, - exportArena3d, - hexColor, - openInArena3d, -} from './contours' -import { - BUNDLE_MAX_EDGES, - activeView, - captureActiveView, - createView, - openInNewView, - renderViewBar, - restoreView, - setControls, - snapshotData, - viewSettings, - views, -} from './profiler' -import { EDGE_TYPES, NODE_PALETTES, colorAtIndex } from './palette' -import { - MAX_NETWORK_NODES, - UNGROUPED, - effectiveGroupsFor, - escapeHtml, - groupLabel, - nextAutoEdgeColor, - nodeFillMode, - sanitizeColor, -} from './network_state' -import { NORMA_CFG } from './config' -import { - NORMA_PARSERS, - addNormaEntry, - downloadText, - fileStem, - libEntry, - libSelection, - listSample, - nextPaint, - normaLibrary, - parseNormaNetwork, - plural, - readTextWithProgress, - renderLibraryLists, - setStatus, - startProgress, -} from './layouts/controls' -import { S } from './state' -import { WEBGL_ACTIVE, cy } from './cy' -import { - applyGroupColorOverrides, - benjaminiHochberg, - groupStatsTsv, - hypergeomUpper, - renderGroupStats, -} from './arena3d' -import { applyGroupsAndColorsInPlace, refreshLibraryView } from './library' -import { applyTheme } from './themes' -import { - applyValueColors, - groupSeparation, - valueColumns, - viewHasValues, -} from './clustering/mapping' -import { - bundleAsync, - exportEdges, - exportNodes, - exportShownOnly, - frLayoutAsync, - shownEdges, - shownNodes, -} from './metrics' -import { compareState } from './label_colors' -import { currentTab, louvain, profileGraph, simpleGraph, switchTab } from './wiring' -import { - dataVersion, - demoToNormaTexts, - setHistoryBaseline, - updateUndoButtons, -} from './demo_downloads' -import { edgeIsDirected } from './export/dialog' -import { generateRandomNetwork } from './sample_data' -import { loadData, refreshAllDerivedUI, sortedByName } from './hulls' -import { net3d, requestRender3d, rgbOf } from './view3d/state' -import { openGroupNetwork } from './group_network' -import { readFileText } from './uploads' -import { resolveStringRoute } from './string/requests' - -/* ---------- enrichment ---------- */ -export const groupAnalysisState = { stats: null, enrichment: null } - -function annotationChoices() { - const out = [{ value: 'view', label: 'Groups of the current view' }] - sortedByName(normaLibrary.annotation, (e) => e.name).forEach((e) => - out.push({ value: e.id, label: e.name }) - ) - return out -} - -// Map term -> Set(member ids) for an annotation choice -function termSetsFor(choice) { - const terms = new Map() - if (choice === 'view') { - cy.nodes().forEach((n) => - (n.data('groups') || []).forEach((g) => { - if (g === UNGROUPED) return - const label = groupLabel(g) - if (!terms.has(label)) terms.set(label, new Set()) - terms.get(label).add(n.id()) - }) - ) - return terms - } - const entry = libEntry('annotation', choice) - if (!entry) return terms - entry.parsed.groups.forEach(({ name, members }) => terms.set(name, new Set(members))) - return terms -} - -export function refreshEnrichmentChoices() { - ;['gaTerms', 'gaSetsFrom'].forEach((id) => { - const sel = document.getElementById(id) - const current = sel.value - sel.innerHTML = '' - annotationChoices().forEach((o) => sel.add(new Option(o.label, o.value))) - if ([...sel.options].some((o) => o.value === current)) sel.value = current - else if (id === 'gaTerms') { - // default: terms from a file other than the view's own grouping - const own = S.currentLibView && S.currentLibView.annotation - const other = normaLibrary.annotation.find((e) => e.id !== own) - if (other) sel.value = other.id - } - }) - const groupsMode = document.getElementById('gaSets').value === 'groups' - document.getElementById('gaSetsFromRow').hidden = !groupsMode - document.getElementById('gaSetsFromLabel').hidden = !groupsMode - const selected = cy.nodes(':selected').length - document.getElementById('gaSelectedNote').textContent = - document.getElementById('gaSets').value === 'selected' - ? selected - ? `${plural(selected, 'node')} selected in the view.` - : 'Select nodes in the 2D or 3D view first (Shift-drag or Shift-click).' - : '' -} - -function runEnrichment() { - const setsMode = document.getElementById('gaSets').value - const termChoice = document.getElementById('gaTerms').value - const bgMode = document.getElementById('gaBackground').value - const minOverlap = Math.max(1, parseInt(document.getElementById('gaMinOverlap').value, 10) || 2) - const fdrMax = parseFloat(document.getElementById('gaFdr').value) || 0.05 - const shown = new Set(shownNodes().map((n) => n.id())) - if (!shown.size) { - setStatus('gaEnrichStatus', [{ level: 'error', text: 'Show a network first.' }]) - return - } - const terms = termSetsFor(termChoice) - if (!terms.size) { - setStatus('gaEnrichStatus', [{ level: 'error', text: 'The chosen annotation has no groups.' }]) - return - } - // universe - let universe - if (bgMode === 'view') universe = shown - else { - universe = new Set() - terms.forEach((set) => set.forEach((id) => universe.add(id))) - shown.forEach((id) => universe.add(id)) - } - // test sets - const tests = [] - if (setsMode === 'selected') { - const sel = cy - .nodes(':selected') - .map((n) => n.id()) - .filter((id) => universe.has(id)) - if (!sel.length) { - setStatus('gaEnrichStatus', [ - { - level: 'error', - text: 'Select nodes in the view first (Shift-drag or Shift-click), or test each group of an annotation.', - }, - ]) - return - } - tests.push({ name: 'Selected nodes', members: new Set(sel) }) - } else { - termSetsFor(document.getElementById('gaSetsFrom').value).forEach((set, name) => { - const members = new Set([...set].filter((id) => universe.has(id))) - if (members.size >= minOverlap) tests.push({ name, members }) - }) - } - const N = universe.size - const termList = [...terms] - .map(([name, set]) => ({ name, set: new Set([...set].filter((id) => universe.has(id))) })) - .filter((t) => t.set.size >= minOverlap) - const rows = [] - let tested = 0 - tests.forEach((t) => { - const n = t.members.size - const cand = termList - .map((term) => { - let k = 0 - term.set.forEach((id) => { - if (t.members.has(id)) k++ - }) - return { - term: term.name, - k, - K: term.set.size, - genes: [...term.set].filter((id) => t.members.has(id)), - } - }) - .filter( - (c) => - c.k >= 1 && - !( - setsMode === 'groups' && - c.term === t.name && - document.getElementById('gaSetsFrom').value === termChoice - ) - ) - const ps = cand.map((c) => hypergeomUpper(c.k, N, c.K, n)) - const qs = benjaminiHochberg(ps) - tested += cand.length - cand.forEach((c, i) => { - if (c.k < minOverlap || qs[i] > fdrMax) return - rows.push({ - set: t.name, - n, - term: c.term, - k: c.k, - K: c.K, - N, - fold: c.k / n / (c.K / N), - p: ps[i], - fdr: qs[i], - genes: c.genes, - }) - }) - }) - rows.sort((a, b) => a.fdr - b.fdr || a.p - b.p) - groupAnalysisState.enrichment = { rows, tests: tests.length, tested, N, termChoice, setsMode } - renderEnrichment() - setStatus('gaEnrichStatus', [ - { - level: rows.length ? 'ok' : 'warn', - text: `Tested ${plural(tested, 'term–set pair')} in ${plural(tests.length, 'set')} against a background of ${plural(N, 'node')}: ${rows.length ? `${plural(rows.length, 'enriched term')} with FDR ≤ ${fdrMax}` : `no term reaches FDR ≤ ${fdrMax}`}.`, - }, - ]) - document.getElementById('btnGaEnrichTsv').disabled = !rows.length -} - -function renderEnrichment() { - const st = groupAnalysisState.enrichment - const root = document.getElementById('gaEnrichResults') - if (!st || !st.rows.length) { - root.innerHTML = '' - return - } - const fp = (v) => (v < 1e-3 ? v.toExponential(2) : v.toFixed(4)) - const shown = st.rows.slice(0, 300) - root.innerHTML = `
    - ${st.setsMode === 'groups' ? '' : ''} - ${shown - .map( - (r, i) => ` - ${st.setsMode === 'groups' ? `` : ''} - - - ` - ) - .join('')} -
    SetTermIn setTerm sizeFoldp-valueFDR
    ${escapeHtml(r.set)}${escapeHtml(r.term)}${r.k} / ${r.n}${r.K} / ${r.N}${r.fold.toFixed(2)}${fp(r.p)}${fp(r.fdr)}
    ${st.rows.length > shown.length ? `

    Showing the first ${shown.length} of ${st.rows.length}; the download has all.

    ` : ''}` - root.querySelectorAll('.ga-show').forEach((b) => - b.addEventListener('click', () => { - const r = shown[+b.dataset.row] - cy.batch(() => { - cy.$(':selected').unselect() - r.genes.forEach((id) => cy.$id(id).select()) - }) - switchTab('network') - const sel = cy.nodes(':selected') - if (sel.length) cy.animate({ fit: { eles: sel, padding: 80 } }, { duration: 300 }) - }) - ) -} - -function enrichmentTsv() { - const st = groupAnalysisState.enrichment - if (!st) return - const lines = [ - [ - 'Set', - 'Set size', - 'Term', - 'Overlap', - 'Term size', - 'Background', - 'Fold enrichment', - 'p-value', - 'FDR (BH)', - 'Nodes', - ].join('\t'), - ...st.rows.map((r) => - [ - r.set, - r.n, - r.term, - r.k, - r.K, - r.N, - r.fold.toFixed(4), - r.p.toExponential(4), - r.fdr.toExponential(4), - r.genes.join(','), - ].join('\t') - ), - ] - downloadText('enrichment.tsv', lines.join('\n') + '\n') -} - -/* ============================================================ - COMPARISON AS A NETWORK - The compared networks merged into one view: each edge's channel says - which networks contain it (A only, B only, A + B, ...), and each node's - group says which networks contain the node. - ============================================================ */ -function patternLabel(mask, nets) { - const letters = nets.map((_, i) => String.fromCharCode(65 + i)).filter((_, i) => mask & (1 << i)) - if (letters.length === nets.length) return nets.length === 2 ? 'Shared' : 'In all' - return letters.length === 1 ? `${letters[0]} only` : letters.join(' + ') -} - -function openComparisonNetwork() { - const nets = compareState.results - if (!nets || nets.length < 2) return - const nodeMask = new Map() - nets.forEach((net, i) => - net.ids.forEach((id) => nodeMask.set(id, (nodeMask.get(id) || 0) | (1 << i))) - ) - const edgeMask = new Map() - nets.forEach((net, i) => - net.edgeSet.forEach((key) => edgeMask.set(key, (edgeMask.get(key) || 0) | (1 << i))) - ) - const full = (1 << nets.length) - 1 - const colorFor = (mask) => { - if (mask === full) return '#9ca3af' - const bits = nets.map((_, i) => i).filter((i) => mask & (1 << i)) - if (bits.length === 1) return nets[bits[0]].color - return colorAtIndex(NODE_PALETTES.vivid, mask + 3) - } - const edgeColors = {}, - nodeColors = {} - const edges = [...edgeMask].map(([key, mask], i) => { - const [a, b, dir] = key.split('\t') - const type = patternLabel(mask, nets) - edgeColors[type] = colorFor(mask) - return { - id: 'c' + i, - source: a, - target: b, - type, - directed: dir === 'directed', - 'in networks': type, - } - }) - const nodes = [...nodeMask].map(([id, mask]) => { - const g = `Nodes: ${patternLabel(mask, nets)}` - nodeColors[g] = colorFor(mask) - return { id, groups: [g] } - }) - // groups and channels in a readable order: single networks first, shared last - const order = (masks) => - [...new Set(masks)].sort( - (x, y) => (x === full) - (y === full) || popcount(x) - popcount(y) || x - y - ) - const popcount = (x) => { - let c = 0 - while (x) { - c += x & 1 - x >>= 1 - } - return c - } - const data = { - nodes, - edges, - nodeColors, - edgeColors, - groupOrder: order([...nodeMask.values()]).map((m) => `Nodes: ${patternLabel(m, nets)}`), - groupAttrs: Object.fromEntries( - nets.map((net, i) => [ - `Nodes: ${String.fromCharCode(65 + i)} only`, - { description: `Only in ${net.name}` }, - ]) - ), - config: { - layoutSelect: 'fr', - edgeCurveStyle: 'bezier', - legendShow: true, - legendGroups: true, - legendChannels: true, - legendTitle: 'Network comparison', - nodeFillSelect: 'groups', - }, - legendExtra: [ - { - title: 'Networks', - items: nets.map((net, i) => ({ - kind: 'glyph', - label: `${String.fromCharCode(65 + i)}: ${net.name}`, - color: net.color, - })), - }, - ], - } - const title = nets.map((n, i) => `${String.fromCharCode(65 + i)}`).join(' vs ') - openInNewView(`Comparison ${title}: ${nets.map((n) => n.name).join(' / ')}`.slice(0, 80), () => { - loadData(data) - applyGroupColorOverrides(data.nodeColors) - }) - switchTab('network') - const counts = order([...edgeMask.values()]).map( - (m) => `${patternLabel(m, nets)}: ${[...edgeMask.values()].filter((x) => x === m).length}` - ) - setStatus('cmpStatus', [ - { - level: 'ok', - text: `Opened the merged network with ${plural(nodes.length, 'node')} and ${plural(edges.length, 'edge')} (${counts.join(', ')}). Tick channels in the Display tab to show or hide each part.`, - }, - ]) -} - -/* ============================================================ - LOCAL EXPLORATION - Neighbourhood of a node (or the selected nodes) and all shortest paths - between two selected nodes, each opened as a new view that keeps the - nodes' positions, groups, colors and attributes. - ============================================================ */ -function adjacencyForPaths() { - // follows edge directions where edges are directed - const out = new Map(), - inn = new Map() - const add = (m, a, b) => { - if (!m.has(a)) m.set(a, new Set()) - m.get(a).add(b) - } - shownEdges(true).forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s === t) return - add(out, s, t) - add(inn, t, s) - if (!edgeIsDirected(e)) { - add(out, t, s) - add(inn, s, t) - } - }) - return { out, inn } -} - -function bfsDistances(start, adj, limit = Infinity) { - const dist = new Map([[start, 0]]) - const q = [start] - for (let h = 0; h < q.length; h++) { - const v = q[h] - const d = dist.get(v) - if (d >= limit) continue - ;(adj.get(v) || []).forEach((w) => { - if (!dist.has(w)) { - dist.set(w, d + 1) - q.push(w) - } - }) - } - return dist -} - -function openSubnetworkView(name, keepIds, marks, keepEdge) { - const data = snapshotData() - const keep = new Set(keepIds) - data.nodes = data.nodes - .filter((n) => keep.has(n.id)) - .map((n) => (marks.has(n.id) ? { ...n, role: marks.get(n.id) } : n)) - const shownEdgeIds = new Set(shownEdges(true).map((e) => e.id())) - data.edges = data.edges.filter( - (e) => - shownEdgeIds.has(e.id) && - keep.has(e.source) && - keep.has(e.target) && - (!keepEdge || keepEdge(e)) - ) - const used = new Set(data.nodes.flatMap((n) => n.groups)) - data.groupOrder = data.groupOrder.filter((g) => used.has(g)) - const positions = {} - cy.nodes().forEach((n) => { - if (keep.has(n.id())) positions[n.id()] = { ...n.position() } - }) - const config = viewSettings() - openInNewView(name, () => { - setControls(config) - loadData(data, { positions }) - refreshAllDerivedUI() - }) - switchTab(net3dActiveTab()) - cy.batch(() => - cy.nodes().forEach((n) => { - if (marks.has(n.id())) n.select() - }) - ) - cy.fit(undefined, 60) -} - -function net3dActiveTab() { - return net3d.active ? 'network3d' : 'network' -} - -function openNeighbourhood(ids, steps) { - const { out, inn } = adjacencyForPaths() - // neighbours in either direction - const both = new Map() - ;[out, inn].forEach((m) => - m.forEach((set, k) => { - if (!both.has(k)) both.set(k, new Set()) - set.forEach((v) => both.get(k).add(v)) - }) - ) - const keep = new Set() - ids.forEach((id) => bfsDistances(id, both, steps).forEach((_, v) => keep.add(v))) - const marks = new Map(ids.map((id) => [id, 'center'])) - const label = ids.length === 1 ? ids[0] : `${ids.length} nodes` - openSubnetworkView(`${label}: ${steps}-step neighbourhood`, [...keep], marks) - toast(`Opened the ${steps}-step neighbourhood of ${label}: ${plural(keep.size, 'node')}.`) -} - -function openShortestPaths(a, b) { - const { out, inn } = adjacencyForPaths() - const ds = bfsDistances(a, out), - dt = bfsDistances(b, inn) - if (!ds.has(b)) { - toast(`No path leads from ${a} to ${b} in the shown part of the network.`, 'warn') - return - } - const d = ds.get(b) - const keep = [...ds.keys()].filter((v) => dt.has(v) && ds.get(v) + dt.get(v) === d) - const keepSet = new Set(keep) - // only edges that lie on a shortest path - const onPath = (e) => { - const s = e.source, - t = e.target - const fwd = ds.has(s) && dt.has(t) && ds.get(s) + 1 + dt.get(t) === d - const rev = !e.directed && ds.has(t) && dt.has(s) && ds.get(t) + 1 + dt.get(s) === d - return keepSet.has(s) && keepSet.has(t) && (fwd || rev) - } - const marks = new Map([ - [a, 'source'], - [b, 'target'], - ]) - openSubnetworkView(`Shortest paths ${a} → ${b}`, keep, marks, onPath) - const count = countShortestPaths(a, b, out, ds, d) - toast( - `Opened ${plural(count, 'shortest path')} of length ${d} from ${a} to ${b} (${plural(keep.length, 'node')}).` - ) -} - -function countShortestPaths(a, b, out, ds, d) { - const ways = new Map([[a, 1]]) - const layers = [...ds].filter(([, x]) => x <= d).sort((x, y) => x[1] - y[1]) - layers.forEach(([v, x]) => { - const w = ways.get(v) || 0 - ;(out.get(v) || []).forEach((u) => { - if (ds.get(u) === x + 1) ways.set(u, (ways.get(u) || 0) + w) - }) - }) - return ways.get(b) || 0 -} - -export function toast(text, level = 'ok') { - let el = document.getElementById('toast') - if (!el) { - el = document.createElement('div') - el.id = 'toast' - el.setAttribute('role', 'status') - el.setAttribute('aria-live', 'polite') - document.getElementById('canvas').appendChild(el) - } - el.className = `toast ${level}` - el.textContent = text - el.hidden = false - clearTimeout(toast.timer) - toast.timer = setTimeout(() => { - el.hidden = true - }, 6000) -} - -// remembers the order in which nodes were selected (paths go first -> second) -const selectionOrder = [] - -/* ============================================================ - TIME SERIES AND CONDITIONS - With several numeric columns (time points, conditions) the view can - step through them, by hand or as an animation, with the layout fixed - and, by default, one color scale for all steps. Columns of other - numeric expression files can be added as further steps. - ============================================================ */ -const frameState = { playing: false, timer: null } - -function frameColumns() { - return valueColumns() -} - -function numericLibraryEntries() { - return normaLibrary.colors.filter((e) => e.parsed.numeric) -} - -export function updateFrameControls() { - const box = document.getElementById('valueFrames') - if (!box) return - const cols = viewHasValues() ? frameColumns() : [] - const otherFiles = numericLibraryEntries().length - box.hidden = !(cols.length > 1 || (cols.length && otherFiles > 1)) - const slider = document.getElementById('frameSlider') - slider.max = Math.max(0, cols.length - 1) - const i = Math.max(0, cols.indexOf(document.getElementById('valueColumn').value)) - slider.value = i - document.getElementById('frameLabel').textContent = cols.length - ? `Step ${i + 1} of ${cols.length}: ${cols[i]}` - : '' - document.getElementById('btnFramePlay').textContent = frameState.playing ? '❚❚ Pause' : '▶ Play' - const badge = document.getElementById('frameBadge') - badge.hidden = - box.hidden || - (nodeFillMode() !== 'values' && document.getElementById('sizeMetric').value !== 'value') - badge.textContent = cols.length ? `${cols[i]} · ${i + 1} / ${cols.length}` : '' -} - -function showFrame(i) { - const cols = frameColumns() - if (!cols.length) return - const k = ((i % cols.length) + cols.length) % cols.length - document.getElementById('valueColumn').value = cols[k] - applyValueColors() - if (net3d.active) requestRender3d() -} - -function stepFrame(delta) { - const cols = frameColumns() - showFrame(Math.max(0, cols.indexOf(document.getElementById('valueColumn').value)) + delta) -} - -function toggleFramePlay() { - frameState.playing = !frameState.playing - clearInterval(frameState.timer) - if (frameState.playing) { - if (nodeFillMode() !== 'values') { - document.getElementById('nodeFillSelect').value = 'values' - document - .getElementById('nodeFillSelect') - .dispatchEvent(new Event('change', { bubbles: true })) - } - const ms = parseInt(document.getElementById('frameSpeed').value, 10) || 1000 - frameState.timer = setInterval(() => { - if (!cy.nodes().length || !viewHasValues()) { - toggleFramePlay() - return - } - stepFrame(1) - }, ms) - } - updateFrameControls() -} - -export function stopFramePlay() { - if (frameState.playing) toggleFramePlay() -} - -// Adds the columns of every numeric expression file in Files as steps. -function addFramesFromFiles() { - const entries = numericLibraryEntries() - if (!entries.length) return - const nodeIds = new Set(cy.nodes().map((n) => n.id())) - const existing = new Set(frameColumns()) - const added = [] - cy.batch(() => { - entries.forEach((entry) => { - entry.parsed.columns.forEach((col) => { - const name = entry.parsed.columns.length === 1 ? entry.name : `${entry.name}: ${col}` - // the view's own numeric file is already there under its plain column names - const ownFile = S.currentLibView && S.currentLibView.colors === entry.id - if (existing.has(name) || (ownFile && existing.has(col))) return - existing.add(name) - added.push(name) - cy.nodes().forEach((n) => { - const row = entry.parsed.values.get(n.id()) - const vals = { ...(n.data('values') || {}) } - vals[name] = row ? row[col] : null - n.data('values', vals) - }) - }) - }) - }) - applyValueColors() - setStatus('frameStatus', [ - { - level: added.length ? 'ok' : 'warn', - text: added.length - ? `Added ${plural(added.length, 'step')}: ${listSample(added)}.` - : 'All numeric files are already steps.', - }, - ]) - updateFrameControls() - void nodeIds -} - -/* ============================================================ - EXPORT TO OTHER TOOLS - GraphML (yEd, Cytoscape, igraph, NetworkX), GEXF 1.3 with colors, - sizes and positions (Gephi), SIF (Cytoscape) and Cytoscape JSON - (.cyjs, Cytoscape desktop and Cytoscape.js). - ============================================================ */ -function xmlEsc(v) { - return String(v).replace( - /[&<>"']/g, - (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] - ) -} - -function exportModel() { - const nodes = exportNodes() - const idSet = new Set(nodes.map((n) => n.id())) - const edges = exportEdges().filter( - (e) => idSet.has(e.data('source')) && idSet.has(e.data('target')) - ) - const nodeAttrKeys = new Set(), - edgeAttrKeys = new Set(), - valueKeys = new Set() - nodes.forEach((n) => { - Object.keys(n.data('attrs') || {}).forEach((k) => nodeAttrKeys.add(k)) - Object.keys(n.data('values') || {}).forEach((k) => valueKeys.add(k)) - }) - edges.forEach((e) => Object.keys(e.data('attrs') || {}).forEach((k) => edgeAttrKeys.add(k))) - const kind = (list, key, get) => - list.every((x) => { - const v = get(x) - return v === undefined || v === null || typeof v === 'number' - }) - ? 'double' - : 'string' - return { - nodes, - edges, - nodeAttrs: [...nodeAttrKeys].map((k) => ({ - key: k, - type: kind(nodes, k, (n) => (n.data('attrs') || {})[k]), - })), - edgeAttrs: [...edgeAttrKeys].map((k) => ({ - key: k, - type: kind(edges, k, (e) => (e.data('attrs') || {})[k]), - })), - valueKeys: [...valueKeys], - weighted: edges.some((e) => typeof e.data('weight') === 'number'), - anyDirected: edges.some((e) => edgeIsDirected(e)), - } -} - -function nodeGroupsText(n) { - return effectiveGroupsFor(n) - .filter((g) => g !== UNGROUPED) - .map(groupLabel) - .join(';') -} - -function attrText(v) { - return v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v) -} - -function channelLabel(t) { - return (EDGE_TYPES[t] || { label: t }).label -} - -function toGraphML() { - const m = exportModel() - const keys = [ - ['n_label', 'node', 'label', 'string'], - ['n_groups', 'node', 'groups', 'string'], - ['n_color', 'node', 'color', 'string'], - ['n_size', 'node', 'size', 'double'], - ['n_x', 'node', 'x', 'double'], - ['n_y', 'node', 'y', 'double'], - ...m.valueKeys.map((k, i) => [`n_v${i}`, 'node', `value: ${k}`, 'double']), - ...m.nodeAttrs.map((a, i) => [`n_a${i}`, 'node', a.key, a.type]), - ['e_channel', 'edge', 'channel', 'string'], - ['e_color', 'edge', 'color', 'string'], - ...(m.weighted ? [['e_weight', 'edge', 'weight', 'double']] : []), - ...m.edgeAttrs.map((a, i) => [`e_a${i}`, 'edge', a.key, a.type]), - ] - const lines = [ - '', - '', - ' ', - ...keys.map( - ([id, forWhat, name, type]) => - ` ` - ), - ` `, - ] - const d = (k, v) => - v === '' || v === null || v === undefined || (typeof v === 'number' && !Number.isFinite(v)) - ? '' - : `${xmlEsc(v)}` - m.nodes.forEach((n) => { - const p = n.position() - const vals = n.data('values') || {} - const attrs = n.data('attrs') || {} - lines.push( - ` ` + - d('n_label', n.id()) + - d('n_groups', nodeGroupsText(n)) + - d('n_color', hexColor(n.style('background-color'))) + - d('n_size', Math.round(n.width() * 100) / 100) + - d('n_x', Math.round(p.x * 100) / 100) + - d('n_y', Math.round(p.y * 100) / 100) + - m.valueKeys.map((k, i) => d(`n_v${i}`, vals[k])).join('') + - m.nodeAttrs.map((a, i) => d(`n_a${i}`, attrText(attrs[a.key]))).join('') + - '' - ) - }) - m.edges.forEach((e, i) => { - const attrs = e.data('attrs') || {} - lines.push( - ` ` + - d('e_channel', channelLabel(e.data('type'))) + - d('e_color', hexColor(e.style('line-color'))) + - (m.weighted ? d('e_weight', e.data('weight')) : '') + - m.edgeAttrs.map((a, k) => d(`e_a${k}`, attrText(attrs[a.key]))).join('') + - '' - ) - }) - lines.push(' ', '') - return lines.join('\n') + '\n' -} - -function toGEXF() { - const m = exportModel() - const nodeAttrDefs = [ - ['groups', 'string'], - ...m.valueKeys.map((k) => [`value: ${k}`, 'double']), - ...m.nodeAttrs.map((a) => [a.key, a.type]), - ] - const edgeAttrDefs = [['channel', 'string'], ...m.edgeAttrs.map((a) => [a.key, a.type])] - const lines = [ - '', - '', - ` NORMA 3.0${xmlEsc((activeView() || {}).name || 'NORMA network')}`, - ` `, - ' ', - ...nodeAttrDefs.map( - ([k, t], i) => ` ` - ), - ' ', - ' ', - ...edgeAttrDefs.map( - ([k, t], i) => ` ` - ), - ' ', - ' ', - ] - const att = (i, v) => - v === '' || v === null || v === undefined || (typeof v === 'number' && !Number.isFinite(v)) - ? '' - : `` - m.nodes.forEach((n) => { - const [r, g, b] = rgbOf(n.style('background-color')) - const p = n.position() - const vals = n.data('values') || {}, - attrs = n.data('attrs') || {} - const values = [ - nodeGroupsText(n), - ...m.valueKeys.map((k) => vals[k]), - ...m.nodeAttrs.map((a) => attrText(attrs[a.key])), - ] - lines.push( - ` ${values.map((v, i) => att(i, v)).join('')}` + - `` - ) - }) - lines.push(' ', ' ') - m.edges.forEach((e, i) => { - const [r, g, b] = rgbOf(e.style('line-color')) - const attrs = e.data('attrs') || {} - const values = [channelLabel(e.data('type')), ...m.edgeAttrs.map((a) => attrText(attrs[a.key]))] - const w = typeof e.data('weight') === 'number' ? ` weight="${e.data('weight')}"` : '' - lines.push( - ` ` + - `${values.map((v, k) => att(k, v)).join('')}` - ) - }) - lines.push(' ', ' ', '') - return lines.join('\n') + '\n' -} - -function toSIF() { - const m = exportModel() - const lines = [] - const connected = new Set() - const clean = (s) => String(s).replace(/[\t\r\n]+/g, ' ') - m.edges.forEach((e) => { - connected.add(e.data('source')) - connected.add(e.data('target')) - lines.push( - [ - clean(e.data('source')), - clean(channelLabel(e.data('type'))).replace(/\s+/g, '_') || 'pp', - clean(e.data('target')), - ].join('\t') - ) - }) - m.nodes.forEach((n) => { - if (!connected.has(n.id())) lines.push(clean(n.id())) - }) - return lines.join('\n') + '\n' -} - -function toCyJS() { - const m = exportModel() - const v = activeView() - const json = { - format_version: '1.0', - generated_by: 'NORMA 3.0', - target_cytoscapejs_version: '~3.0', - data: { - name: v ? v.name : 'NORMA network', - shared_name: v ? v.name : 'NORMA network', - selected: true, - }, - elements: { - nodes: m.nodes.map((n) => { - const p = n.position() - return { - data: { - id: n.id(), - name: n.id(), - shared_name: n.id(), - groups: nodeGroupsText(n), - color: hexColor(n.style('background-color')), - size: Math.round(n.width() * 100) / 100, - ...Object.fromEntries( - Object.entries(n.data('values') || {}).map(([k, x]) => [`value: ${k}`, x]) - ), - ...(n.data('attrs') || {}), - }, - position: { x: Math.round(p.x * 100) / 100, y: Math.round(p.y * 100) / 100 }, - } - }), - edges: m.edges.map((e, i) => ({ - data: { - id: `e${i}`, - source: e.data('source'), - target: e.data('target'), - interaction: channelLabel(e.data('type')), - shared_interaction: channelLabel(e.data('type')), - name: `${e.data('source')} (${channelLabel(e.data('type'))}) ${e.data('target')}`, - directed: edgeIsDirected(e), - color: hexColor(e.style('line-color')), - ...(typeof e.data('weight') === 'number' ? { weight: e.data('weight') } : {}), - ...(e.data('attrs') || {}), - }, - })), - }, - } - return JSON.stringify(json, null, 1) -} - -function exportOtherFormat(kind) { - if (!cy.nodes().length) { - setStatus('otherExportStatus', [{ level: 'error', text: 'Show a network first.' }]) - return - } - const v = activeView() - const stem = v ? fileStem(v.name) : 'network' - const spec = { - graphml: ['GraphML', 'graphml', toGraphML], - gexf: ['GEXF', 'gexf', toGEXF], - sif: ['SIF', 'sif', toSIF], - cyjs: ['Cytoscape JSON', 'cyjs', toCyJS], - }[kind] - const text = spec[2]() - downloadText(`${stem}.${spec[1]}`, text) - const m = exportModel() - setStatus('otherExportStatus', [ - { - level: 'ok', - text: `Saved ${stem}.${spec[1]} (${spec[0]}): ${plural(m.nodes.length, 'node')}, ${plural(m.edges.length, 'edge')}${exportShownOnly() ? ', ticked groups and channels only' : ''}.`, - }, - ]) -} - -/* ============================================================ - SESSIONS - One file with every view (data, positions, settings, 3D camera) and - every file in Files (including STRING extras), to continue later. - ============================================================ */ -const SESSION_FORMAT = 'norma3-session' - -function sessionObject() { - captureActiveView() - const lib = {} - Object.keys(normaLibrary).forEach((kind) => { - lib[kind] = normaLibrary[kind].map((e) => { - const { parsed, ...rest } = e - return rest - }) - }) - return { - format: SESSION_FORMAT, - version: 1, - savedAt: new Date().toISOString(), - app: 'NORMA 3.0', - activeViewId: S.activeViewId, - theme: document.getElementById('themeSelect').value, - views: views.map((v) => ({ - id: v.id, - name: v.name, - autoName: v.autoName, - data: v.data, - selection: v.selection, - state: v.state, - ...(v.needsRefresh ? { needsRefresh: v.needsRefresh } : {}), - })), - library: lib, - edgeTypes: JSON.parse(JSON.stringify(EDGE_TYPES)), - } -} - -function saveSession() { - const s = sessionObject() - const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-') - downloadText(`norma-session-${stamp}.json`, JSON.stringify(s)) - setStatus('sessionSaveStatus', [ - { - level: 'ok', - text: `Saved the session: ${plural(s.views.length, 'view')} and ${plural( - Object.values(s.library).reduce((n, l) => n + l.length, 0), - 'file' - )}.`, - }, - ]) -} - -export function isSessionObject(obj) { - return obj && obj.format === SESSION_FORMAT && Array.isArray(obj.views) -} - -export function loadSession(obj) { - if (!isSessionObject(obj)) throw new Error('This is not a NORMA session file.') - stopFramePlay() - // files - const kinds = Object.keys(normaLibrary) - const rebuilt = {} - let maxSeq = S.normaEntrySeq - kinds.forEach((kind) => { - rebuilt[kind] = [] - ;((obj.library && obj.library[kind]) || []).forEach((saved) => { - const parsed = NORMA_PARSERS[kind](saved.text, saved.options || {}) - rebuilt[kind].push({ ...saved, parsed }) - const n = parseInt(String(saved.id).replace(/\D/g, ''), 10) - if (Number.isFinite(n)) maxSeq = Math.max(maxSeq, n) - }) - }) - kinds.forEach((kind) => { - normaLibrary[kind].length = 0 - normaLibrary[kind].push(...rebuilt[kind]) - }) - S.normaEntrySeq = maxSeq - if (obj.edgeTypes) - Object.entries(obj.edgeTypes).forEach(([k, t]) => { - const safe = { - ...t, - color: - sanitizeColor(t && t.color) || - (EDGE_TYPES[k] && EDGE_TYPES[k].color) || - nextAutoEdgeColor(), - } - if (!EDGE_TYPES[k]) EDGE_TYPES[k] = safe - else Object.assign(EDGE_TYPES[k], safe) - }) - // views - views.length = 0 - let maxView = S.viewSeq - obj.views.forEach((v) => { - views.push({ - id: v.id, - name: v.name, - autoName: v.autoName, - data: v.data, - selection: v.selection || { networks: [], annotation: '', colors: '' }, - state: v.state || { config: { ...S.DEFAULT_VIEW_CONFIG } }, - needsRefresh: v.needsRefresh || null, - }) - const n = parseInt(String(v.id).replace(/\D/g, ''), 10) - if (Number.isFinite(n)) maxView = Math.max(maxView, n) - }) - S.viewSeq = maxView - if (!views.length) { - S.activeViewId = null - createView('Untitled view') - } else { - S.activeViewId = views.some((v) => v.id === obj.activeViewId) ? obj.activeViewId : views[0].id - const v = activeView() - libSelection.networks = new Set(v.selection.networks || []) - libSelection.annotation = v.selection.annotation || '' - libSelection.colors = v.selection.colors || '' - S.historySuspended++ - try { - restoreView(v) - } finally { - S.historySuspended-- - } - S.dataCache = { version: dataVersion, data: v.data } - } - if (obj.theme) { - document.getElementById('themeSelect').value = obj.theme - applyTheme(obj.theme) - } - renderLibraryLists() - setHistoryBaseline() - renderViewBar() - updateUndoButtons() - return { views: views.length, files: kinds.reduce((n, k) => n + normaLibrary[k].length, 0) } -} - -async function openSessionFile(file) { - try { - const obj = JSON.parse(await readFileText(file)) - const r = loadSession(obj) - setStatus('sessionStatus', [ - { - level: 'ok', - text: `Opened the session "${file.name}": ${plural(r.views, 'view')} and ${plural(r.files, 'file')}.`, - }, - ]) - } catch (err) { - setStatus('sessionStatus', [ - { level: 'error', text: `The session couldn't be opened: ${err.message}` }, - ]) - } -} - -/* ============================================================ - WEBGL DRAWING (experimental) - Cytoscape.js can draw with WebGL, which is much faster on large - networks. It is chosen before the network canvas is created, so the - setting takes effect after reloading the page. - ============================================================ */ -export function webglPreference() { - try { - const q = new URLSearchParams(location.search).get('webgl') - if (q !== null) return q === '1' || q === 'true' - return localStorage.getItem('norma3-webgl') === '1' - } catch (e) { - return false - } -} - -/* ============================================================ - RUNTIME TABLE (Network Profiler) - Times the main steps on random networks of growing size, for a - supplementary table: reading the network file, building the network, - weighted layout, edge bundling, profiling, Louvain and the group - separation score. Nothing on screen changes. - ============================================================ */ -const RUNTIME_SIZES = [100, 500, 1000, 2500, 5000, 10000] - -const runtimeState = { rows: [], running: false, cancel: false } - -async function runRuntimeTable() { - if (runtimeState.running) { - runtimeState.cancel = true - return - } - const btn = document.getElementById('btnRuntime') - const sizes = RUNTIME_SIZES.filter( - (n) => n <= (parseInt(document.getElementById('runtimeMax').value, 10) || 5000) - ) - runtimeState.running = true - runtimeState.cancel = false - btn.textContent = 'Stop' - runtimeState.rows = [] - const time = async (fn) => { - const t = performance.now() - const r = await fn() - return [performance.now() - t, r] - } - try { - // larger networks take much longer: weigh each size by n^1.5 - const weightOf = (n) => Math.pow(n, 1.5) - const totalWeight = sizes.reduce((a, n) => a + weightOf(n), 0) - let doneWeight = 0 - for (const n of sizes) { - if (runtimeState.cancel) break - const text = `Timing a network of ${n.toLocaleString()} nodes…` - const show = (f) => - setStatus('runtimeStatus', [ - { level: 'busy', text, progress: (doneWeight + weightOf(n) * f) / totalWeight }, - ]) - show(0) - await nextPaint() - const demo = generateRandomNetwork(n, { seed: n + 3 }) - const texts = demoToNormaTexts(demo) - const row = { nodes: n } - const [tParse, parsed] = await time(() => parseNormaNetwork(texts.network, {})) - row.edges = parsed.edges.length - row.parse = tParse - const [tBuild, headless] = await time(() => - cytoscape({ - headless: true, - styleEnabled: false, - elements: [ - ...demo.nodes.map((x) => ({ data: { id: x.id } })), - ...demo.edges.map((e, i) => ({ - data: { id: 'r' + i, source: e.source, target: e.target }, - })), - ], - }) - ) - row.build = tBuild - headless.destroy() - const edges = demo.edges.map((e) => ({ source: e.source, target: e.target, weight: 1 })) - const ids = demo.nodes.map((x) => x.id) - const [tLayout, pos] = await time(() => frLayoutAsync(ids, edges, (f) => show(0.1 + 0.4 * f))) - row.layout = tLayout - if (runtimeState.cancel) break - const segs = demo.edges.map((e) => ({ - sx: pos[e.source].x * 40, - sy: pos[e.source].y * 40, - tx: pos[e.target].x * 40, - ty: pos[e.target].y * 40, - })) - if (segs.length <= BUNDLE_MAX_EDGES) { - show(0.5) - const [tBundle] = await time(() => - bundleAsync(segs, { threshold: 0.6, iterations: 60 }, (f) => show(0.5 + 0.4 * f)) - ) - row.bundle = tBundle - } else row.bundle = NaN - show(0.9) - await nextPaint() - const g = simpleGraph( - ids, - demo.edges.map((e) => [e.source, e.target]) - ) - const [tProfile] = await time(() => profileGraph(g)) - row.profile = tProfile - const [tLouvain] = await time(() => louvain(g)) - row.louvain = tLouvain - const groupsOf = Object.fromEntries(demo.nodes.map((x) => [x.id, x.groups || []])) - const [tSep] = await time(() => groupSeparation(pos, groupsOf)) - row.separation = tSep - runtimeState.rows.push(row) - renderRuntimeTable() - doneWeight += weightOf(n) - await new Promise((r) => setTimeout(r, 30)) - } - setStatus('runtimeStatus', [ - { - level: runtimeState.cancel ? 'warn' : 'ok', - text: `${runtimeState.cancel ? 'Stopped' : 'Done'}: timed ${plural(runtimeState.rows.length, 'network size')} in this browser (${navigator.hardwareConcurrency || '?'} logical processors). Rendering time depends on the screen and is not included.`, - }, - ]) - document.getElementById('btnRuntimeTsv').disabled = !runtimeState.rows.length - } finally { - runtimeState.running = false - btn.textContent = 'Time the main steps' - } -} - -const RUNTIME_COLUMNS = [ - ['parse', 'Read file'], - ['build', 'Build network'], - ['layout', 'Weighted layout'], - ['bundle', 'Edge bundling'], - ['profile', 'Profile statistics'], - ['louvain', 'Louvain'], - ['separation', 'Separation score'], -] - -function renderRuntimeTable() { - const root = document.getElementById('runtimeResults') - const fmt = (v) => - !Number.isFinite(v) ? '—' : v < 1000 ? `${Math.round(v)} ms` : `${(v / 1000).toFixed(2)} s` - root.innerHTML = `
    - ${RUNTIME_COLUMNS.map(([, l]) => ``).join('')} - ${runtimeState.rows.map((r) => `${RUNTIME_COLUMNS.map(([k]) => ``).join('')}`).join('')} -
    NodesEdges${l}
    ${r.nodes.toLocaleString()}${r.edges.toLocaleString()}${fmt(r[k])}
    ` -} - -function runtimeTsv() { - const lines = [ - ['Nodes', 'Edges', ...RUNTIME_COLUMNS.map(([, l]) => `${l} (ms)`)].join('\t'), - ...runtimeState.rows.map((r) => - [ - r.nodes, - r.edges, - ...RUNTIME_COLUMNS.map(([k]) => (Number.isFinite(r[k]) ? r[k].toFixed(1) : '')), - ].join('\t') - ), - ] - lines.push( - `# ${navigator.userAgent}; ${navigator.hardwareConcurrency || '?'} logical processors; WebGL drawing ${WEBGL_ACTIVE ? 'on' : 'off'}` - ) - downloadText('norma-runtime.tsv', lines.join('\n') + '\n') -} - -/* ---------- Network Comparison -> Arena3D ---------- - Each compared network is one layer holding its own nodes and edges. - Every node keeps the same position in every layer (one layout of all - networks together), so layers line up. Between layers, the common - edges (and, if chosen, the copies of common nodes) link the layers: - an edge present in networks A and B also runs from its source in - layer A to its target in layer B. */ -export async function buildCompareArena3dModel() { - const nets = compareState.results - if (!nets || nets.length < 2) throw new Error('Compare at least two networks first.') - if (nets.length > ARENA3D_MAX_LAYERS) - throw new Error(`Arena3D takes up to ${ARENA3D_MAX_LAYERS} layers.`) - const between = document.getElementById('cmpArenaBetween').value - const letter = (i) => String.fromCharCode(65 + i) - const layerName = nets.map((n, i) => arenaSafe(`${letter(i)} ${n.name}`).slice(0, 60)) - // union network and one shared layout - const nodeMask = new Map(), - edgeMask = new Map() - nets.forEach((net, i) => { - net.ids.forEach((id) => nodeMask.set(id, (nodeMask.get(id) || 0) | (1 << i))) - net.edgeSet.forEach((k) => edgeMask.set(k, (edgeMask.get(k) || 0) | (1 << i))) - }) - const ids = [...nodeMask.keys()] - if (ids.length > MAX_NETWORK_NODES) - throw new Error( - `The compared networks have ${ids.length.toLocaleString('en-US')} nodes together; NORMA handles up to ${MAX_NETWORK_NODES.toLocaleString('en-US')}.` - ) - const unionEdges = [...edgeMask.keys()].map((k) => { - const [a, b] = k.split('\t') - return { source: a, target: b, weight: 1 } - }) - const raw = ids.length > 1 ? await frLayoutAsync(ids, unionEdges) : { [ids[0]]: { x: 0, y: 0 } } - const xs = ids.map((id) => raw[id].x), - ys = ids.map((id) => raw[id].y) - const cx = (Math.min(...xs) + Math.max(...xs)) / 2, - cy0 = (Math.min(...ys) + Math.max(...ys)) / 2 - const span = Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)) || 1 - const half = ARENA3D_LAYER_WIDTH * 0.42 - const k = ids.length > 1 ? (2 * half) / span : 0 - const pos = (id) => ({ y: String(-(raw[id].y - cy0) * k), z: String((raw[id].x - cx) * k) }) - const full = (1 << nets.length) - 1 - const bits = (m) => nets.map((_, i) => i).filter((i) => m & (1 << i)) - const COMMON = '#9CA3AF' - - const layers = nets.map((net, i) => ({ - name: layerName[i], - position_x: String((i - (nets.length - 1) / 2) * ARENA3D_LAYER_SPACING), - position_y: '0', - position_z: '0', - last_layer_scale: '1', - rotation_x: '0', - rotation_y: '0', - rotation_z: '0', - floor_current_color: hexColor(net.color), - geometry_parameters_width: String(ARENA3D_LAYER_WIDTH), - })) - const nodes = [] - nets.forEach((net, i) => - net.ids.forEach((id) => { - const m = nodeMask.get(id) - const p = pos(id) - nodes.push({ - name: arenaSafe(id), - layer: layerName[i], - position_x: '0', - position_y: p.y, - position_z: p.z, - scale: '1', - color: bits(m).length === 1 ? hexColor(net.color) : COMMON, - url: '', - descr: - bits(m).length === 1 ? `Only in ${net.name}` : `In ${bits(m).map(letter).join(', ')}`, - }) - }) - ) - const edges = [], - rows = [] - const add = (a, la, b, lb, color, channel, opacity = '1') => { - edges.push({ - src: `${arenaSafe(a)}_${la}`, - trg: `${arenaSafe(b)}_${lb}`, - opacity, - color, - channel, - }) - rows.push([arenaSafe(a), la, arenaSafe(b), lb, 1, channel]) - } - let within = 0, - across = 0, - nodeLinks = 0, - directed = false - // edges inside each layer - nets.forEach((net, i) => - net.edgeSet.forEach((key) => { - const [a, b, dir] = key.split('\t') - if (dir === 'directed') directed = true - const m = edgeMask.get(key) - const only = bits(m).length === 1 - add( - a, - layerName[i], - b, - layerName[i], - only ? hexColor(net.color) : COMMON, - only ? `${letter(i)} only` : 'common', - '1' - ) - within++ - }) - ) - // common edges between consecutive layers that contain them - if (between === 'edges' || between === 'both') { - edgeMask.forEach((m, key) => { - const bs = bits(m) - if (bs.length < 2) return - const [a, b] = key.split('\t') - for (let j = 0; j + 1 < bs.length; j++) { - add( - a, - layerName[bs[j]], - b, - layerName[bs[j + 1]], - '#FFFFFF', - 'common edge between layers', - '0.7' - ) - across++ - } - }) - } - // copies of common nodes - if (between === 'nodes' || between === 'both') { - nodeMask.forEach((m, id) => { - const bs = bits(m) - for (let j = 0; j + 1 < bs.length; j++) { - add(id, layerName[bs[j]], id, layerName[bs[j + 1]], '#FACC15', 'same node', '0.5') - nodeLinks++ - } - }) - } - const json = { - scene: { - position_x: '0', - position_y: '0', - scale: String( - nets.length > 4 ? Math.round(((0.6561 * 4) / nets.length) * 1e4) / 1e4 : 0.6561 - ), - color: '#000000', - rotation_x: '0.261799387799149', - rotation_y: '0.261799387799149', - rotation_z: '0.0872664625997165', - }, - layers, - nodes, - edges, - universalLabelColor: '#FFFFFF', - direction: directed, - edgeOpacityByWeight: false, - edgeWidthByWeight: false, - } - const commonEdges = [...edgeMask.values()].filter((m) => bits(m).length > 1).length - const commonNodes = [...nodeMask.values()].filter((m) => bits(m).length > 1).length - return { - json, - rows, - stats: { - layers: layers.length, - nodes: nodes.length, - edges: edges.length, - interLayer: across + nodeLinks, - copyEdges: nodeLinks, - multiLayerNodes: commonNodes, - distinctNodes: ids.length, - droppedLayers: 0, - droppedNodes: 0, - compare: { - within, - across, - nodeLinks, - commonEdges, - full: [...edgeMask.values()].filter((m) => m === full).length, - }, - }, - } -} - -/* ============================================================ - DATABASE IMPORTERS - Reactome, OmniPath, NDEx, IntAct and the Gene Ontology, next to - STRING. Each one builds a network and/or groupings and adds them to - Files like any uploaded file. Requests go through server.py's relay - when NORMA runs on it (no cross-site restrictions), otherwise straight - to the service. Bodies are always strings (see the STRING importer). - ============================================================ */ -const DB_URLS = { - reactome: 'https://reactome.org/ContentService', - omnipath: 'https://omnipathdb.org', - ndex: 'https://www.ndexbio.org', - intact: 'https://www.ebi.ac.uk/Tools/webservices/psicquic/intact/webservices/current/search', - quickgo: 'https://www.ebi.ac.uk/QuickGO/services', - goapi: 'https://api.geneontology.org/api', -} - -const DB_LABELS = { - reactome: 'Reactome', - omnipath: 'OmniPath', - ndex: 'NDEx', - intact: 'IntAct', - go: 'Gene Ontology', -} - -const dbState = { busy: {}, abort: {}, task: {}, mapping: {} } - -async function dbFetch( - key, - url, - { - method = 'GET', - body = null, - contentType = null, - accept = 'application/json', - text = false, - } = {} -) { - const route = - NORMA_CFG.features.relays.databases === false ? 'direct' : await resolveStringRoute() - const target = route === 'proxy' ? `db-api/fetch?url=${encodeURIComponent(url)}` : url - const headers = { Accept: accept } - if (contentType) headers['Content-Type'] = contentType - const ctl = new AbortController() - dbState.abort[key] = ctl - const timer = setTimeout(() => ctl.abort(), 120000) - let response - try { - response = await fetch(target, { method, headers, body, signal: ctl.signal }) - } catch (err) { - if (err.name === 'AbortError') - throw new Error( - dbState.cancelled && dbState.cancelled[key] - ? 'Cancelled.' - : `${DB_LABELS[key]} did not answer within 2 minutes.` - ) - throw new Error( - route === 'proxy' - ? `The server relay could not be reached (${err.message}). Check that server.py is still running.` - : `${DB_LABELS[key]} could not be reached (${err.message}). Browsers may block direct calls to other sites; running NORMA with server.py avoids that (see Help).` - ) - } finally { - clearTimeout(timer) - } - // download progress, except for the many small requests of dbMap (counted there) - const task = dbState.task[key] - const body2 = await readTextWithProgress( - response, - task && !dbState.mapping[key] ? (got, total) => task.bytes(got, total) : null - ) - if (!response.ok) { - if (route === 'proxy' && !response.headers.get('X-Norma-Relay')) - throw new Error( - 'This server has no database relay. Start NORMA with server.py, or choose Connect: Directly under Import from STRING → STRING server.' - ) - let detail = body2 - .slice(0, 200) - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim() - try { - const j = JSON.parse(body2) - detail = j.message || j.errorMessage || j.error || detail - } catch (e) {} - throw new Error( - `${DB_LABELS[key]} answered with an error (${response.status})${detail ? ': ' + detail : ''}.` - ) - } - if (text) return body2 - try { - return body2.trim() ? JSON.parse(body2) : null - } catch (e) { - throw new Error(`${DB_LABELS[key]} sent an answer that is not JSON.`) - } -} - -// runs fn over items with a few requests at a time -async function dbMap(key, items, limit, fn, progress) { - const out = new Array(items.length) - let next = 0, - done = 0 - dbState.mapping[key] = (dbState.mapping[key] || 0) + 1 - const worker = async () => { - while (next < items.length) { - if (dbState.cancelled[key]) throw new Error('Cancelled.') - const i = next++ - try { - out[i] = await fn(items[i], i) - } catch (err) { - if (/Cancelled/.test(err.message)) throw err - out[i] = { error: err } - } - done++ - if (progress) progress(done, items.length) - if (dbState.task[key]) dbState.task[key].sub(done / items.length) - } - } - try { - await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)) - } finally { - dbState.mapping[key]-- - } - return out -} - -function dbStatus(key, notes) { - setStatus(`${key}Status`, notes) -} - -// Progress of an import: dbPlan sets its weighted steps, dbStep moves on, -// dbProgress changes the text of the current step. -function dbPlan(key, weights) { - dbState.task[key] = startProgress(`${key}Status`, weights) - return dbState.task[key] -} - -function dbStep(key, i, text) { - if (!dbState.task[key] || !dbState.task[key].active) dbPlan(key, [1]) - dbState.task[key].step(i, text) -} - -function dbProgress(key, text) { - const task = dbState.task[key] - if (task && task.active) task.say(text) - else dbStep(key, 0, text) -} - -function dbBusy(key, busy) { - dbState.busy[key] = busy - if (busy) dbState.cancelled[key] = false - document.querySelectorAll(`[data-db-run="${key}"]`).forEach((b) => { - b.disabled = busy - }) - const cancel = document.getElementById(`${key}Cancel`) - if (cancel) cancel.hidden = !busy -} - -async function dbRun(key, fn) { - if (dbState.busy[key]) return - dbBusy(key, true) - dbState.mapping[key] = 0 - dbStep(key, 0, `Contacting ${DB_LABELS[key]}…`) - try { - await fn() - } catch (err) { - dbStatus(key, [{ level: 'error', text: err.message }]) - } finally { - if (dbState.task[key]) dbState.task[key].stop() - dbState.task[key] = null - dbBusy(key, false) - } -} - -export function dbCancel(key) { - dbState.cancelled[key] = true - if (dbState.abort[key]) dbState.abort[key].abort() -} - -const dbSafe = (s) => - String(s ?? '') - .replace(/[\t\r\n]+/g, ' ') - .replace(/,/g, ';') - .trim() - -function dbSplitList(v) { - return String(v || '') - .split(/[\s,;]+/) - .map((x) => x.trim()) - .filter(Boolean) -} - -const UNIPROT_RE = /^([OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2})(-\d+)?$/ - -/* Adds a fetched network and its groupings to Files and opens them. - edges: [{ source, target, type, weight, directed }] with node names - nodeAttrs: { name: {...} }, groupings: [{ label, groups: [{ name, members, meta }] }] */ -function dbAddImport( - key, - { - name, - edges, - nodeAttrs = {}, - groupings = [], - positions = null, - notes = [], - summary = '', - preferGrouping = 0, - } -) { - if (!edges.length) throw new Error('No connections were found with these settings.') - const weighted = edges.some((e) => typeof e.weight === 'number') - const typed = - new Set(edges.map((e) => e.type || '')).size > 1 || - edges.some((e) => e.type && e.type !== 'link') - const anyDirected = edges.some((e) => e.directed) - const header = [ - 'Source', - 'Target', - ...(weighted ? ['Weight'] : []), - ...(typed ? ['Type'] : []), - ...(anyDirected ? ['Direction'] : []), - ] - const lines = edges.map((e) => - [ - dbSafe(e.source), - dbSafe(e.target), - ...(weighted ? [typeof e.weight === 'number' ? Math.round(e.weight * 1e4) / 1e4 : 1] : []), - ...(typed ? [dbSafe(e.type || 'link')] : []), - ...(anyDirected ? [e.directed ? 'directed' : 'undirected'] : []), - ].join('\t') - ) - const netEntry = addNormaEntry( - 'network', - name, - [header.join('\t'), ...lines].join('\n') + '\n', - '', - null, - {} - ) - const cleanAttrs = {} - Object.entries(nodeAttrs).forEach(([k, v]) => { - cleanAttrs[dbSafe(k)] = v - }) - netEntry.nodeAttrs = cleanAttrs - netEntry.dbSource = key - if (positions) { - const pos = {} - Object.entries(positions).forEach(([k, p]) => { - pos[dbSafe(k)] = p - }) - netEntry.nodePositions = pos - } - const nodeSet = new Set(netEntry.parsed.nodes) - const annEntries = [] - groupings.forEach((gr) => { - const used = new Set() - const meta = {} - const rows = [] - gr.groups.forEach((g) => { - const members = [...new Set(g.members.map(dbSafe))].filter((m) => nodeSet.has(m)) - if (members.length < (gr.minSize || 1)) return - let gname = - String(g.name) - .replace(/[\t\r\n]+/g, ' ') - .trim() || 'group' - while (used.has(gname)) gname += '′' - used.add(gname) - if (g.meta) meta[gname] = g.meta - rows.push(`${gname}\t${members.join(',')}`) - }) - if (!rows.length) return - const entry = addNormaEntry( - 'annotation', - `${name}: ${gr.label}`, - rows.join('\n') + '\n', - '', - null, - {} - ) - entry.groupMeta = meta - entry.forNetwork = netEntry.id - annEntries.push(entry) - }) - const chosen = annEntries[Math.min(preferGrouping, annEntries.length - 1)] - openInNewView(name, () => { - libSelection.networks = new Set([netEntry.id]) - libSelection.annotation = chosen ? chosen.id : '' - libSelection.colors = '' - renderLibraryLists() - refreshLibraryView() - if (anyDirected) { - // directed databases (OmniPath, GO-CAM) open with arrows - const dirSel = document.getElementById('edgeDirection') - dirSel.value = 'data' - dirSel.dispatchEvent(new Event('change', { bubbles: true })) - } - }) - switchTab('network') - dbStatus(key, [ - { - level: 'ok', - text: `Imported "${name}": ${netEntry.parsed.summary}${summary ? `. ${summary}` : ''}.`, - }, - ...(annEntries.length - ? [ - { - level: 'ok', - text: `Added ${plural(annEntries.length, 'grouping')}: ${annEntries.map((e) => e.name.slice(name.length + 2)).join(', ')}. Switch between them with the Grouping list at the top.`, - }, - ] - : []), - ...netEntry.parsed.notes.map((t) => ({ level: 'warn', text: t })), - ...notes, - ]) - return { netEntry, annEntries } -} - -// Adds groupings for the network already shown (GO terms, ...). -function dbAddGroupingsToView(key, groupings, label) { - const entries = [] - groupings.forEach((gr) => { - const rows = [], - meta = {}, - used = new Set() - gr.groups.forEach((g) => { - let gname = String(g.name) - .replace(/[\t\r\n]+/g, ' ') - .trim() - while (used.has(gname)) gname += '′' - used.add(gname) - if (g.meta) meta[gname] = g.meta - rows.push(`${gname}\t${g.members.join(',')}`) - }) - if (!rows.length) return - const entry = addNormaEntry( - 'annotation', - `${label}: ${gr.label}`, - rows.join('\n') + '\n', - '', - null, - {} - ) - entry.groupMeta = meta - entries.push(entry) - }) - if (!entries.length) return entries - if (S.currentLibView) { - libSelection.annotation = entries[0].id - renderLibraryLists() - refreshLibraryView() - } else { - // views from examples or JSON: apply the first grouping directly - const groupsOf = {} - entries[0].parsed.groups.forEach((g) => - g.members.forEach((m) => (groupsOf[m] = groupsOf[m] || []).push(g.name)) - ) - document.getElementById('nodeFillSelect').value = 'groups' - applyGroupsAndColorsInPlace( - groupsOf, - new Map(), - entries[0].parsed.groups.map((g) => g.name), - entries[0].groupMeta - ) - renderLibraryLists() - } - return entries -} - -/* --------------------------------------------------------------- - REACTOME: the proteins of a pathway's reactions, linked when they - take part in the same reaction; groups are its sub-pathways. - --------------------------------------------------------------- */ -export const REACTOME_SPECIES = [ - 'Homo sapiens', - 'Mus musculus', - 'Rattus norvegicus', - 'Danio rerio', - 'Drosophila melanogaster', - 'Caenorhabditis elegans', - 'Saccharomyces cerevisiae', - 'Gallus gallus', - 'Sus scrofa', - 'Bos taurus', - 'Canis familiaris', - 'Xenopus tropicalis', - 'Dictyostelium discoideum', - 'Plasmodium falciparum', - 'Schizosaccharomyces pombe', -] - -const REACTION_CLASSES = new Set([ - 'Reaction', - 'BlackBoxEvent', - 'Polymerisation', - 'Depolymerisation', - 'FailedReaction', - 'ReactionLikeEvent', - 'CellLineagePath', - 'CellDevelopmentStep', -]) - -const stripTags = (s) => String(s || '').replace(/<[^>]+>/g, '') - -export async function reactomeSearch() { - await dbRun('reactome', async () => { - const q = document.getElementById('reactomeQuery').value.trim() - const species = document.getElementById('reactomeSpecies').value - if (!q) throw new Error('Type a pathway name or a Reactome identifier such as R-HSA-69278.') - const sel = document.getElementById('reactomePathway') - sel.innerHTML = '' - if (/^R-[A-Z]{3}-\d+(\.\d+)?$/i.test(q)) { - const p = await dbFetch('reactome', `${DB_URLS.reactome}/data/query/${encodeURIComponent(q)}`) - sel.add(new Option(`${p.displayName} (${p.stId || q})`, p.stId || q)) - } else { - dbProgress('reactome', 'Searching Reactome…') - const r = await dbFetch( - 'reactome', - `${DB_URLS.reactome}/search/query?query=${encodeURIComponent(q)}&species=${encodeURIComponent(species)}&types=Pathway&cluster=true` - ) - const entries = ((r && r.results) || []).flatMap((g) => g.entries || []) - entries - .slice(0, 40) - .forEach((e) => - sel.add(new Option(`${stripTags(e.name)} (${e.stId || e.id})`, e.stId || e.id)) - ) - } - document.getElementById('reactomePickRow').hidden = !sel.options.length - dbStatus( - 'reactome', - sel.options.length - ? [ - { - level: 'ok', - text: `Found ${plural(sel.options.length, 'pathway')}. Choose one and fetch its network.`, - }, - ] - : [{ level: 'warn', text: 'No pathway matched. Try another name or check the species.' }] - ) - }) -} - -export async function reactomeFetch() { - await dbRun('reactome', async () => { - const pid = document.getElementById('reactomePathway').value - if (!pid) throw new Error('Search for a pathway and choose one first.') - const maxReactions = Math.max( - 5, - Math.min(500, parseInt(document.getElementById('reactomeMaxReactions').value, 10) || 150) - ) - const maxSize = Math.max( - 2, - parseInt(document.getElementById('reactomeMaxSize').value, 10) || 25 - ) - const smallMolecules = document.getElementById('reactomeSmall').checked - // steps: pathway, sub-pathways, reactions, building - dbPlan('reactome', [2, 1, 6, 1]) - dbStep('reactome', 0, 'Reading the pathway…') - const top = await dbFetch( - 'reactome', - `${DB_URLS.reactome}/data/query/${encodeURIComponent(pid)}` - ) - const contained = - (await dbFetch( - 'reactome', - `${DB_URLS.reactome}/data/pathway/${encodeURIComponent(pid)}/containedEvents` - )) || [] - const byDbId = new Map( - contained.filter((e) => e && typeof e === 'object').map((e) => [e.dbId, e]) - ) - const resolve = (x) => (typeof x === 'object' ? x : byDbId.get(x)) - const reactions = [ - ...new Map( - contained.filter((e) => e && REACTION_CLASSES.has(e.schemaClass)).map((e) => [e.stId, e]) - ).values(), - ] - const notes = [] - let used = reactions - if (reactions.length > maxReactions) { - used = reactions.slice(0, maxReactions) - notes.push({ - level: 'warn', - text: `The pathway has ${reactions.length} reactions; the first ${maxReactions} were read (raise "Most reactions" for more).`, - }) - } - if (!used.length) throw new Error('This pathway has no reactions to read.') - // sub-pathways and the reactions each contains - const children = (top.hasEvent || []) - .map(resolve) - .filter((e) => e && e.schemaClass === 'Pathway') - .slice(0, 40) - dbStep('reactome', 1, `Reading ${plural(children.length, 'sub-pathway')}…`) - const childReactions = await dbMap('reactome', children, 4, async (ch) => { - const evs = - (await dbFetch( - 'reactome', - `${DB_URLS.reactome}/data/pathway/${encodeURIComponent(ch.stId)}/containedEvents` - )) || [] - return new Set(evs.filter((e) => e && typeof e === 'object').map((e) => e.stId)) - }) - // participants of each reaction - dbStep('reactome', 2, `Reading reactions: 0 of ${used.length}…`) - const parts = await dbMap( - 'reactome', - used, - 4, - (r) => - dbFetch('reactome', `${DB_URLS.reactome}/data/participants/${encodeURIComponent(r.stId)}`), - (d, n) => dbProgress('reactome', `Reading reactions: ${d} of ${n}…`) - ) - const nodeAttrs = {} - const nameOf = new Map() // reference identifier -> node name - const usedNames = new Map() - const reactionMembers = new Map() - let skippedBig = 0, - failed = 0 - parts.forEach((p, i) => { - if (!p || p.error) { - failed++ - return - } - const members = new Set() - ;(Array.isArray(p) ? p : []).forEach((part) => - (part.refEntities || []).forEach((ref) => { - const cls = ref.schemaClass || '' - const isProtein = - /ReferenceGeneProduct|ReferenceIsoform|ReferenceDNASequence|ReferenceRNASequence/.test( - cls - ) || UNIPROT_RE.test(ref.identifier || '') - if (!isProtein && !(smallMolecules && /ReferenceMolecule/.test(cls))) return - const id = String(ref.identifier || ref.dbId) - if (!nameOf.has(id)) { - const disp = String(ref.displayName || '') - let label = disp.includes(' ') - ? disp.split(/\s+/).slice(1).join(' ') - : (Array.isArray(ref.name) ? ref.name[0] : ref.name) || id - label = label || id - if (usedNames.has(label) && usedNames.get(label) !== id) label = `${label} (${id})` - usedNames.set(label, id) - nameOf.set(id, label) - nodeAttrs[label] = { - ...(isProtein ? { uniprot: id } : { chebi: id }), - reactome: `https://reactome.org/content/query?q=${encodeURIComponent(id)}`, - } - } - members.add(nameOf.get(id)) - }) - ) - if (members.size > maxSize) { - skippedBig++ - return - } - reactionMembers.set(used[i].stId, { name: used[i].displayName, members: [...members] }) - }) - if (failed) - notes.push({ level: 'warn', text: `${plural(failed, 'reaction')} could not be read.` }) - if (skippedBig) - notes.push({ - level: 'ok', - text: `Left out ${plural(skippedBig, 'reaction')} with more than ${maxSize} molecules, which would link them all to each other.`, - }) - // edges: molecules sharing a reaction, weighted by how many they share - const pairs = new Map() - reactionMembers.forEach(({ members }) => { - for (let a = 0; a < members.length; a++) - for (let b = a + 1; b < members.length; b++) { - const [x, y] = - members[a] < members[b] ? [members[a], members[b]] : [members[b], members[a]] - const k = x + '\t' + y - pairs.set(k, (pairs.get(k) || 0) + 1) - } - }) - const edges = [...pairs].map(([k, w]) => { - const [s, t] = k.split('\t') - return { source: s, target: t, weight: w, type: 'same reaction' } - }) - const groupings = [] - if (document.getElementById('reactomeGroupSub').checked && children.length) { - const groups = children.map((ch, i) => { - const set = childReactions[i] && !childReactions[i].error ? childReactions[i] : new Set() - const members = new Set() - reactionMembers.forEach((r, rid) => { - if (set.has(rid)) r.members.forEach((m) => members.add(m)) - }) - return { - name: stripTags(ch.displayName), - members: [...members], - meta: { description: `Reactome ${ch.stId}`, stId: ch.stId }, - } - }) - groupings.push({ label: 'sub-pathways', groups }) - } - if (document.getElementById('reactomeGroupReactions').checked) { - const groups = [...reactionMembers] - .filter(([, r]) => r.members.length >= 2) - .sort((a, b) => b[1].members.length - a[1].members.length) - .slice(0, 40) - .map(([rid, r]) => ({ - name: stripTags(r.name), - members: r.members, - meta: { description: `Reactome reaction ${rid}`, stId: rid }, - })) - groupings.push({ label: 'reactions', groups }) - } - dbStep('reactome', 3, 'Building and opening the network…') - await nextPaint() - dbAddImport('reactome', { - name: `Reactome ${stripTags(top.displayName)} (${pid})`, - edges, - nodeAttrs, - groupings, - notes, - summary: `from ${plural(reactionMembers.size, 'reaction')}; edges join molecules of the same reaction, weighted by how many they share`, - }) - }) -} - -/* --------------------------------------------------------------- - OMNIPATH: signalling, TF-target and ligand-receptor interactions - of the given proteins; groups from complexes, intercellular roles - or pathway annotations. - --------------------------------------------------------------- */ -export async function omnipathFetch() { - await dbRun('omnipath', async () => { - const names = dbSplitList(document.getElementById('omnipathQuery').value) - if (!names.length) - throw new Error('Type one or more gene symbols or UniProt accessions, for example EGFR.') - const organism = document.getElementById('omnipathOrganism').value - const datasets = [...document.querySelectorAll('#omnipathDatasets input:checked')].map( - (i) => i.value - ) - if (!datasets.length) throw new Error('Tick at least one dataset.') - const among = document.getElementById('omnipathAmong').checked - const maxPartners = Math.max(0, parseInt(document.getElementById('omnipathMax').value, 10) || 0) - const channels = document.getElementById('omnipathChannels').value - const wantedGroups = [...document.querySelectorAll('#omnipathGroups input:checked')].map( - (i) => i.value - ) - const annResource = document.getElementById('omnipathAnnotation').value.trim() - // steps: interactions, complexes, intercellular roles, annotations, building - const omniSteps = [ - 6, - wantedGroups.includes('complexes') ? 2 : 0, - wantedGroups.includes('intercell') ? 2 : 0, - wantedGroups.includes('annotations') && annResource ? 2 : 0, - 1, - ] - dbPlan('omnipath', omniSteps) - dbStep('omnipath', 0, 'Fetching interactions from OmniPath…') - const url = `${DB_URLS.omnipath}/interactions?partners=${encodeURIComponent(names.join(','))}&genesymbols=yes&organisms=${organism}&datasets=${datasets.join(',')}&fields=sources,references,curation_effort,type${among ? '&source_target=AND' : ''}&format=json` - const rows = (await dbFetch('omnipath', url)) || [] - if (!Array.isArray(rows) || !rows.length) - throw new Error('OmniPath has no interactions for these proteins with the ticked datasets.') - const query = new Set(names.map((n) => n.toUpperCase())) - const nodeAttrs = {} - const nodeOf = (id, sym) => { - const name = sym || id - if (!nodeAttrs[name]) - nodeAttrs[name] = { - ...(UNIPROT_RE.test(id) ? { uniprot: id } : { omnipath_id: id }), - query: query.has(String(name).toUpperCase()) || query.has(String(id).toUpperCase()), - } - return name - } - let edges = rows - .map((r) => { - const s = nodeOf(r.source, r.source_genesymbol), - t = nodeOf(r.target, r.target_genesymbol) - const stim = +r.is_stimulation === 1 || r.is_stimulation === true, - inh = +r.is_inhibition === 1 || r.is_inhibition === true - const type = - channels === 'sign' - ? stim && inh - ? 'stimulation and inhibition' - : stim - ? 'stimulation' - : inh - ? 'inhibition' - : 'unsigned' - : channels === 'type' - ? String(r.type || 'interaction').replace(/_/g, ' ') - : 'interaction' - const sources = Array.isArray(r.sources) - ? r.sources - : String(r.sources || '') - .split(';') - .filter(Boolean) - return { - source: s, - target: t, - directed: +r.is_directed === 1 || r.is_directed === true, - type, - weight: Number.isFinite(+r.curation_effort) ? +r.curation_effort : sources.length || 1, - } - }) - .filter((e) => e.source !== e.target) - const notes = [] - // keep the best-supported partners - if (!among && maxPartners > 0) { - const support = new Map() - edges.forEach((e) => - [e.source, e.target].forEach((n) => { - if (!nodeAttrs[n].query) support.set(n, (support.get(n) || 0) + e.weight) - }) - ) - if (support.size > maxPartners) { - const keep = new Set( - [...support] - .sort((a, b) => b[1] - a[1]) - .slice(0, maxPartners) - .map((x) => x[0]) - ) - const before = support.size - edges = edges.filter((e) => - [e.source, e.target].every((n) => nodeAttrs[n].query || keep.has(n)) - ) - notes.push({ - level: 'ok', - text: `Kept the ${maxPartners} best-supported of ${before} partners (by curation effort).`, - }) - } - } - // one edge per pair, channel and direction - const merged = new Map() - edges.forEach((e) => { - const k = [ - e.directed ? e.source : [e.source, e.target].sort().join('\u0000'), - e.directed ? e.target : '', - e.type, - e.directed, - ].join('\t') - const m = merged.get(k) - if (m) m.weight = Math.max(m.weight, e.weight) - else merged.set(k, { ...e }) - }) - edges = [...merged.values()] - const present = new Set(edges.flatMap((e) => [e.source, e.target])) - const uniprots = [...present].map((n) => nodeAttrs[n].uniprot).filter(Boolean) - const symOf = new Map( - [...present].filter((n) => nodeAttrs[n].uniprot).map((n) => [nodeAttrs[n].uniprot, n]) - ) - const groupings = [] - const wanted = [...document.querySelectorAll('#omnipathGroups input:checked')].map( - (i) => i.value - ) - const batches = [] - for (let i = 0; i < uniprots.length; i += 150) batches.push(uniprots.slice(i, i + 150)) - const fetchAll = async (path) => - ( - await dbMap('omnipath', batches, 2, (b) => - dbFetch( - 'omnipath', - `${DB_URLS.omnipath}/${path}${path.includes('?') ? '&' : '?'}proteins=${b.join(',')}&format=json` - ) - ) - ).flatMap((x) => (Array.isArray(x) ? x : [])) - if (wanted.includes('complexes') && uniprots.length) { - dbStep('omnipath', 1, 'Fetching complexes…') - const cx = await fetchAll('complexes') - const groups = new Map() - cx.forEach((c) => { - const comps = Array.isArray(c.components) - ? c.components - : String(c.components || '').split('_') - const members = comps.map((u) => symOf.get(u)).filter(Boolean) - if (members.length < 2) return - const name = - c.name || String(c.components_genesymbols || '').replace(/_/g, ':') || comps.join(':') - if (!groups.has(name)) - groups.set(name, { - name, - members, - meta: { - description: `Complex; sources: ${Array.isArray(c.sources) ? c.sources.join(', ') : c.sources || ''}`, - }, - }) - }) - groupings.push({ - label: 'complexes', - minSize: 2, - groups: [...groups.values()] - .sort((a, b) => b.members.length - a.members.length) - .slice(0, 60), - }) - } - if (wanted.includes('intercell') && uniprots.length) { - dbStep('omnipath', 2, 'Fetching intercellular roles…') - const ic = await fetchAll('intercell?scope=generic') - const groups = new Map() - ic.forEach((r) => { - const name = String(r.parent || r.category || '').replace(/_/g, ' ') - const sym = symOf.get(r.uniprot) - if (!name || !sym) return - if (!groups.has(name)) groups.set(name, { name, members: [] }) - groups.get(name).members.push(sym) - }) - groupings.push({ label: 'intercellular roles', groups: [...groups.values()] }) - } - const resource = document.getElementById('omnipathAnnotation').value.trim() - if (wanted.includes('annotations') && resource && uniprots.length) { - dbStep('omnipath', 3, `Fetching ${resource} annotations…`) - const an = await fetchAll(`annotations?resources=${encodeURIComponent(resource)}`) - const LABELS = [ - 'pathway', - 'location', - 'state', - 'function', - 'category', - 'mainclass', - 'classification', - 'family', - 'disease', - ] - const groups = new Map() - an.forEach((r) => { - const sym = symOf.get(r.uniprot) - if (!sym || r.value === undefined || r.value === null || r.value === '') return - if (!LABELS.includes(String(r.label).toLowerCase())) return - const name = String(r.value) - if (!groups.has(name)) - groups.set(name, { - name, - members: new Set(), - meta: { description: `${resource}: ${r.label}` }, - }) - groups.get(name).members.add(sym) - }) - if (!groups.size) - notes.push({ - level: 'warn', - text: `${resource} has no pathway-like annotations for these proteins.`, - }) - groupings.push({ - label: resource, - groups: [...groups.values()].map((g) => ({ ...g, members: [...g.members] })), - }) - } - dbStep('omnipath', omniSteps.length - 1, 'Building and opening the network…') - await nextPaint() - dbAddImport('omnipath', { - name: `OmniPath ${names.slice(0, 3).join(', ')}${names.length > 3 ? ` +${names.length - 3}` : ''}`, - edges, - nodeAttrs, - groupings, - notes, - summary: `datasets ${datasets.join(', ')}; channels by ${channels === 'sign' ? 'effect sign' : channels === 'type' ? 'interaction type' : 'nothing'}; weights are curation effort`, - }) - }) -} - -/* --------------------------------------------------------------- - NDEx: public networks by search or UUID, in CX2; node attributes - become groupings and NDEx positions are kept. - --------------------------------------------------------------- */ -export async function ndexSearch() { - await dbRun('ndex', async () => { - const q = document.getElementById('ndexQuery').value.trim() - if (!q) throw new Error('Type search words (for example "TP53 signaling") or a network UUID.') - const sel = document.getElementById('ndexNetwork') - sel.innerHTML = '' - if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(q)) { - sel.add(new Option(`Network ${q}`, q)) - } else { - dbProgress('ndex', 'Searching NDEx…') - const r = await dbFetch('ndex', `${DB_URLS.ndex}/v2/search/network?start=0&size=40`, { - method: 'POST', - body: JSON.stringify({ searchString: q }), - contentType: 'application/json', - }) - ;((r && r.networks) || []).forEach((n) => { - const big = (n.nodeCount || 0) > MAX_NETWORK_NODES ? ' — larger than NORMA shows' : '' - sel.add( - new Option( - `${n.name} · ${(n.nodeCount || 0).toLocaleString()} nodes, ${(n.edgeCount || 0).toLocaleString()} edges · ${n.owner || ''}${big}`, - n.externalId - ) - ) - }) - } - document.getElementById('ndexPickRow').hidden = !sel.options.length - dbStatus( - 'ndex', - sel.options.length - ? [ - { - level: 'ok', - text: `Found ${plural(sel.options.length, 'network')}. Choose one and fetch it.`, - }, - ] - : [{ level: 'warn', text: 'No public network matched.' }] - ) - }) -} - -// Reads a CX2 document: attribute aliases and defaults are resolved. -function readCx2(cx) { - const aspects = {} - ;(Array.isArray(cx) ? cx : []).forEach((part) => - Object.entries(part).forEach(([k, v]) => { - if (Array.isArray(v)) (aspects[k] = aspects[k] || []).push(...v) - }) - ) - const decl = (aspects.attributeDeclarations || [])[0] || {} - const resolver = (kind) => { - const d = decl[kind] || {} - const alias = {}, - defaults = {} - Object.entries(d).forEach(([name, spec]) => { - if (spec && spec.a) alias[spec.a] = name - if (spec && spec.v !== undefined) defaults[name] = spec.v - }) - return (v) => { - const out = { ...defaults } - Object.entries(v || {}).forEach(([k, val]) => { - out[alias[k] || k] = val - }) - return out - } - } - const rn = resolver('nodes'), - re = resolver('edges') - const netAttrs = Object.assign({}, ...(aspects.networkAttributes || [])) - return { - name: netAttrs.name || '', - nodes: (aspects.nodes || []).map((n) => ({ id: n.id, v: rn(n.v), x: n.x, y: n.y })), - edges: (aspects.edges || []).map((e) => ({ id: e.id, s: e.s, t: e.t, v: re(e.v) })), - declarations: decl, - } -} - -export async function ndexFetch() { - await dbRun('ndex', async () => { - const uuid = document.getElementById('ndexNetwork').value - if (!uuid) throw new Error('Search and choose a network first.') - dbPlan('ndex', [8, 1]) - dbStep('ndex', 0, 'Downloading the network from NDEx…') - const cx = await dbFetch('ndex', `${DB_URLS.ndex}/v3/networks/${encodeURIComponent(uuid)}`) - const net = readCx2(cx) - if (!net.nodes.length) throw new Error('This network has no nodes, or is not public.') - const names = new Map(), - used = new Set() - net.nodes.forEach((n) => { - let name = String(n.v.name ?? n.v.n ?? n.v.label ?? n.id).trim() || String(n.id) - if (used.has(name)) name = `${name} (${n.id})` - used.add(name) - names.set(n.id, name) - }) - const weightKey = ['weight', 'Weight', 'score', 'Score', 'confidence'].find((k) => - net.edges.some((e) => typeof e.v[k] === 'number') - ) - const edges = net.edges - .filter((e) => names.has(e.s) && names.has(e.t) && e.s !== e.t) - .map((e) => ({ - source: names.get(e.s), - target: names.get(e.t), - type: String(e.v.interaction ?? e.v.i ?? 'interacts'), - ...(weightKey && typeof e.v[weightKey] === 'number' ? { weight: e.v[weightKey] } : {}), - directed: e.v.directed === true, - })) - // node attributes and groupings from categorical attributes - const SKIP = /^(name|n|represents|r|alias|aliases|id|x|y|z|label|description|url|uri|link)$/i - const keys = new Set() - net.nodes.forEach((n) => Object.keys(n.v).forEach((k) => keys.add(k))) - const nodeAttrs = {} - net.nodes.forEach((n) => { - const a = {} - Object.entries(n.v).forEach(([k, v]) => { - if ( - !/^(name|n)$/.test(k) && - (typeof v !== 'object' || v === null) && - Object.keys(a).length < 25 - ) - a[k] = v - }) - const rep = String(n.v.represents || n.v.r || '') - const m = rep.match(/uniprot(?:kb)?:([A-Z0-9-]+)/i) - if (m) a.uniprot = m[1] - nodeAttrs[names.get(n.id)] = a - }) - const groupings = [] - ;[...keys] - .filter((k) => !SKIP.test(k)) - .forEach((k) => { - const groups = new Map() - let covered = 0 - net.nodes.forEach((n) => { - const v = n.v[k] - const vals = Array.isArray(v) - ? v - : typeof v === 'string' || typeof v === 'boolean' - ? [String(v)] - : [] - const clean = vals.map((x) => String(x).trim()).filter((x) => x && x.length <= 80) - if (clean.length) covered++ - clean.forEach((x) => { - if (!groups.has(x)) groups.set(x, []) - groups.get(x).push(names.get(n.id)) - }) - }) - if (groups.size < 2 || groups.size > 60 || covered < net.nodes.length * 0.3) return - groupings.push({ - label: `groups by ${k}`, - groups: [...groups].map(([name, members]) => ({ name, members })), - score: covered, - }) - }) - groupings.sort((a, b) => b.score - a.score) - const positions = net.nodes.every((n) => Number.isFinite(n.x) && Number.isFinite(n.y)) - ? Object.fromEntries(net.nodes.map((n) => [names.get(n.id), { x: n.x, y: n.y }])) - : null - const notes = [] - if (!groupings.length) - notes.push({ - level: 'ok', - text: 'The network has no node attribute that makes useful groups (2 to 60 values covering at least 30% of the nodes); add an annotation or use communities.', - }) - if (positions) notes.push({ level: 'ok', text: 'The layout saved in NDEx is kept.' }) - dbStep('ndex', 1, 'Building and opening the network…') - await nextPaint() - dbAddImport('ndex', { - name: `NDEx ${net.name || uuid}`.slice(0, 90), - edges, - nodeAttrs, - groupings: groupings.slice(0, 6), - positions, - notes, - summary: `network ${uuid}`, - }) - }) -} - -/* --------------------------------------------------------------- - INTACT: molecular interactions from the IntAct PSICQUIC service - (PSI-MITAB 2.5), filtered by the IntAct MI-score. - --------------------------------------------------------------- */ -function mitabName(idField, altField, aliasField) { - const all = [aliasField, altField].join('|').split('|') - const pick = (re) => { - for (const x of all) { - const m = x.match(re) - if (m) return m[1] - } - return null - } - return ( - pick(/^uniprotkb:([^(]+)\(gene name\)/) || - pick(/^[^:]+:([^(]+)\(gene name\)/) || - pick(/^psi-mi:([^(]+)\(display_short\)/) || - String(idField) - .split('|')[0] - .replace(/^[^:]+:/, '') - ) -} - -function mitabTerm(field) { - const m = String(field).match(/\(([^)]+)\)/) - return m ? m[1] : String(field).replace(/^[^:]+:/, '') -} - -export async function intactFetch() { - await dbRun('intact', async () => { - const names = dbSplitList(document.getElementById('intactQuery').value) - if (!names.length) - throw new Error('Type one or more gene names or UniProt accessions, for example TP53.') - const taxon = document.getElementById('intactTaxon').value - const minScore = parseFloat(document.getElementById('intactScore').value) || 0 - const maxRows = Math.max( - 50, - Math.min(5000, parseInt(document.getElementById('intactMaxRows').value, 10) || 1000) - ) - const among = document.getElementById('intactAmong').checked - const maxPartners = Math.max(0, parseInt(document.getElementById('intactMax').value, 10) || 0) - const ids = names.map((n) => (/[\s():]/.test(n) ? `"${n}"` : n)).join(' OR ') - let miql = `identifier:(${ids})` - if (taxon) miql += ` AND taxidA:${taxon} AND taxidB:${taxon}` - dbPlan('intact', [8, 1]) - dbStep('intact', 0, 'Fetching interactions from IntAct…') - const text = await dbFetch( - 'intact', - `${DB_URLS.intact}/query/${encodeURIComponent(miql)}?format=tab25&firstResult=0&maxResults=${maxRows}`, - { accept: 'text/plain', text: true } - ) - const lines = String(text || '') - .split('\n') - .filter((l) => l.trim()) - if (!lines.length) - throw new Error('IntAct has no interactions for these names in this organism.') - const nodeAttrs = {} - const query = new Set(names.map((n) => n.toUpperCase())) - const agg = new Map() - let lowScore = 0 - lines.forEach((line) => { - const c = line.split('\t') - if (c.length < 15) return - const nameA = mitabName(c[0], c[2], c[4]), - nameB = mitabName(c[1], c[3], c[5]) - if (!nameA || !nameB || nameA === nameB) return - const score = (String(c[14]).match(/intact-miscore:([\d.]+)/) || [])[1] - const s = score === undefined ? NaN : parseFloat(score) - if (Number.isFinite(s) && s < minScore) { - lowScore++ - return - } - ;[ - [nameA, c[0]], - [nameB, c[1]], - ].forEach(([nm, idf]) => { - if (nodeAttrs[nm]) return - const acc = (String(idf).match(/uniprotkb:([A-Z0-9-]+)/) || [])[1] - nodeAttrs[nm] = { - ...(acc ? { uniprot: acc } : { intact_id: String(idf).split('|')[0] }), - query: query.has(nm.toUpperCase()) || (acc && query.has(acc.toUpperCase())), - } - }) - const type = mitabTerm(String(c[11]).split('|')[0]) || 'interaction' - const [x, y] = nameA < nameB ? [nameA, nameB] : [nameB, nameA] - const k = `${x}\t${y}\t${type}` - const cur = agg.get(k) || { source: x, target: y, type, weight: 0, evidence: 0 } - cur.weight = Math.max(cur.weight, Number.isFinite(s) ? s : 0) - cur.evidence++ - agg.set(k, cur) - }) - let edges = [...agg.values()] - if (among) edges = edges.filter((e) => nodeAttrs[e.source].query && nodeAttrs[e.target].query) - const notes = [] - if (lines.length >= maxRows) - notes.push({ - level: 'warn', - text: `Read the first ${maxRows} interaction records; raise "Most records" for more.`, - }) - if (lowScore) - notes.push({ - level: 'ok', - text: `Left out ${plural(lowScore, 'record')} with an MI-score below ${minScore}.`, - }) - if (!among && maxPartners > 0) { - const best = new Map() - edges.forEach((e) => - [e.source, e.target].forEach((n) => { - if (!nodeAttrs[n].query) best.set(n, Math.max(best.get(n) || 0, e.weight)) - }) - ) - if (best.size > maxPartners) { - const keep = new Set( - [...best] - .sort((a, b) => b[1] - a[1]) - .slice(0, maxPartners) - .map((x) => x[0]) - ) - notes.push({ - level: 'ok', - text: `Kept the ${maxPartners} highest-scoring of ${best.size} partners.`, - }) - edges = edges.filter((e) => - [e.source, e.target].every((n) => nodeAttrs[n].query || keep.has(n)) - ) - } - } - const present = new Set(edges.flatMap((e) => [e.source, e.target])) - const groupings = [ - { - label: 'query and partners', - groups: [ - { name: 'Query proteins', members: [...present].filter((n) => nodeAttrs[n].query) }, - { - name: 'Interaction partners', - members: [...present].filter((n) => !nodeAttrs[n].query), - }, - ], - }, - ] - dbStep('intact', 1, 'Building and opening the network…') - await nextPaint() - dbAddImport('intact', { - name: `IntAct ${names.slice(0, 3).join(', ')}${names.length > 3 ? ` +${names.length - 3}` : ''}`, - edges: edges.map(({ evidence, ...e }) => e), - nodeAttrs, - groupings, - notes, - summary: `MI-score at least ${minScore}; channels are interaction types, weights the best MI-score`, - }) - }) -} - -/* --------------------------------------------------------------- - GENE ONTOLOGY: a GO-CAM model as a causal gene-to-gene network, - and GO terms (QuickGO annotations) as groupings for the view. - --------------------------------------------------------------- */ -const RO_LABELS = { - 'RO:0002629': 'directly positively regulates', - 'RO:0002630': 'directly negatively regulates', - 'RO:0002413': 'directly provides input for', - 'RO:0002211': 'regulates', - 'RO:0002213': 'positively regulates', - 'RO:0002212': 'negatively regulates', - 'RO:0002304': 'causally upstream of, positive effect', - 'RO:0002305': 'causally upstream of, negative effect', - 'RO:0002411': 'causally upstream of', - 'RO:0002418': 'causally upstream of or within', - 'RO:0012009': 'constitutively upstream of', - 'RO:0012010': 'removes input for', - 'RO:0002407': 'indirectly positively regulates', - 'RO:0002409': 'indirectly negatively regulates', -} - -const termOf = (x) => (x == null ? null : typeof x === 'string' ? x : x.term || x.id || null) - -export async function goLoadModels() { - await dbRun('go', async () => { - const taxon = document.getElementById('goTaxon').value - dbProgress('go', 'Listing GO-CAM models…') - const r = await dbFetch('go', `${DB_URLS.goapi}/taxon/${encodeURIComponent(taxon)}/models`) - const list = (Array.isArray(r) ? r : (r && (r.models || r.results)) || []).map((m) => - typeof m === 'string' - ? { id: m, title: m } - : { id: m.id || m.gocam || m.model_id, title: m.title || m.name || m.id || m.gocam } - ) - goState.models = list.filter((m) => m.id) - filterGoModels() - dbStatus('go', [ - { - level: goState.models.length ? 'ok' : 'warn', - text: goState.models.length - ? `Found ${plural(goState.models.length, 'GO-CAM model')}. Filter and choose one.` - : 'No GO-CAM models for this organism.', - }, - ]) - }) -} - -const goState = { models: [] } - -export function filterGoModels() { - const f = document.getElementById('goModelFilter').value.trim().toLowerCase() - const sel = document.getElementById('goModel') - sel.innerHTML = '' - goState.models - .filter( - (m) => - !f || String(m.title).toLowerCase().includes(f) || String(m.id).toLowerCase().includes(f) - ) - .slice(0, 300) - .forEach((m) => sel.add(new Option(m.title, m.id))) - document.getElementById('goPickRow').hidden = !goState.models.length -} - -export async function goFetchModel() { - await dbRun('go', async () => { - const typed = document.getElementById('goModelId').value.trim() - const id = (typed || document.getElementById('goModel').value || '').replace(/^gomodel:/, '') - if (!id) throw new Error('Choose a GO-CAM model or type its identifier.') - dbPlan('go', [6, 1]) - dbStep('go', 0, 'Downloading the GO-CAM model…') - const m = await dbFetch('go', `${DB_URLS.goapi}/gocam-model/${encodeURIComponent(id)}`) - const labels = new Map((m.objects || []).map((o) => [o.id, o.label || o.id])) - const label = (t) => { - const l = labels.get(t) || t - // gene product labels carry the organism, e.g. "Tp53 Mmus" - return String(l).replace(/\s+[A-Z][a-z]{3}$/, '') - } - const acts = new Map((m.activities || []).map((a) => [a.id, a])) - const enabler = (a) => termOf(a && a.enabled_by) - const nodeAttrs = {} - const nodeName = (t) => { - const n = label(t) - if (!nodeAttrs[n]) - nodeAttrs[n] = { - go_cam_id: t, - ...(/UniProtKB:/.test(t) ? { uniprot: t.replace('UniProtKB:', '') } : {}), - } - return n - } - const edges = [] - acts.forEach((a) => { - const src = enabler(a) - if (!src) return - nodeName(src) - ;(a.causal_associations || []).forEach((ca) => { - const down = acts.get(termOf(ca.downstream_activity)) - const tgt = enabler(down) - if (!tgt) return - const pred = termOf(ca.predicate) - edges.push({ - source: nodeName(src), - target: nodeName(tgt), - directed: true, - type: labels.get(pred) || RO_LABELS[pred] || pred || 'causally related', - }) - }) - }) - const byAspect = (field, lbl) => { - const groups = new Map() - acts.forEach((a) => { - const src = enabler(a) - const raw = a[field] - const terms = (Array.isArray(raw) ? raw : raw ? [raw] : []).map(termOf).filter(Boolean) - if (!src) return - terms.forEach((t) => { - const n = labels.get(t) || t - if (!groups.has(n)) - groups.set(n, { name: n, members: [], meta: { description: `${lbl} ${t}`, term: t } }) - groups.get(n).members.push(label(src)) - }) - }) - return { label: lbl, groups: [...groups.values()] } - } - const groupings = [ - byAspect('part_of', 'biological process'), - byAspect('occurs_in', 'cellular component'), - byAspect('molecular_function', 'molecular function'), - ] - if (!edges.length) throw new Error('This model has no causal links between gene products.') - dbStep('go', 1, 'Building and opening the network…') - await nextPaint() - dbAddImport('go', { - name: `GO-CAM ${m.title || id}`.slice(0, 90), - edges, - nodeAttrs, - groupings, - summary: `model ${id}; edges are causal relations between activities, pointing downstream`, - }) - }) -} - -export async function goAnnotateView() { - await dbRun('go', async () => { - if (!cy.nodes().length) throw new Error('Show a network first.') - const aspects = [...document.querySelectorAll('#goAspects input:checked')].map((i) => i.value) - if (!aspects.length) throw new Error('Tick at least one GO aspect.') - const noIEA = document.getElementById('goNoIea').checked - const minSize = Math.max(1, parseInt(document.getElementById('goMinSize').value, 10) || 2) - const maxGroups = Math.max(1, parseInt(document.getElementById('goMaxGroups').value, 10) || 30) - // UniProt accessions from node attributes or node names - const accOf = new Map() - cy.nodes().forEach((n) => { - const a = n.data('attrs') || {} - const acc = a.uniprot || (UNIPROT_RE.test(n.id()) ? n.id() : null) - if (acc) accOf.set(String(acc).replace(/-\d+$/, ''), n.id()) - }) - if (!accOf.size) - throw new Error( - 'No node has a UniProt accession (as its name or in a "uniprot" attribute). Networks from Reactome, OmniPath, IntAct and many NDEx networks carry them.' - ) - const accs = [...accOf.keys()] - const batches = [] - for (let i = 0; i < accs.length; i += 100) batches.push(accs.slice(i, i + 100)) - const groupings = [] - // steps: one per aspect, then adding the groupings - dbPlan('go', [...aspects.map(() => 3), 1]) - for (const [ai, aspect] of aspects.entries()) { - dbStep( - 'go', - ai, - `Reading ${aspect.replace('_', ' ')} annotations: 0 of ${batches.length} batches…` - ) - const terms = new Map() - const results = await dbMap( - 'go', - batches, - 2, - async (b) => { - const all = [] - for (let page = 1; page <= 25; page++) { - const r = await dbFetch( - 'go', - `${DB_URLS.quickgo}/annotation/search?geneProductId=${b.join(',')}&aspect=${aspect}&limit=200&page=${page}&includeFields=goName` - ) - all.push(...((r && r.results) || [])) - const info = r && r.pageInfo - if (!info || page >= (info.total || 1)) break - } - return all - }, - (d, n) => - dbProgress('go', `Reading ${aspect.replace('_', ' ')} annotations: ${d} of ${n} batches…`) - ) - results.flat().forEach((r) => { - if (!r || r.error) return - if (noIEA && (r.goEvidence === 'IEA' || r.evidenceCode === 'ECO:0000501')) return - if (r.qualifier && /^NOT/i.test(r.qualifier)) return - const acc = String(r.geneProductId || '') - .replace(/^UniProtKB:/, '') - .replace(/-\d+$/, '') - const node = accOf.get(acc) - if (!node) return - const name = r.goName || r.goId - if (!terms.has(name)) - terms.set(name, { - name, - members: new Set(), - meta: { description: `GO ${aspect.replace('_', ' ')}`, term: r.goId }, - }) - terms.get(name).members.add(node) - }) - const groups = [...terms.values()] - .map((t) => ({ ...t, members: [...t.members] })) - .filter((t) => t.members.length >= minSize && t.members.length < accOf.size) - .sort((a, b) => b.members.length - a.members.length) - .slice(0, maxGroups) - groupings.push({ label: `GO ${aspect.replace('_', ' ')}`, groups }) - } - dbStep('go', aspects.length, 'Adding the groupings…') - await nextPaint() - const v = activeView() - const entries = dbAddGroupingsToView('go', groupings, v ? v.name : 'View') - dbStatus( - 'go', - entries.length - ? [ - { - level: 'ok', - text: `Added ${plural(entries.length, 'GO grouping')} for ${plural(accOf.size, 'node')} with a UniProt accession: ${entries.map((e) => `${e.name.split(': ').pop()} (${e.parsed.summary})`).join(', ')}.`, - }, - ] - : [{ level: 'warn', text: 'No GO terms with enough members were found.' }] - ) - }) -} - -// page wiring, run by main.ts in the original order -export function init() { - document.getElementById('btnGaStats').addEventListener('click', renderGroupStats) - - document.getElementById('btnGaStatsTsv').addEventListener('click', groupStatsTsv) - - document.getElementById('btnGaGroupNet').addEventListener('click', openGroupNetwork) - - document.getElementById('btnGaEnrich').addEventListener('click', runEnrichment) - - document.getElementById('btnGaEnrichTsv').addEventListener('click', enrichmentTsv) - - document.getElementById('gaSets').addEventListener('change', refreshEnrichmentChoices) - - cy.on('select unselect', () => { - if (currentTab === 'profiler') refreshEnrichmentChoices() - }) - - document.getElementById('btnCompareNet').addEventListener('click', openComparisonNetwork) - - document.querySelectorAll('#infoExplore [data-steps]').forEach((btn) => - btn.addEventListener('click', () => { - if (S.infoNodeId && cy.$id(S.infoNodeId).length) - openNeighbourhood([S.infoNodeId], +btn.dataset.steps) - }) - ) - - document.getElementById('btnSelNeighbours').addEventListener('click', () => { - const ids = cy.nodes(':selected').map((n) => n.id()) - if (ids.length) openNeighbourhood(ids, 1) - }) - - document.getElementById('btnSelPaths').addEventListener('click', () => { - const ids = cy.nodes(':selected').map((n) => n.id()) - if (ids.length !== 2) return - // paths go from the node selected first to the one selected second - const rank = (id) => { - const i = selectionOrder.indexOf(id) - return i < 0 ? Infinity : i - } - const [a, b] = rank(ids[0]) <= rank(ids[1]) ? ids : [ids[1], ids[0]] - openShortestPaths(a, b) - }) - - cy.on('select', 'node', (e) => { - const id = e.target.id() - const i = selectionOrder.indexOf(id) - if (i >= 0) selectionOrder.splice(i, 1) - selectionOrder.push(id) - if (selectionOrder.length > 50) selectionOrder.shift() - }) - - cy.on('unselect', 'node', (e) => { - const i = selectionOrder.indexOf(e.target.id()) - if (i >= 0) selectionOrder.splice(i, 1) - }) - - document - .querySelectorAll('[data-export-format]') - .forEach((b) => b.addEventListener('click', () => exportOtherFormat(b.dataset.exportFormat))) - - document.getElementById('btnSessionSave').addEventListener('click', saveSession) - - document - .getElementById('btnSessionOpen') - .addEventListener('click', () => document.getElementById('sessionFileInput').click()) - - document.getElementById('sessionFileInput').addEventListener('change', (e) => { - const f = e.target.files[0] - if (f) openSessionFile(f) - e.target.value = '' - }) - - ;(function setupWebglToggle() { - const box = document.getElementById('webglToggle') - const note = document.getElementById('webglNote') - box.checked = WEBGL_ACTIVE - let stored = true - try { - localStorage.getItem('x') - } catch (e) { - stored = false - } - const describe = () => { - const wanted = box.checked - if (wanted === WEBGL_ACTIVE) { - note.textContent = WEBGL_ACTIVE ? 'WebGL drawing is on.' : '' - document.getElementById('btnWebglReload').hidden = true - } else { - note.textContent = stored - ? 'Reload the page to switch; open views are lost unless you save the session first.' - : 'This page can\u2019t store the setting; open it with ?webgl=1 at the end of the address instead.' - document.getElementById('btnWebglReload').hidden = !stored - } - } - box.addEventListener('change', () => { - try { - localStorage.setItem('norma3-webgl', box.checked ? '1' : '0') - } catch (e) {} - describe() - }) - document.getElementById('btnWebglReload').addEventListener('click', () => location.reload()) - describe() - })() - - document.getElementById('btnRuntime').addEventListener('click', runRuntimeTable) - - document.getElementById('btnRuntimeTsv').addEventListener('click', runtimeTsv) - - /* frames wiring */ - document.getElementById('btnFramePrev').addEventListener('click', () => { - stopFramePlay() - stepFrame(-1) - }) - - document.getElementById('btnFrameNext').addEventListener('click', () => { - stopFramePlay() - stepFrame(1) - }) - - document.getElementById('btnFramePlay').addEventListener('click', toggleFramePlay) - - document.getElementById('frameSlider').addEventListener('input', (e) => { - stopFramePlay() - showFrame(parseInt(e.target.value, 10) || 0) - }) - - document.getElementById('frameSpeed').addEventListener('change', () => { - if (frameState.playing) { - toggleFramePlay() - toggleFramePlay() - } - }) - - document.getElementById('btnFramesFromFiles').addEventListener('click', addFramesFromFiles) - - document.getElementById('valueSameRange').addEventListener('change', applyValueColors) - - document - .getElementById('btnCmpArenaOpen') - .addEventListener('click', () => openInArena3d('compare')) - - document - .getElementById('btnCmpArenaJson') - .addEventListener('click', () => exportArena3d('json', 'compare')) - - document - .getElementById('btnCmpArenaTsv') - .addEventListener('click', () => exportArena3d('tsv', 'compare')) - - dbState.cancelled = {} -} diff --git a/frontend/src/examples.ts b/frontend/src/examples.ts deleted file mode 100644 index 76c0f94..0000000 --- a/frontend/src/examples.ts +++ /dev/null @@ -1,78 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { NORMA_EXAMPLE_SETS } from './wiring' -import { - addNormaEntry, - libSelection, - normaLibrary, - plural, - renderLibraryLists, - setLayoutMode, -} from './layouts/controls' -import { applyEdgeCurveStyle } from './profiler' -import { applyEdgeDirection } from './export/dialog' -import { applyValueColors, scheduleLegend } from './clustering/mapping' -import { refreshLibraryView } from './library' - -/* ---------- NORMA example sets ---------- */ -// A layout to use the next time a network loads (set by showcase examples). -let nextLoadLayout = null - -export function takeNextLoadLayout() { - const name = nextLoadLayout - nextLoadLayout = null - if (name) { - setLayoutMode('connections') - document.getElementById('layoutSelect').value = name - } - return name -} - -export function loadNormaExampleSet(key) { - const set = NORMA_EXAMPLE_SETS[key] - if (!set) return - const entries = set.files.map((f, i) => { - if (!normaLibrary[f.kind]) return null // e.g. original-format copies, offered only as downloads - const sourceKey = `${key}/${i}` - const existing = normaLibrary[f.kind].find((e) => e.sourceKey === sourceKey) - return ( - existing || - addNormaEntry(f.kind, f.name, f.text, f.fileName, sourceKey, { directed: !!f.directed }) - ) - }) - const real = entries.filter(Boolean) - const net = real.find((e) => e.kind === 'network') - const ann = real.find((e) => e.kind === 'annotation') - const col = real.find((e) => e.kind === 'colors') - libSelection.networks = new Set( - set.showAll ? real.filter((e) => e.kind === 'network').map((e) => e.id) : [net.id] - ) - libSelection.annotation = ann ? ann.id : '' - libSelection.colors = col ? col.id : '' - renderLibraryLists() - nextLoadLayout = (set.display && set.display.layoutSelect) || null - refreshLibraryView() - nextLoadLayout = null - // showcase examples switch on the features they are meant to show - if (set.display) { - Object.entries(set.display).forEach(([id, value]) => { - const el = document.getElementById(id) - if (!el) return - if (el.type === 'checkbox') el.checked = !!value - else el.value = value - if (id !== 'layoutSelect') el.dispatchEvent(new Event('change', { bubbles: true })) - }) - applyEdgeDirection() - applyEdgeCurveStyle() - applyValueColors() - scheduleLegend() - } - const others = real.filter((e) => e.kind === 'annotation').length - 1 - if (others > 0) { - const el = document.getElementById('normaStatus') - const note = document.createElement('div') - note.className = 'note ok' - note.textContent = `This example has ${plural(others, 'more annotation')} in the Annotations list. Tick one and choose Show in this view to switch; node positions are kept.` - el.insertBefore(note, el.children[1] || null) - } -} diff --git a/frontend/src/export/dialog.ts b/frontend/src/export/dialog.ts deleted file mode 100644 index 4b1b760..0000000 --- a/frontend/src/export/dialog.ts +++ /dev/null @@ -1,611 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { activeView, applyEdgeCurveStyle } from '../profiler' -import { applyEdgeMerge } from '../parallel_edges' -import { - applyNodeSizing, - frPending, - getFrWorker, - invalidateFullMetrics, - updateStats, - workStep, -} from '../metrics' -import { buildSvg } from './svg' -import { buildSvg3d, export3dSize, renderExportCanvas3d } from '../view3d/export' -import { canvasToBlob, pdfFromCanvas, renderExportCanvas, withDpi } from './raster' -import { - composeCanvasWithLegend, - composeSvgWithLegend, - exportLegendLayout, - legendIsEmpty, - legendModel, -} from '../clustering/mapping' -import { currentTheme } from '../themes' -import { cy, setStyle } from '../cy' -import { exportFrame, exportPixelSize, exportSizeText, withLegendSize } from './draw' -import { markDirty3d, net3d } from '../view3d/state' -import { mulberry32 } from '../sample_data' -import { nextPaint, setStatus } from '../layouts/controls' - -/* ---------- dialog ---------- */ -const EXPORT_FORMATS = { - png: { label: 'PNG', ext: 'png', raster: true, transparent: true }, - jpeg: { label: 'JPEG', ext: 'jpg', raster: true, transparent: false }, - webp: { label: 'WebP', ext: 'webp', raster: true, transparent: true }, - svg: { label: 'SVG', ext: 'svg', raster: false, transparent: true }, - pdf: { label: 'PDF', ext: 'pdf', raster: true, transparent: false }, -} - -function exportOptions() { - const format = document.querySelector('input[name="imgFormat"]:checked').value - const bgMode = document.getElementById('imgBackground').value - const fmtInfo = EXPORT_FORMATS[format] - let bg = null - if (bgMode === 'theme') bg = currentTheme.bg - else if (bgMode === 'white') bg = '#ffffff' - else if (bgMode === 'custom') bg = document.getElementById('imgBgColor').value - if (!bg && !fmtInfo.transparent) bg = '#ffffff' - return { - format, - area: document.getElementById('imgArea').value, - scale: parseFloat(document.getElementById('imgScale').value) || 2, - bg, - hulls: - document.getElementById('imgHulls').checked && - document.getElementById('showGroupHulls').checked, - clean: document.getElementById('imgClean').checked, - legend: document.getElementById('imgLegend').checked, - fileName: (document.getElementById('imgFileName').value.trim() || 'network').replace( - /[\\/:*?"<>|]+/g, - '_' - ), - } -} - -function updateExportDialog() { - const o = exportOptions() - const info = EXPORT_FORMATS[o.format] - const bgSel = document.getElementById('imgBackground') - const transparentOpt = bgSel.querySelector('option[value="transparent"]') - transparentOpt.disabled = !info.transparent - if (!info.transparent && bgSel.value === 'transparent') bgSel.value = 'white' - document.getElementById('imgBgColor').hidden = bgSel.value !== 'custom' - document.getElementById('imgHullsRow').hidden = !document.getElementById('showGroupHulls').checked - document.getElementById('imgLegendRow').hidden = legendIsEmpty(legendModel()) - document.getElementById('imgScaleLabel').textContent = info.raster - ? 'Resolution' - : 'Size in the file' - document.getElementById('exportTitle').textContent = net3d.active - ? 'Export 3D image' - : 'Export image' - const size = document.getElementById('imgSize') - if (net3d.active) { - document.getElementById('btnImgSave').disabled = !cy.nodes().length - const s3 = export3dSize(o.scale) - const all3 = withLegendSize( - s3.outW, - s3.outH, - o.format === 'svg' ? Math.min(o.scale, 8) : s3.k, - o - ) - let t3 = exportSizeText(all3.W, all3.H, s3.k, o.format) - if (all3.W !== s3.outW) t3 += ' · legend included' - t3 += ' · 3D view from the current angle' - if (s3.limited) t3 += ' · reduced to the largest size this browser can draw' - size.textContent = t3 - document.getElementById('imgExt').textContent = '.' + info.ext - return - } - const frame = exportFrame(o.area) - if (!frame) { - size.textContent = 'Nothing is shown to export.' - document.getElementById('btnImgSave').disabled = true - return - } - document.getElementById('btnImgSave').disabled = false - const { W, H, k, limited } = exportPixelSize(frame, o.scale) - const all = withLegendSize(W, H, k / frame.unitPx, o) - let text = exportSizeText(all.W, all.H, k / frame.unitPx, o.format) - if (all.W !== W) text += ' · legend included' - if (limited) text += ' · reduced to the largest size this browser can draw' - size.textContent = text - document.getElementById('imgExt').textContent = '.' + info.ext -} - -function openExportDialog() { - const dlg = document.getElementById('exportDialog') - const v = typeof activeView === 'function' && activeView() - const nameInput = document.getElementById('imgFileName') - nameInput.value = (v ? v.name : 'network').replace(/[\\/:*?"<>|]+/g, '_') - setStatus('imgStatus', []) - updateExportDialog() - if (typeof dlg.showModal === 'function') dlg.showModal() - else dlg.setAttribute('open', '') -} - -async function saveExportImage() { - const o = exportOptions() - const info = EXPORT_FORMATS[o.format] - const btn = document.getElementById('btnImgSave') - btn.disabled = true - setStatus('imgStatus', [{ level: 'busy', text: 'Drawing the image…', progress: null }]) - await nextPaint() - // optionally hide selection and search/click highlighting while drawing - let restore = null - if (o.clean) { - const selected = cy.elements(':selected') - const dimmed = cy.elements('.dimmed'), - highlighted = cy.elements('.highlighted') - cy.batch(() => { - selected.unselect() - dimmed.removeClass('dimmed') - highlighted.removeClass('highlighted') - }) - restore = () => - cy.batch(() => { - selected.select() - dimmed.addClass('dimmed') - highlighted.addClass('highlighted') - }) - } - try { - let blob - // the legend (optional) is drawn to the right, at the picture's scale - const legendLayout = exportLegendLayout(o) - const mime = - o.format === 'jpeg' ? 'image/jpeg' : o.format === 'webp' ? 'image/webp' : 'image/png' - if (net3d.active) { - net3d.cacheDirty = true - const s3 = export3dSize(o.scale) - if (o.format === 'svg') { - const text = await buildSvg3d({ ...o, title: o.fileName }).text() - blob = new Blob([composeSvgWithLegend(text, legendLayout, Math.min(o.scale, 8), o.bg)], { - type: 'image/svg+xml', - }) - } else { - const canvas = composeCanvasWithLegend(renderExportCanvas3d(o), legendLayout, s3.k, o.bg) - blob = - o.format === 'pdf' - ? await pdfFromCanvas(canvas, canvas.width / s3.k, canvas.height / s3.k) - : await withDpi(await canvasToBlob(canvas, mime, 0.95), 96 * s3.k) - } - } else if (o.format === 'svg') { - cy.forceRender && cy.forceRender() - const frame = exportFrame(o.area) - const { k } = exportPixelSize(frame, o.scale) - const text = await buildSvg({ ...o, title: o.fileName }).text() - blob = new Blob([composeSvgWithLegend(text, legendLayout, k / frame.unitPx, o.bg)], { - type: 'image/svg+xml', - }) - } else { - cy.forceRender && cy.forceRender() - const frame = exportFrame(o.area) - const { k } = exportPixelSize(frame, o.scale) - const legendScale = k / frame.unitPx - const canvas = composeCanvasWithLegend(renderExportCanvas(o), legendLayout, legendScale, o.bg) - if (o.format === 'pdf') { - // one screen pixel = one point, so the page matches the on-screen size - blob = await pdfFromCanvas(canvas, canvas.width / legendScale, canvas.height / legendScale) - } else { - blob = await withDpi(await canvasToBlob(canvas, mime, 0.95), 96 * legendScale) - } - } - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `${o.fileName}.${info.ext}` - document.body.appendChild(a) - a.click() - a.remove() - setTimeout(() => URL.revokeObjectURL(url), 2000) - setStatus('imgStatus', [ - { - level: 'ok', - text: `Saved ${o.fileName}.${info.ext} (${(blob.size / 1024 / 1024).toFixed(blob.size > 1024 * 1024 ? 1 : 2)} MB).`, - }, - ]) - } catch (err) { - setStatus('imgStatus', [ - { level: 'error', text: `The image couldn't be saved: ${err.message}` }, - ]) - } finally { - if (restore) restore() - if (net3d.active) markDirty3d() - btn.disabled = false - } -} - -/* ============================================================ - EDGE DIRECTION - Each edge may be directed (source -> target) or undirected, from its - file or JSON. The view's Direction setting decides how edges are - handled: as in the data, all as directed, or all as undirected. - Arrows, merging, node degrees and centralities, the breadthfirst - layout, node and edge details, the Profiler, Compare and exports all - follow the effective direction. - ============================================================ */ -export function directionMode() { - const el = document.getElementById('edgeDirection') - return el ? el.value : 'data' -} - -export function edgeIsDirected(e) { - const mode = directionMode() - if (mode === 'directed') return true - if (mode === 'undirected') return false - return !!e.data('directed') -} - -// 'undirected', 'directed' or 'mixed' for a collection of edges -export function directionOf(edges) { - let d = 0, - n = 0 - edges.forEach((e) => { - n++ - if (edgeIsDirected(e)) d++ - }) - return d === 0 ? 'undirected' : d === n ? 'directed' : 'mixed' -} - -export function anyEdgeDirected(edges) { - return (edges || cy.edges()).some((e) => edgeIsDirected(e)) -} - -export function arrowsShown() { - return directionMode() !== 'undirected' && anyEdgeDirected(cy.edges()) -} - -export function applyEdgeDirection() { - const shape = document.getElementById('arrowShape').value - const scale = parseFloat(document.getElementById('arrowScale').value) || 1 - document.getElementById('arrowScaleValue').textContent = scale.toFixed(1) + '×' - const anyDirectedData = cy.edges().some((e) => e.data('directed')) - document.getElementById('edgeDirectionHint').textContent = - directionMode() === 'data' - ? anyDirectedData - ? 'Arrows show the edges marked as directed in the data.' - : 'No edge in this view is marked as directed.' - : directionMode() === 'directed' - ? 'Every edge is treated as pointing from its source to its target.' - : anyDirectedData - ? 'Direction is ignored (the default). This network has directed edges: choose "As in the data" to show them.' - : 'Direction is ignored everywhere (the default), as in earlier versions of NORMA.' - cy.batch(() => { - cy.edges().forEach((e) => { - const arrow = edgeIsDirected(e) ? shape : 'none' - if (e.data('arrow') !== arrow) e.data('arrow', arrow) - }) - }) - document.getElementById('arrowControls').hidden = !arrowsShown() - setStyle('edge', { - 'target-arrow-shape': 'data(arrow)', - 'target-arrow-color': 'data(color)', - 'arrow-scale': scale, - }) - // merged edges and haystack edges depend on direction - applyEdgeMerge() - applyEdgeCurveStyle() - invalidateFullMetrics() - if (document.getElementById('sizeMetric').value !== 'fixed') applyNodeSizing() - updateStats() -} - -/* ============================================================ - 3D NETWORK - A second view of the same network, drawn in 3D on a plain canvas. - Everything visual is read from the 2D (Cytoscape) elements, so groups, - colors, pies, shapes, labels, edge labels, arrows, selection, search - highlighting and ticked groups/channels look and behave the same. - Only node positions (x, y, z) and the camera are 3D-specific; both are - kept per view. - Coordinates: x to the right, y down (as in 2D), z away from the viewer. - ============================================================ */ - -/* ---------- 3D force-directed layout (weighted Fruchterman-Reingold) ---------- - Self-contained so it can also run in the background worker. - edges: [{ s, t, w }] with node ids; init: optional { id: [x, y, z] }. */ -export function fr3dLayout(ids, edges, opts) { - opts = opts || {} - const n = ids.length - const out = {} - if (!n) return out - const rand = mulberry32(opts.seed || 123) - const idx = new Map(ids.map((id, i) => [id, i])) - const K = opts.spacing || 60 // ideal edge length - const E = [] - let maxW = 0 - edges.forEach((e) => { - const a = idx.get(e.s), - b = idx.get(e.t) - if (a === undefined || b === undefined || a === b) return - const w = Number.isFinite(e.w) && e.w > 0 ? e.w : 1 - if (w > maxW) maxW = w - E.push([a, b, w]) - }) - // connected components (union-find); each is laid out on its own, as in - // the 2D weighted layout, so unconnected pieces don't drift away - const parent = Int32Array.from({ length: n }, (_, i) => i) - const find = (x) => { - while (parent[x] !== x) { - parent[x] = parent[parent[x]] - x = parent[x] - } - return x - } - E.forEach(([a, b]) => { - const ra = find(a), - rb = find(b) - if (ra !== rb) parent[ra] = rb - }) - const compMap = new Map() - for (let i = 0; i < n; i++) { - const r = find(i) - if (!compMap.has(r)) compMap.set(r, []) - compMap.get(r).push(i) - } - const comps = [...compMap.values()].sort((x, y) => y.length - x.length) - const compOf = new Int32Array(n), - local = new Int32Array(n) - comps.forEach((m, c) => - m.forEach((g, i) => { - compOf[g] = c - local[g] = i - }) - ) - const compEdges = comps.map(() => []) - E.forEach(([a, b, w]) => compEdges[compOf[a]].push([local[a], local[b], w])) - - function layoutComponent(m, CE) { - const N = m - const X = new Float64Array(N), - Y = new Float64Array(N), - Z = new Float64Array(N) - const R0 = K * Math.cbrt(N) * 0.9 - for (let i = 0; i < N; i++) { - let x, y, z - do { - x = rand() * 2 - 1 - y = rand() * 2 - 1 - z = rand() * 2 - 1 - } while (x * x + y * y + z * z > 1) - X[i] = x * R0 - Y[i] = y * R0 - Z[i] = z * R0 - } - if (N === 1) { - X[0] = Y[0] = Z[0] = 0 - return { X, Y, Z, R: K * 0.5 } - } - const iters = opts.iterations || (N <= 300 ? 400 : N <= 1500 ? 250 : 120) - const DX = new Float64Array(N), - DY = new Float64Array(N), - DZ = new Float64Array(N) - const K2 = K * K - const useGrid = N > 1500 - let temp = R0 * 0.5 - const cool = temp / (iters + 1) - for (let it = 0; it < iters; it++) { - if ((it & 3) === 0) workStep(it / iters) - DX.fill(0) - DY.fill(0) - DZ.fill(0) - if (!useGrid) { - for (let i = 0; i < N; i++) { - for (let j = i + 1; j < N; j++) { - let dx = X[i] - X[j], - dy = Y[i] - Y[j], - dz = Z[i] - Z[j] - let d2 = dx * dx + dy * dy + dz * dz - if (d2 < 0.01) { - dx = rand() - 0.5 - dy = rand() - 0.5 - dz = rand() - 0.5 - d2 = 0.01 - } - const f = K2 / d2 - DX[i] += dx * f - DY[i] += dy * f - DZ[i] += dz * f - DX[j] -= dx * f - DY[j] -= dy * f - DZ[j] -= dz * f - } - } - } else { - // repulsion only from nodes in the neighbouring grid cells - const cell = 2 * K - const grid = new Map() - const key = (p, q, r) => p + ',' + q + ',' + r - for (let i = 0; i < N; i++) { - const k = key(Math.floor(X[i] / cell), Math.floor(Y[i] / cell), Math.floor(Z[i] / cell)) - const list = grid.get(k) - if (list) list.push(i) - else grid.set(k, [i]) - } - for (let i = 0; i < N; i++) { - const cx = Math.floor(X[i] / cell), - cy = Math.floor(Y[i] / cell), - cz = Math.floor(Z[i] / cell) - for (let p = -1; p <= 1; p++) - for (let q = -1; q <= 1; q++) - for (let r = -1; r <= 1; r++) { - const list = grid.get(key(cx + p, cy + q, cz + r)) - if (!list) continue - for (let t = 0; t < list.length; t++) { - const j = list[t] - if (j === i) continue - let dx = X[i] - X[j], - dy = Y[i] - Y[j], - dz = Z[i] - Z[j] - let d2 = dx * dx + dy * dy + dz * dz - if (d2 < 0.01) { - dx = rand() - 0.5 - dy = rand() - 0.5 - dz = rand() - 0.5 - d2 = 0.01 - } - const f = K2 / d2 - DX[i] += dx * f - DY[i] += dy * f - DZ[i] += dz * f - } - } - } - } - for (let e = 0; e < CE.length; e++) { - const [p, q, w] = CE[e] - const dx = X[p] - X[q], - dy = Y[p] - Y[q], - dz = Z[p] - Z[q] - const d = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01 - const f = (d / K) * (w / maxW) // (d^2 / K) / d, scaled by weight - DX[p] -= dx * f - DY[p] -= dy * f - DZ[p] -= dz * f - DX[q] += dx * f - DY[q] += dy * f - DZ[q] += dz * f - } - for (let i = 0; i < N; i++) { - DX[i] -= X[i] * 0.01 - DY[i] -= Y[i] * 0.01 - DZ[i] -= Z[i] * 0.01 - const d = Math.sqrt(DX[i] * DX[i] + DY[i] * DY[i] + DZ[i] * DZ[i]) || 1 - const step = Math.min(d, temp) - X[i] += (DX[i] / d) * step - Y[i] += (DY[i] / d) * step - Z[i] += (DZ[i] / d) * step - } - temp = Math.max(K * 0.02, temp - cool) - } - let mx = 0, - my = 0, - mz = 0 - for (let i = 0; i < N; i++) { - mx += X[i] - my += Y[i] - mz += Z[i] - } - mx /= N - my /= N - mz /= N - let R = 0 - for (let i = 0; i < N; i++) { - X[i] -= mx - Y[i] -= my - Z[i] -= mz - R = Math.max(R, Math.sqrt(X[i] * X[i] + Y[i] * Y[i] + Z[i] * Z[i])) - } - return { X, Y, Z, R: R + K * 0.5 } - } - - let laidOut = 0 - const parts = comps.map((m, c) => { - S.workRange = [laidOut / n, (laidOut + m.length) / n] - laidOut += m.length - return layoutComponent(m.length, compEdges[c]) - }) - S.workRange = [0, 1] - // pack: the largest piece in the middle, the others on a shell around it - const centers = [[0, 0, 0]] - if (parts.length > 1) { - const others = parts.slice(1) - const maxR = Math.max(...others.map((p) => p.R)) - const area = others.reduce((s, p) => s + Math.PI * (p.R + K * 0.5) ** 2, 0) * 1.4 - const shell = Math.max(parts[0].R + maxR + K, Math.sqrt(area / (4 * Math.PI))) - const golden = Math.PI * (3 - Math.sqrt(5)) - others.forEach((p, i) => { - const m = others.length - const y = m === 1 ? 0 : 1 - (i / (m - 1)) * 2 - const r = Math.sqrt(Math.max(0, 1 - y * y)) - const t = golden * i - centers.push([Math.cos(t) * r * shell, y * shell, Math.sin(t) * r * shell]) - }) - } - comps.forEach((m, c) => { - const p = parts[c], - ctr = centers[c] - m.forEach((g, i) => { - out[ids[g]] = [p.X[i] + ctr[0], p.Y[i] + ctr[1], p.Z[i] + ctr[2]] - }) - }) - return out -} - -export function fr3dAsync(ids, edges, opts, onProgress) { - const worker = ids.length > 150 ? getFrWorker() : null - if (!worker) return Promise.resolve(fr3dLayout(ids, edges, opts)) - const id = ++S.frRequestSeq - return new Promise((resolve) => { - frPending.set(id, { resolve, onProgress, local: () => fr3dLayout(ids, edges, opts) }) - worker.postMessage({ id, kind: 'fr3d', ids, edges, opts }) - }) -} - -// Points spread evenly on a unit sphere (Fibonacci lattice). -export function fibonacciSphere(count) { - const pts = [] - const golden = Math.PI * (3 - Math.sqrt(5)) - for (let i = 0; i < count; i++) { - const y = count === 1 ? 0 : 1 - (i / (count - 1)) * 2 - const r = Math.sqrt(Math.max(0, 1 - y * y)) - const t = golden * i - pts.push([Math.cos(t) * r, y, Math.sin(t) * r]) - } - return pts -} - -// Points filling a ball of the given radius, first ones in the middle. -export function fibonacciBall(count, radius) { - const shell = fibonacciSphere(count) - return shell.map((p, i) => { - const r = radius * Math.cbrt((i + 0.5) / count) - return [p[0] * r, p[1] * r, p[2] * r] - }) -} - -// page wiring, run by main.ts in the original order -export function init() { - ;['imgArea', 'imgScale', 'imgBackground', 'imgHulls', 'imgClean', 'imgLegend'].forEach((id) => { - document.getElementById(id).addEventListener('change', updateExportDialog) - }) - - document - .querySelectorAll('input[name="imgFormat"]') - .forEach((r) => r.addEventListener('change', updateExportDialog)) - - document.getElementById('btnImgSave').addEventListener('click', saveExportImage) - - document - .getElementById('btnImgCancel') - .addEventListener('click', () => document.getElementById('exportDialog').close()) - - document.getElementById('btnExportImage').addEventListener('click', openExportDialog) - - document.getElementById('btnExportImage2').addEventListener('click', openExportDialog) - - ;(function () { - const tag = document.getElementById('statDirection') - const useData = () => { - if (!tag.classList.contains('stat-dir-off')) return - const sel = document.getElementById('edgeDirection') - sel.value = 'data' - // a real change event, so undo and the view's settings follow as usual - sel.dispatchEvent(new Event('change', { bubbles: true })) - } - tag.addEventListener('click', useData) - tag.addEventListener('keydown', (ev) => { - if (ev.key === 'Enter' || ev.key === ' ') { - ev.preventDefault() - useData() - } - }) - })() - - ;['edgeDirection', 'arrowShape'].forEach((id) => { - document.getElementById(id).addEventListener('change', applyEdgeDirection) - }) - - document.getElementById('arrowScale').addEventListener('input', applyEdgeDirection) -} diff --git a/frontend/src/export/draw.ts b/frontend/src/export/draw.ts deleted file mode 100644 index 22526fd..0000000 --- a/frontend/src/export/draw.ts +++ /dev/null @@ -1,516 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { PROFILE_DIRECTED_STATS } from '../directed_stats' -import { PROFILE_STATS, formatStat } from '../wiring' -import { - UPSET_MAX_COLUMNS, - compareState, - exclusiveCounts, - jaccard, - membership, - runComparison, - spearman, -} from '../label_colors' -import { cy } from '../cy' -import { downloadText } from '../layouts/controls' -import { escapeHtml } from '../network_state' -import { layoutLegend, legendIsEmpty, legendModel } from '../clustering/mapping' -import { profSection } from '../profiler' - -/* ---------- drawing ---------- */ -function legendHtml(nets) { - return `
    ${nets - .map( - (n, i) => - `${String.fromCharCode(65 + i)}: ${escapeHtml(n.name)}` - ) - .join('')}
    ` -} - -// Venn diagram for 2 or 3 sets. Regions are labelled with exact counts; -// circle sizes are schematic, not proportional. -function vennSvg(nets, sets) { - const k = sets.length - const counts = exclusiveCounts(membership(sets)) - const c = (m) => (counts.get(m) || 0).toLocaleString() - const W = 360, - H = k === 2 ? 220 : 300 - const circles = - k === 2 - ? [ - { x: 140, y: 110, r: 88 }, - { x: 220, y: 110, r: 88 }, - ] - : [ - { x: 145, y: 115, r: 88 }, - { x: 215, y: 115, r: 88 }, - { x: 180, y: 180, r: 88 }, - ] - const regions = - k === 2 - ? [ - [1, 95, 114], - [2, 265, 114], - [3, 180, 114], - ] - : [ - [1, 105, 100], - [2, 255, 100], - [4, 180, 240], - [3, 180, 72], - [5, 128, 178], - [6, 232, 178], - [7, 180, 140], - ] - let svg = `` - circles.forEach((ci, i) => { - svg += `` - }) - const labelPos = - k === 2 - ? [ - [12, 18, 'start'], - [348, 18, 'end'], - ] - : [ - [12, 18, 'start'], - [348, 18, 'end'], - [180, 294, 'middle'], - ] - labelPos.forEach(([x, y, anchor], i) => { - svg += `${String.fromCharCode(65 + i)} (${sets[i].size.toLocaleString()})` - }) - regions.forEach(([m, x, y]) => { - svg += `${c(m)}` - }) - return svg + '' -} - -// UpSet plot: every non-empty exclusive intersection as a column. -function upsetSvg(nets, sets) { - const k = sets.length - const allCounts = [...exclusiveCounts(membership(sets))].sort( - (a, b) => b[1] - a[1] || a[0] - b[0] - ) - // with many networks there can be hundreds of combinations: show the largest - const counts = allCounts.slice(0, UPSET_MAX_COLUMNS) - const colW = k > 6 ? 22 : 26, - rowH = k > 6 ? 18 : 22, - leftW = 220, - barH = 130, - top = 16 - const W = leftW + counts.length * colW + 20 - const H = top + barH + 10 + k * rowH + 10 - const maxC = Math.max(...counts.map((x) => x[1]), 1) - const maxS = Math.max(...sets.map((s) => s.size), 1) - let svg = `` - // intersection bars - counts.forEach(([m, n], j) => { - const x = leftW + j * colW + 5 - const h = (n / maxC) * barH - const y = top + barH - h - svg += `${n.toLocaleString()} in exactly: ${sets - .map((_, i) => ((m >> i) & 1 ? String.fromCharCode(65 + i) : '')) - .filter(Boolean) - .join(' + ')}` - svg += `${n.toLocaleString()}` - }) - svg += `` - // set rows: size bars (right-aligned toward the matrix) and names - const matrixTop = top + barH + 10 - sets.forEach((set, i) => { - const y = matrixTop + i * rowH - if (i % 2 === 0) - svg += `` - const bw = (set.size / maxS) * 60 - svg += `${set.size.toLocaleString()}` - const short = nets[i].name.length > 20 ? nets[i].name.slice(0, 19) + '…' : nets[i].name - svg += `${escapeHtml(nets[i].name)}${String.fromCharCode(65 + i)} ${escapeHtml(short)}` - }) - // dot matrix - counts.forEach(([m], j) => { - const cx = leftW + j * colW + colW / 2 - const on = [] - for (let i = 0; i < k; i++) { - const cy0 = matrixTop + i * rowH + rowH / 2 - const member = (m >> i) & 1 - if (member) on.push(cy0) - svg += `` - } - if (on.length > 1) - svg += `` - }) - svg += '' - if (allCounts.length > counts.length) { - const rest = allCounts.slice(counts.length).reduce((t, c) => t + c[1], 0) - svg += `

    Showing the ${counts.length} largest of ${allCounts.length} combinations; the other ${allCounts.length - counts.length} hold ${rest.toLocaleString()} items in total (all are in the downloaded table).

    ` - } - return svg -} - -// Square matrix with shaded cells; values in [0, 1] unless `signed`. -function matrixHtml(nets, value, format, signed) { - let html = - '' + - nets - .map( - (n, i) => - `` - ) - .join('') + - '' - nets.forEach((a, i) => { - html += `` - nets.forEach((b, j) => { - const v = value(i, j) - if (v == null || Number.isNaN(v.v)) { - html += '' - return - } - const t = signed ? Math.abs(v.v) : v.v - const hue = signed && v.v < 0 ? '#dc2626' : '#2563eb' - html += `` - }) - html += '' - }) - return html + '
    ${String.fromCharCode(65 + i)}
    ${String.fromCharCode(65 + i)}–${format(v)}
    ' -} - -// Complementary cumulative degree distribution, log-log, one line per network. -function ccdfSvg(nets) { - const W = 520, - H = 260, - L = 48, - R = 12, - T = 12, - B = 34 - const pw = W - L - R, - ph = H - T - B - let maxDeg = 1 - nets.forEach((n) => - n.stats.degree.forEach((d) => { - if (d > maxDeg) maxDeg = d - }) - ) - const lx = Math.log10(Math.max(maxDeg, 10)) - const minP = Math.min( - ...nets.map((n) => 1 / Math.max(1, n.stats.degree.filter((d) => d > 0).length)) - ) - const ly = Math.log10(minP) - const X = (d) => L + (Math.log10(d) / lx) * pw - const Y = (p) => T + (Math.log10(p) / ly) * ph - let svg = `` - svg += `` - for (let e = 0; e <= Math.ceil(lx); e++) { - const d = 10 ** e - if (d > 10 ** lx * 1.001) break - svg += `${d.toLocaleString()}` - } - for (let e = 0; e >= Math.ceil(ly - 1e-9); e--) { - const p = 10 ** e - svg += `${p >= 0.01 ? p : p.toExponential(0)}` - } - svg += `degree k` - svg += `share of nodes with degree ≥ k` - nets.forEach((n) => { - const degs = n.stats.degree.filter((d) => d > 0).sort((a, b) => a - b) - if (!degs.length) return - const pts = [] - const total = degs.length - for (let i = 0; i < degs.length; i++) { - if (i > 0 && degs[i] === degs[i - 1]) continue - pts.push([degs[i], (total - i) / total]) - } - const path = pts - .map(([d, p], i) => `${i ? 'L' : 'M'}${X(d).toFixed(1)},${Y(p).toFixed(1)}`) - .join('') - svg += `` - }) - return svg + '' -} - -export function renderComparison(nets) { - const root = document.getElementById('cmpResults') - root.innerHTML = '' - const letter = (i) => String.fromCharCode(65 + i) - const nodeSets = nets.map((n) => n.nodeSet) - const edgeSets = nets.map((n) => n.edgeSet) - - // overview - const ov = profSection( - 'Networks compared', - document.getElementById('cmpDirected').checked - ? 'Nodes are matched by name, edges by the nodes they join and their direction (channels ignored): A→B, B→A and an undirected A–B are different edges.' - : 'Nodes are matched by name, edges by the pair of nodes they join (channels and direction ignored).' - ) - ov.insertAdjacentHTML( - 'beforeend', - `
    ${nets - .map( - (n, i) => ` -
    -
    ${letter(i)}
    -
    ${escapeHtml(n.name)}
    -
    ${n.kind === 'view' ? 'view' : 'file'} · ${n.nodeSet.size.toLocaleString()} nodes · ${n.edgeSet.size.toLocaleString()} edges
    -
    ` - ) - .join('')}
    ` - ) - const shareNodes = [...membership(nodeSets).values()].filter( - (m) => m === (1 << nets.length) - 1 - ).length - const shareEdges = [...membership(edgeSets).values()].filter( - (m) => m === (1 << nets.length) - 1 - ).length - ov.insertAdjacentHTML( - 'beforeend', - `

    ${shareNodes.toLocaleString()} nodes and ${shareEdges.toLocaleString()} edges are in all ${nets.length} networks.

    ` - ) - root.appendChild(ov) - - // overlaps - ;[ - ['Node overlap', nodeSets, 'nodes'], - ['Edge overlap', edgeSets, 'edges'], - ].forEach(([title, sets, what]) => { - const sec = profSection( - title, - nets.length <= 3 - ? `Left: Venn diagram with the number of ${what} in each region (circle sizes are schematic). Right: the same regions as an UpSet plot, largest first.` - : `UpSet plot: each column is a group of ${what} found in exactly the networks marked below it, largest first. Venn diagrams are shown for two or three networks.` - ) - sec.insertAdjacentHTML('beforeend', legendHtml(nets)) - const cards = document.createElement('div') - cards.className = 'cmp-row' - if (nets.length <= 3) - cards.insertAdjacentHTML( - 'beforeend', - `
    ${vennSvg(nets, sets)}
    ` - ) - cards.insertAdjacentHTML( - 'beforeend', - `
    ${upsetSvg(nets, sets)}
    ` - ) - sec.appendChild(cards) - sec.insertAdjacentHTML( - 'beforeend', - `

    Jaccard similarity of ${what}

    Shared ${what} divided by ${what} in either network; hover a cell for the counts.

    ` + - matrixHtml( - nets, - (i, j) => { - const r = jaccard(sets[i], sets[j]) - return { - v: r.j, - title: `${letter(i)} and ${letter(j)}: ${r.inter.toLocaleString()} shared of ${r.union.toLocaleString()}`, - } - }, - (v) => v.v.toFixed(2) - ) - ) - root.appendChild(sec) - }) - - // degree agreement - const dsec = profSection( - 'Degree agreement on shared nodes', - 'Spearman correlation between the degrees a node has in two networks, over the nodes both contain. Near 1: hubs stay hubs; near 0: unrelated; negative: roles swap.' - ) - dsec.insertAdjacentHTML( - 'beforeend', - matrixHtml( - nets, - (i, j) => { - if (i === j) return { v: 1, title: 'same network' } - const shared = [...nets[i].nodeSet].filter((x) => nets[j].nodeSet.has(x)) - const rho = spearman( - shared.map((x) => nets[i].degreeOf.get(x)), - shared.map((x) => nets[j].degreeOf.get(x)) - ) - return { v: rho, title: `${shared.length.toLocaleString()} shared nodes` } - }, - (v) => v.v.toFixed(2), - true - ) - ) - root.appendChild(dsec) - - // degree distributions - const ccdf = profSection( - 'Degree distributions', - 'Share of nodes with at least a given degree, on log–log axes. Straight, shallow lines indicate heavy-tailed (hub-dominated) networks.' - ) - ccdf.insertAdjacentHTML( - 'beforeend', - legendHtml(nets) + `
    ${ccdfSvg(nets)}
    ` - ) - root.appendChild(ccdf) - - // topology table - const tsec = profSection( - 'Topology side by side', - 'The profiler\u2019s statistics for each network. The highest value in each row is marked.' - ) - let html = - '
    ' + - nets - .map( - (n, i) => - `` - ) - .join('') + - '' - PROFILE_STATS.forEach((st) => { - const vals = nets.map((n) => n.stats[st.key]) - const nums = vals.filter((v) => typeof v === 'number' && Number.isFinite(v)) - const max = nums.length > 1 && new Set(nums).size > 1 ? Math.max(...nums) : null - html += - `` + - vals - .map( - (v) => - `` - ) - .join('') + - '' - }) - if (nets.some((n) => n.directed)) { - html += `` - PROFILE_DIRECTED_STATS.forEach((st) => { - const vals = nets.map((n) => (n.directed ? n.directed[st.key] : undefined)) - const nums = vals.filter((v) => typeof v === 'number' && Number.isFinite(v)) - const max = nums.length > 1 && new Set(nums).size > 1 ? Math.max(...nums) : null - html += - `` + - vals - .map( - (v) => - `` - ) - .join('') + - '' - }) - } - html += '
    Statistic${letter(i)}: ${escapeHtml(n.name)}
    ${escapeHtml(st.label)}${escapeHtml(st.desc)}${escapeHtml(formatStat(v))}
    Direction
    ${escapeHtml(st.label)}${escapeHtml(st.desc)}${v === undefined ? '—' : escapeHtml(formatStat(v))}
    ' - tsec.insertAdjacentHTML('beforeend', html) - root.appendChild(tsec) -} - -function comparisonTsv() { - const nets = compareState.results - if (!nets) return - const letter = (i) => String.fromCharCode(65 + i) - const header = nets.map((n, i) => `${letter(i)} ${n.name}`) - const nodeMask = membership(nets.map((n) => n.nodeSet)) - const edgeMask = membership(nets.map((n) => n.edgeSet)) - const lines = ['# Node membership (1 = present)', ['Node', ...header].join('\t')] - ;[...nodeMask] - .sort((a, b) => (a[0] < b[0] ? -1 : 1)) - .forEach(([id, m]) => lines.push([id, ...nets.map((_, i) => (m >> i) & 1)].join('\t'))) - lines.push( - '', - '# Edge membership (1 = present)', - ['Source', 'Target', 'Direction', ...header].join('\t') - ) - ;[...edgeMask] - .sort((a, b) => (a[0] < b[0] ? -1 : 1)) - .forEach(([key, m]) => - lines.push([...key.split('\t'), ...nets.map((_, i) => (m >> i) & 1)].join('\t')) - ) - lines.push('', '# Topology', ['Statistic', ...header].join('\t')) - PROFILE_STATS.forEach((st) => - lines.push([st.label, ...nets.map((n) => formatStat(n.stats[st.key]))].join('\t')) - ) - if (nets.some((n) => n.directed)) - PROFILE_DIRECTED_STATS.forEach((st) => - lines.push( - [st.label, ...nets.map((n) => (n.directed ? formatStat(n.directed[st.key]) : ''))].join( - '\t' - ) - ) - ) - downloadText('network-comparison.tsv', lines.join('\n') + '\n') -} - -/* ============================================================ - IMAGE EXPORT - PNG, JPEG and WebP are drawn by Cytoscape's own renderer onto a - canvas of the chosen resolution, with group shading drawn underneath - (it lives on a separate layer on screen). SVG is written element by - element, so it stays sharp at any size and can be edited. PDF wraps - a high-resolution JPEG in a single page sized to the image. - Area: the whole shown network, or exactly what is on screen. - ============================================================ */ -export const EXPORT_MAX_SIDE = 16384 -// common browser canvas limit -export const EXPORT_MAX_PIXELS = (16384 * 16384) / 2.2 - -export function exportFrame(area) { - if (area === 'view') { - const z = cy.zoom(), - pan = cy.pan() - return { x1: -pan.x / z, y1: -pan.y / z, w: cy.width() / z, h: cy.height() / z, unitPx: z } - } - const eles = cy.elements().filter((e) => e.visible()) - if (!eles.length) return null - const bb = eles.boundingBox({ includeLabels: true, includeOverlays: false }) - // room for group shading around the outermost nodes - const shading = document.getElementById('showGroupHulls').checked ? hullMarginModel() + 8 : 0 - const pad = Math.max(16, shading) - return { x1: bb.x1 - pad, y1: bb.y1 - pad, w: bb.w + 2 * pad, h: bb.h + 2 * pad, unitPx: 1 } -} - -// Shading margin in model units, matching what the screen shows. -export function hullMarginModel() { - const z = cy.zoom() - return (30 * Math.max(0.15, Math.min(1, z))) / z -} - -// Size of the whole exported picture, including the legend when it is added. -export function withLegendSize(W, H, factor, o) { - if (!o.legend || document.getElementById('imgLegendRow').hidden) return { W, H } - const m = legendModel() - if (legendIsEmpty(m)) return { W, H } - const lay = layoutLegend(m) - return { W: W + Math.round(lay.w * factor), H: Math.max(H, Math.round(lay.h * factor)) } -} - -// "W × H px · N dpi at on-screen size · print size at 300 dpi" for the export dialog. -export function exportSizeText(W, H, factor, format) { - const px = `${W.toLocaleString('en-US')} × ${H.toLocaleString('en-US')} px` - if (format === 'svg') - return `${px} (the picture's size in the file); vector graphics stay sharp at any size and print resolution` - const dpi = Math.round(96 * factor) - const inch = (v) => v.toFixed(v < 10 ? 1 : 0) - const at = (d) => - `${inch(W / d)} × ${inch(H / d)} in (${((W / d) * 2.54).toFixed(1)} × ${((H / d) * 2.54).toFixed(1)} cm)` - let text = `${px} · ${dpi} dpi when printed at the on-screen size, ${at(dpi)} · ${at(300)} at 300 dpi` - if (format === 'pdf') text += ' · one page at the on-screen size' - return text -} - -// Pixel size for a frame at a given scale, reduced to stay within canvas limits. -export function exportPixelSize(frame, scale) { - let k = frame.unitPx * scale - const W = frame.w * k, - H = frame.h * k - const shrink = Math.min( - 1, - EXPORT_MAX_SIDE / W, - EXPORT_MAX_SIDE / H, - Math.sqrt(EXPORT_MAX_PIXELS / (W * H)) - ) - k *= shrink - return { - k, - W: Math.max(1, Math.round(frame.w * k)), - H: Math.max(1, Math.round(frame.h * k)), - limited: shrink < 1, - } -} - -// page wiring, run by main.ts in the original order -export function init() { - document.getElementById('btnCompare').addEventListener('click', runComparison) - - document.getElementById('btnCompareTsv').addEventListener('click', comparisonTsv) -} diff --git a/frontend/src/export/raster.ts b/frontend/src/export/raster.ts deleted file mode 100644 index 6ab443e..0000000 --- a/frontend/src/export/raster.ts +++ /dev/null @@ -1,145 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { cy } from '../cy' -import { drawHullsOnCanvas, hullShapes } from './shading' -import { exportFrame, exportPixelSize } from './draw' - -/* ---------- raster ---------- */ -export function renderExportCanvas(opts) { - const frame = exportFrame(opts.area) - if (!frame) throw new Error('Nothing is shown to export.') - const { k, W, H } = exportPixelSize(frame, opts.scale) - const canvas = document.createElement('canvas') - canvas.width = W - canvas.height = H - const ctx = canvas.getContext('2d') - if (opts.bg) { - ctx.fillStyle = opts.bg - ctx.fillRect(0, 0, W, H) - } - const toOut = (p) => ({ x: (p.x - frame.x1) * k, y: (p.y - frame.y1) * k }) - if (opts.hulls) drawHullsOnCanvas(ctx, hullShapes(toOut, k), k) - const R = cy.renderer() - if (R && typeof R.drawElements === 'function' && typeof R.getCachedZSortedEles === 'function') { - ctx.save() - ctx.setTransform(k, 0, 0, k, -frame.x1 * k, -frame.y1 * k) - R.drawElements(ctx, R.getCachedZSortedEles()) - ctx.restore() - } else { - throw new Error('This browser build of Cytoscape cannot draw images.') - } - return canvas -} - -// Writes the resolution into PNG (pHYs chunk) and JPEG (JFIF density) -// files, so layout and print programs open them at the right size. -const CRC_TABLE = (() => { - const t = new Uint32Array(256) - for (let n = 0; n < 256; n++) { - let c = n - for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 - t[n] = c >>> 0 - } - return t -})() - -function crc32(bytes) { - let c = 0xffffffff - for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8) - return (c ^ 0xffffffff) >>> 0 -} - -export async function withDpi(blob, dpi) { - if (!blob || !(dpi > 0)) return blob - const buf = new Uint8Array(await blob.arrayBuffer()) - if (blob.type === 'image/png' && buf[12] === 0x49 && buf[13] === 0x48) { - // IHDR first - const ppm = Math.round(dpi / 0.0254) - const chunk = new Uint8Array(21) - const dv = new DataView(chunk.buffer) - dv.setUint32(0, 9) - chunk.set([0x70, 0x48, 0x59, 0x73], 4) // "pHYs" - dv.setUint32(8, ppm) - dv.setUint32(12, ppm) - chunk[16] = 1 // per metre - dv.setUint32(17, crc32(chunk.subarray(4, 17))) - const ihdrEnd = 8 + 25 - const out = new Uint8Array(buf.length + chunk.length) - out.set(buf.subarray(0, ihdrEnd), 0) - out.set(chunk, ihdrEnd) - out.set(buf.subarray(ihdrEnd), ihdrEnd + chunk.length) - return new Blob([out], { type: 'image/png' }) - } - if ( - blob.type === 'image/jpeg' && - buf[2] === 0xff && - buf[3] === 0xe0 && - buf[6] === 0x4a && - buf[7] === 0x46 - ) { - // JFIF APP0 - const d = Math.min(65535, Math.round(dpi)) - buf[13] = 1 // dots per inch - buf[14] = d >> 8 - buf[15] = d & 255 - buf[16] = d >> 8 - buf[17] = d & 255 - return new Blob([buf], { type: 'image/jpeg' }) - } - return blob -} - -export function canvasToBlob(canvas, type, quality) { - return new Promise((resolve, reject) => { - canvas.toBlob( - (b) => - b - ? resolve(b) - : reject( - new Error('The image is too large for this browser. Choose a smaller resolution.') - ), - type, - quality - ) - }) -} - -/* ---------- PDF (single page, embedded JPEG) ---------- */ -export async function pdfFromCanvas(canvas, pageWidthPt, pageHeightPt) { - const jpeg = new Uint8Array(await (await canvasToBlob(canvas, 'image/jpeg', 0.95)).arrayBuffer()) - const enc = new TextEncoder() - const parts = [] - const offsets = [] - let length = 0 - const push = (chunk) => { - const bytes = typeof chunk === 'string' ? enc.encode(chunk) : chunk - parts.push(bytes) - length += bytes.length - } - const w = pageWidthPt.toFixed(2), - h = pageHeightPt.toFixed(2) - const content = `q ${w} 0 0 ${h} 0 0 cm /Im0 Do Q` - push('%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n') - const obj = (n, body) => { - offsets[n] = length - push(`${n} 0 obj\n${body}\nendobj\n`) - } - obj(1, '<< /Type /Catalog /Pages 2 0 R >>') - obj(2, '<< /Type /Pages /Kids [3 0 R] /Count 1 >>') - obj( - 3, - `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${w} ${h}] /Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>` - ) - offsets[4] = length - push( - `4 0 obj\n<< /Type /XObject /Subtype /Image /Width ${canvas.width} /Height ${canvas.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${jpeg.length} >>\nstream\n` - ) - push(jpeg) - push('\nendstream\nendobj\n') - obj(5, `<< /Length ${content.length} >>\nstream\n${content}\nendstream`) - const xref = length - let table = `xref\n0 6\n0000000000 65535 f \n` - for (let i = 1; i <= 5; i++) table += `${String(offsets[i]).padStart(10, '0')} 00000 n \n` - push(table + `trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`) - return new Blob(parts, { type: 'application/pdf' }) -} diff --git a/frontend/src/export/shading.ts b/frontend/src/export/shading.ts deleted file mode 100644 index cf47cc0..0000000 --- a/frontend/src/export/shading.ts +++ /dev/null @@ -1,128 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { UNGROUPED, effectiveGroupsFor, getUsedGroups } from '../network_state' -import { bubblePathsModel } from '../contours' -import { convexHull, hexToRgba, inflateHull } from '../hulls' -import { cy } from '../cy' -import { hullMarginModel } from './draw' - -/* ---------- group shading geometry, in output coordinates ---------- */ -export function hullShapes(toOut, k) { - if (!document.getElementById('showGroupHulls').checked) return [] - const style = document.getElementById('hullStyle').value - const opacity = parseFloat(document.getElementById('hullOpacity').value) || 0.25 - const margin = hullMarginModel() * k - const shapes = [] - if (style === 'bubble') { - const paths = bubblePathsModel(true) - paths.forEach((d, g) => { - if (d) - shapes.push({ - kind: 'path', - color: S.nodeColorMap[g] || '#888888', - d: transformPathD(d, toOut), - }) - }) - return shapes.map((s) => ({ ...s, style, opacity })) - } - getUsedGroups() - .filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - .forEach((g) => { - const nodes = cy - .nodes() - .filter((n) => !n.hasClass('hidden-group') && effectiveGroupsFor(n).includes(g)) - if (!nodes.length) return - const pts = nodes.map((n) => { - const p = toOut(n.position()) - return { x: p.x, y: p.y, r: (n.width() * k) / 2 } - }) - const color = S.nodeColorMap[g] || '#888888' - if (pts.length === 1) { - shapes.push({ - kind: 'circle', - color, - cx: pts[0].x, - cy: pts[0].y, - r: pts[0].r + (margin * 28) / 30, - }) - } else if (pts.length === 2) { - shapes.push({ - kind: 'capsule', - color, - a: pts[0], - b: pts[1], - pad: Math.max(pts[0].r, pts[1].r) + margin, - }) - } else { - const maxR = Math.max(...pts.map((p) => p.r)) - shapes.push({ kind: 'blob', color, hull: inflateHull(convexHull(pts), margin + maxR) }) - } - }) - return shapes.map((s) => ({ ...s, style, opacity })) -} - -export function capsulePath(a, b, pad) { - const dx = b.x - a.x, - dy = b.y - a.y - const d = Math.hypot(dx, dy) || 1 - const nx = (-dy / d) * pad, - ny = (dx / d) * pad - return ( - `M${a.x + nx},${a.y + ny}L${b.x + nx},${b.y + ny}` + - `A${pad},${pad} 0 0 0 ${b.x - nx},${b.y - ny}` + - `L${a.x - nx},${a.y - ny}A${pad},${pad} 0 0 0 ${a.x + nx},${a.y + ny}Z` - ) -} - -export function blobPath(points) { - const n = points.length - const mid = (p, q) => ({ x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }) - const m0 = mid(points[n - 1], points[0]) - let d = `M${m0.x.toFixed(2)},${m0.y.toFixed(2)}` - for (let i = 0; i < n; i++) { - const p = points[i], - m = mid(p, points[(i + 1) % n]) - d += `Q${p.x.toFixed(2)},${p.y.toFixed(2)} ${m.x.toFixed(2)},${m.y.toFixed(2)}` - } - return d + 'Z' -} - -// Maps every coordinate pair of an absolute path (M, L, Q, Z commands). -function transformPathD(d, map) { - return d.replace(/(-?\d*\.?\d+(?:e[-+]?\d+)?),(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi, (_, x, y) => { - const p = map({ x: parseFloat(x), y: parseFloat(y) }) - return `${Math.round(p.x * 100) / 100},${Math.round(p.y * 100) / 100}` - }) -} - -export function hullPathData(s) { - if (s.kind === 'path') return s.d - if (s.kind === 'circle') - return `M${s.cx - s.r},${s.cy}a${s.r},${s.r} 0 1 0 ${2 * s.r},0a${s.r},${s.r} 0 1 0 ${-2 * s.r},0Z` - if (s.kind === 'capsule') return capsulePath(s.a, s.b, s.pad) - return blobPath(s.hull) -} - -export function drawHullsOnCanvas(ctx, shapes, k) { - shapes.forEach((s) => { - const path = new Path2D(hullPathData(s)) - ctx.save() - ctx.fillStyle = hexToRgba(s.color, s.opacity) - if (s.style === 'bubble') { - ctx.fill(path, 'evenodd') - ctx.lineWidth = 1.6 * Math.max(1, k) - ctx.strokeStyle = hexToRgba(s.color, Math.min(1, s.opacity + 0.45)) - ctx.stroke(path) - } else if (s.style === 'fog') { - ctx.filter = `blur(${Math.max(1, ((16 * k) / Math.max(cy.zoom(), 1e-6)) * Math.max(0.15, Math.min(1, cy.zoom())))}px)` - ctx.fill(path) - } else { - ctx.fill(path) - ctx.lineWidth = 1.5 * Math.max(1, k) - ctx.strokeStyle = hexToRgba(s.color, Math.min(1, s.opacity + 0.35)) - ctx.stroke(path) - } - ctx.restore() - }) -} diff --git a/frontend/src/export/svg.ts b/frontend/src/export/svg.ts deleted file mode 100644 index e5ff507..0000000 --- a/frontend/src/export/svg.ts +++ /dev/null @@ -1,319 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { PIE_MAX_SLICES } from '../network_state' -import { cy } from '../cy' -import { exportFrame, exportPixelSize, hullMarginModel } from './draw' -import { hullPathData, hullShapes } from './shading' - -/* ---------- SVG ---------- */ -const svgEsc = (v) => - String(v).replace( - /[&<>"]/g, - (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] - ) - -const fmt = (v) => (Math.round(v * 100) / 100).toString() - -function nodeShapeSvg(n, fill, strokeAttrs) { - const shape = n.style('shape') - const p = n.position(), - w = n.width(), - h = n.height() - if (shape === 'ellipse') - return `` - if ( - shape === 'rectangle' || - shape === 'square' || - shape === 'barrel' || - /round-?rectangle/.test(shape) - ) { - const rx = /round/.test(shape) - ? Math.min(w / 4, h / 4, 8) - : shape === 'barrel' - ? Math.min(w, h) * 0.15 - : 0 - return `` - } - const def = cy.renderer().nodeShapes[shape] - if (def && def.points) { - const pts = [] - for (let i = 0; i < def.points.length; i += 2) { - pts.push(`${fmt(p.x + (def.points[i] * w) / 2)},${fmt(p.y + (def.points[i + 1] * h) / 2)}`) - } - return `` - } - return `` -} - -function pieSvg(n) { - if (n.style('shape') !== 'ellipse') return '' - const p = n.position() - const r = Math.min(n.width(), n.height()) / 2 - let start = -Math.PI / 2 - let out = '' - for (let i = 1; i <= PIE_MAX_SLICES; i++) { - const size = parseFloat(n.data('pieSize' + i)) || 0 - if (size <= 0) continue - const color = n.data('pieColor' + i) - if (size >= 99.999) { - out += `` - break - } - const end = start + (size / 100) * 2 * Math.PI - const large = end - start > Math.PI ? 1 : 0 - out += - `` - start = end - } - return out -} - -function edgePathSvg(e) { - const rs = e._private.rscratch || {} - const pts = rs.allpts - if (pts && pts.length >= 4 && pts.every(Number.isFinite)) { - if (['bezier', 'multibezier', 'self', 'compound'].includes(rs.edgeType) && pts.length >= 6) { - let d = `M${fmt(pts[0])},${fmt(pts[1])}` - for (let i = 2; i + 3 < pts.length; i += 4) - d += `Q${fmt(pts[i])},${fmt(pts[i + 1])} ${fmt(pts[i + 2])},${fmt(pts[i + 3])}` - return { d, mid: bezierMid(pts) } - } - let d = `M${fmt(pts[0])},${fmt(pts[1])}` - for (let i = 2; i + 1 < pts.length; i += 2) d += `L${fmt(pts[i])},${fmt(pts[i + 1])}` - const m = Math.floor((pts.length / 2 - 1) / 2) * 2 - return { - d, - mid: { - x: (pts[m] + pts[m + 2]) / 2, - y: (pts[m + 1] + pts[m + 3]) / 2, - angle: Math.atan2(pts[m + 3] - pts[m + 1], pts[m + 2] - pts[m]), - }, - } - } - const a = e.source().position(), - b = e.target().position() - return { - d: `M${fmt(a.x)},${fmt(a.y)}L${fmt(b.x)},${fmt(b.y)}`, - mid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, angle: Math.atan2(b.y - a.y, b.x - a.x) }, - } -} - -function bezierMid(pts) { - // middle quadratic segment, evaluated at t = 0.5 - const segs = Math.floor((pts.length - 2) / 4) - const s = Math.floor((segs - 1) / 2) * 4 - const x0 = pts[s], - y0 = pts[s + 1], - cx = pts[s + 2], - cy0 = pts[s + 3], - x1 = pts[s + 4], - y1 = pts[s + 5] - const t = segs % 2 ? 0.5 : 1 - const x = (1 - t) * (1 - t) * x0 + 2 * (1 - t) * t * cx + t * t * x1 - const y = (1 - t) * (1 - t) * y0 + 2 * (1 - t) * t * cy0 + t * t * y1 - const dx = 2 * (1 - t) * (cx - x0) + 2 * t * (x1 - cx), - dy = 2 * (1 - t) * (cy0 - y0) + 2 * t * (y1 - cy0) - return { x, y, angle: Math.atan2(dy, dx) } -} - -// Arrowhead at an edge's target, drawn like Cytoscape does: its outline in -// unit coordinates (tip at the origin, back toward negative y), scaled with -// the edge width and turned along the edge's last segment. -function arrowSvg(e, shape, width) { - const rs = e._private.rscratch || {} - const def = cy.renderer().arrowShapes && cy.renderer().arrowShapes[shape] - if (!def || !def.points || !Number.isFinite(rs.arrowEndX)) return '' - const pts = rs.allpts || [] - const endX = Number.isFinite(rs.endX) ? rs.endX : pts[pts.length - 2] - const endY = Number.isFinite(rs.endY) ? rs.endY : pts[pts.length - 1] - let dx = rs.arrowEndX - endX, - dy = rs.arrowEndY - endY - let len = Math.hypot(dx, dy) - if (len < 1e-6) { - // fall back to the direction of the last path segment - const px = pts[pts.length - 4], - py = pts[pts.length - 3] - dx = rs.arrowEndX - px - dy = rs.arrowEndY - py - len = Math.hypot(dx, dy) || 1 - } - const ux = dx / len, - uy = dy / len // forward along the edge - const size = Math.max(Math.pow(width * 13.37, 0.9), 29) * (e.pstyle('arrow-scale').value || 1) - const poly = [] - for (let i = 0; i < def.points.length; i += 2) { - const px = def.points[i] * size, - py = def.points[i + 1] * size - poly.push(`${fmt(rs.arrowEndX - uy * px + ux * py)},${fmt(rs.arrowEndY + ux * px + uy * py)}`) - } - return `` -} - -function textSvg(text, x, y, attrs, fontPx, color, halo, haloWidth, rotate) { - const upright = rotate - ? rotate > Math.PI / 2 || rotate < -Math.PI / 2 - ? rotate + Math.PI - : rotate - : 0 - const transform = upright - ? ` transform="rotate(${fmt((upright * 180) / Math.PI)} ${fmt(x)} ${fmt(y)})"` - : '' - const haloAttr = - haloWidth > 0 - ? ` stroke="${halo}" stroke-width="${fmt(haloWidth * 2)}" stroke-linejoin="round" paint-order="stroke"` - : '' - return `${svgEsc(text)}` -} - -export function buildSvg(opts) { - const frame = exportFrame(opts.area) - if (!frame) throw new Error('Nothing is shown to export.') - const { W, H } = exportPixelSize(frame, opts.scale) - const out = [] - out.push(``) - out.push( - `` - ) - out.push(`${svgEsc(opts.title || 'NORMA network')}`) - if (opts.bg) - out.push( - `` - ) - - if (opts.hulls) { - const shapes = hullShapes((p) => p, 1) - if (shapes.length) { - const fog = shapes[0].style === 'fog' - if (fog) - out.push( - `` - ) - out.push('') - shapes.forEach((s) => { - const stroke = fog - ? '' - : ` stroke="${s.color}" stroke-opacity="${fmt(Math.min(1, s.opacity + 0.35))}" stroke-width="${fmt(1.5 / Math.max(cy.zoom(), 0.01))}"` - const rule = s.kind === 'path' ? ' fill-rule="evenodd"' : '' - out.push( - `` - ) - }) - out.push('') - } - } - - const visible = (ele) => ele.visible() && ele.effectiveOpacity() > 0 - const inFrame = (bb) => - !( - bb.x2 < frame.x1 || - bb.y2 < frame.y1 || - bb.x1 > frame.x1 + frame.w || - bb.y1 > frame.y1 + frame.h - ) - - out.push('') - const edgeLabels = [] - cy.edges().forEach((e) => { - if (!visible(e) || !inFrame(e.boundingBox({ includeLabels: false }))) return - const { d, mid } = edgePathSvg(e) - const width = e.pstyle('width').pfValue - const arrowShape = e.style('target-arrow-shape') - const hasArrow = arrowShape && arrowShape !== 'none' - out.push( - `${svgEsc(`${e.data('source')} ${hasArrow ? '→' : '–'} ${e.data('target')} (${e.data('type')})`)}` - ) - if (hasArrow) { - const arrow = arrowSvg(e, arrowShape, width) - if (arrow) out.push(arrow) - } - const label = e.style('label') - if (label) { - const rs = e._private.rscratch || {} - const pos = Number.isFinite(rs.labelX) ? { x: rs.labelX, y: rs.labelY } : mid - const rot = - e.style('text-rotation') === 'autorotate' - ? Number.isFinite(rs.labelAutoAngle) - ? rs.labelAutoAngle - : mid.angle - : 0 - edgeLabels.push( - textSvg( - label, - pos.x, - pos.y, - ' text-anchor="middle" dominant-baseline="central"', - e.pstyle('font-size').pfValue, - e.style('color'), - e.style('text-background-color'), - 2, - rot - ) - ) - } - }) - out.push('') - - out.push('') - const nodeLabels = [] - cy.nodes().forEach((n) => { - if (!visible(n) || !inFrame(n.boundingBox({ includeLabels: false }))) return - const bw = n.pstyle('border-width').pfValue - const strokeAttrs = - bw > 0 ? ` stroke="${n.style('border-color')}" stroke-width="${fmt(bw)}"` : '' - const opacity = n.effectiveOpacity() - out.push( - `${svgEsc(n.id())}` - ) - out.push(nodeShapeSvg(n, n.style('background-color'), '')) - out.push(pieSvg(n)) - if (bw > 0) out.push(nodeShapeSvg(n, 'none', strokeAttrs)) - out.push('') - const label = n.style('label') - if (label) { - const p = n.position(), - w = n.width(), - h = n.height() - const valign = n.style('text-valign'), - halign = n.style('text-halign') - const mx = n.pstyle('text-margin-x').pfValue || 0, - my = n.pstyle('text-margin-y').pfValue || 0 - let x = p.x + mx, - y = p.y + my, - anchor = 'middle', - baseline = 'central' - if (valign === 'top') { - y = p.y - h / 2 + my - baseline = 'text-after-edge' - } else if (valign === 'bottom') { - y = p.y + h / 2 + my - baseline = 'text-before-edge' - } - if (halign === 'left') { - x = p.x - w / 2 + mx - anchor = 'end' - } else if (halign === 'right') { - x = p.x + w / 2 + mx - anchor = 'start' - } - nodeLabels.push( - textSvg( - label, - x, - y, - ` text-anchor="${anchor}" dominant-baseline="${baseline}"`, - n.pstyle('font-size').pfValue, - n.style('color'), - n.style('text-outline-color'), - n.pstyle('text-outline-width').pfValue, - 0 - ) - ) - } - }) - out.push('') - if (edgeLabels.length) out.push('', ...edgeLabels, '') - if (nodeLabels.length) out.push('', ...nodeLabels, '') - out.push('') - return new Blob([out.join('\n')], { type: 'image/svg+xml' }) -} diff --git a/frontend/src/export_norma.ts b/frontend/src/export_norma.ts deleted file mode 100644 index 6cafb10..0000000 --- a/frontend/src/export_norma.ts +++ /dev/null @@ -1,257 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { UNGROUPED, getUsedGroups, groupLabel } from './network_state' -import { cy } from './cy' -import { downloadText, listSample, normalizeCssColor, plural, setStatus } from './layouts/controls' -import { edgeIsDirected } from './export/dialog' -import { exportEdges, exportNodes, exportShownOnly } from './metrics' -import { getUsedTypes } from './hulls' - -/* ---------- NORMA export ---------- */ -// NORMA can't read tabs or line breaks inside names, and node names can't -// contain commas (the annotation separator). Group names may keep commas. -function normaSafeName(name, renamed, isNode = true) { - let safe = String(name).replace(/[\t\r\n]+/g, ' ') - if (isNode) safe = safe.replace(/,/g, ';') - if (safe !== String(name)) renamed.add(String(name)) - return safe -} - -function renamedNote(renamed) { - return renamed.size - ? [ - { - level: 'warn', - text: `Changed ${plural(renamed.size, 'name')} that NORMA can't read (commas in node names became semicolons, tabs became spaces): ${listSample([...renamed])}.`, - }, - ] - : [] -} - -function connectedNodeIds() { - const ids = new Set() - exportEdges().forEach((e) => { - if (e.data('source') !== e.data('target')) { - ids.add(e.data('source')) - ids.add(e.data('target')) - } - }) - return ids -} - -function requireLoadedNetwork() { - if (exportEdges().length) return true - setStatus('saveStatus', [ - { - level: 'error', - text: cy.edges().length - ? 'No edges are shown: tick at least one group and channel, or untick "Only ticked groups and channels".' - : 'Load a network with at least one edge before saving NORMA files.', - }, - ]) - return false -} - -export function exportNormaNetwork() { - if (!requireLoadedNetwork()) return - const renamed = new Set() - const types = getUsedTypes() - const keepTypes = document.getElementById('normaKeepTypes').checked && types.length > 1 - const rowsByKey = new Map() - let selfLoops = 0, - merged = 0 - exportEdges().forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s === t) { - selfLoops++ - return - } - const type = e.data('type') - const directed = edgeIsDirected(e) - const pair = directed ? 'D\t' + s + '\t' + t : 'U\t' + (s < t ? s + '\t' + t : t + '\t' + s) - const key = pair + (keepTypes ? '\t' + type : '') - const w = e.data('weight') - const row = rowsByKey.get(key) - if (row) { - merged++ - if (typeof w === 'number') row.weight = row.weight === undefined ? w : Math.max(row.weight, w) - } else { - rowsByKey.set(key, { - source: s, - target: t, - type, - directed, - weight: typeof w === 'number' ? w : undefined, - }) - } - }) - const rows = [...rowsByKey.values()] - const withDirection = rows.some((r) => r.directed) - const weighted = rows.some((r) => r.weight !== undefined) - const unweighted = weighted ? rows.filter((r) => r.weight === undefined).length : 0 - const header = ['Source', 'Target'] - if (weighted) header.push('Weight') - if (keepTypes) header.push('Type') - if (withDirection) header.push('Direction') - const lines = [header.join('\t')] - rows.forEach((r) => { - const cols = [normaSafeName(r.source, renamed), normaSafeName(r.target, renamed)] - if (weighted) cols.push(String(r.weight === undefined ? 1 : r.weight)) - if (keepTypes) cols.push(normaSafeName(r.type, renamed, false)) - if (withDirection) cols.push(r.directed ? 'directed' : 'undirected') - lines.push(cols.join('\t')) - }) - downloadText('norma-network.txt', lines.join('\n') + '\n') - - const notes = [ - { - level: 'ok', - text: `Saved norma-network.txt with ${plural(rows.length, 'row')}${weighted ? ', weighted' : ''}${keepTypes ? `, ${plural(types.length, 'type')} in a Type column` : ''}${withDirection ? ', with a Direction column' : ''}.`, - }, - ] - if (merged) - notes.push({ - level: 'ok', - text: keepTypes - ? `Merged ${plural(merged, 'repeated edge')} with the same pair and channel.` - : `Merged ${plural(merged, 'parallel edge')} into single connections${weighted ? ', keeping the highest weight' : ''}. Tick "Keep channels as a Type column" to keep them apart.`, - }) - if (document.getElementById('normaKeepTypes').checked && types.length <= 1) - notes.push({ - level: 'ok', - text: 'The view has a single channel, so no Type column was needed.', - }) - if (keepTypes || withDirection) - notes.push({ - level: 'warn', - text: `The ${[keepTypes && 'Type', withDirection && 'Direction'].filter(Boolean).join(' and ')} column${keepTypes && withDirection ? 's are' : ' is'} new in NORMA 3.0; earlier versions of NORMA read only Source, Target and Weight.`, - }) - if (unweighted) - notes.push({ - level: 'warn', - text: `${plural(unweighted, 'row')} had no weight and got weight 1.`, - }) - if (selfLoops) notes.push({ level: 'ok', text: `Left out ${plural(selfLoops, 'self-loop')}.` }) - const isolated = exportNodes().length - connectedNodeIds().size - if (isolated) - notes.push({ - level: 'warn', - text: `${plural(isolated, 'node has', 'nodes have')} no edges and can't appear in a network file.`, - }) - setStatus('saveStatus', [...notes, ...renamedNote(renamed)]) -} - -export function exportNormaAnnotation() { - if (!requireLoadedNetwork()) return - const groups = getUsedGroups().filter( - (g) => g !== UNGROUPED && (!exportShownOnly() || S.activeGroups.has(g)) - ) - if (!groups.length) { - setStatus('saveStatus', [ - { - level: 'error', - text: 'This view has no ticked groups, so there is no annotation to save.', - }, - ]) - return - } - const renamed = new Set() - const connected = connectedNodeIds() - // A group's display label is its NORMA name, unless two groups share a - // label; then the group key keeps them apart. - const labelCounts = {} - groups.forEach((g) => { - const l = groupLabel(g) - labelCounts[l] = (labelCounts[l] || 0) + 1 - }) - const membersOf = {} - exportNodes().forEach((n) => - (n.data('groups') || []).forEach((g) => (membersOf[g] = membersOf[g] || []).push(n.id())) - ) - const skippedGroups = [] - const droppedNodes = new Set() - const lines = [] - groups.forEach((g) => { - const members = membersOf[g] || [] - const present = members.filter((id) => connected.has(id)) - members.forEach((id) => { - if (!connected.has(id)) droppedNodes.add(id) - }) - const name = labelCounts[groupLabel(g)] > 1 ? g : groupLabel(g) - if (!present.length) { - skippedGroups.push(name) - return - } - lines.push( - `${normaSafeName(name, renamed, false)}\t${present.map((id) => normaSafeName(id, renamed)).join(',')}` - ) - }) - if (!lines.length) { - setStatus('saveStatus', [ - { - level: 'error', - text: 'None of the grouped nodes have edges, so the annotation would be empty.', - }, - ]) - return - } - downloadText('norma-annotation.txt', lines.join('\n') + '\n') - const notes = [ - { level: 'ok', text: `Saved norma-annotation.txt with ${plural(lines.length, 'group')}.` }, - ] - if (droppedNodes.size) - notes.push({ - level: 'warn', - text: `Left out ${plural(droppedNodes.size, 'node')} without edges, since NORMA needs every annotated node in the network file: ${listSample([...droppedNodes])}.`, - }) - if (skippedGroups.length) - notes.push({ - level: 'warn', - text: `Left out ${plural(skippedGroups.length, 'group')} whose nodes have no edges: ${listSample(skippedGroups)}.`, - }) - setStatus('saveStatus', [...notes, ...renamedNote(renamed)]) -} - -export function exportNormaColors() { - if (!requireLoadedNetwork()) return - const renamed = new Set() - const connected = connectedNodeIds() - const nodes = exportNodes().filter((n) => connected.has(n.id())) - const useOwn = nodes.some((n) => n.data('nodeColor')) - const lines = [] - nodes.forEach((n) => { - let color - if (useOwn) { - color = n.data('nodeColor') - } else { - const g = (n.data('groups') || []).find((x) => x !== UNGROUPED) - color = g ? S.nodeColorMap[g] : null - } - if (!color) return - // canvas-normalized rgba() values aren't NORMA colors; keep hex only - const hex = String(color).startsWith('#') ? color : normalizeCssColor(color) || '' - if (!hex.startsWith('#')) return - lines.push(`${normaSafeName(n.id(), renamed)}\t${hex}`) - }) - if (!lines.length) { - setStatus('saveStatus', [ - { - level: 'error', - text: 'No node has a color to save. Load an expression file, or give nodes groups so their group colors can be used.', - }, - ]) - return - } - downloadText('norma-expression.txt', lines.join('\n') + '\n') - setStatus('saveStatus', [ - { - level: 'ok', - text: useOwn - ? `Saved norma-expression.txt with the expression colors of ${plural(lines.length, 'node')}.` - : `Saved norma-expression.txt using each node's first group color (${plural(lines.length, 'node')}), since no expression colors are loaded.`, - }, - ...renamedNote(renamed), - ]) -} diff --git a/frontend/src/group_network.ts b/frontend/src/group_network.ts deleted file mode 100644 index 5814b4b..0000000 --- a/frontend/src/group_network.ts +++ /dev/null @@ -1,101 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { UNGROUPED, effectiveGroupsFor, getUsedGroups, groupLabel } from './network_state' -import { activeView, openInNewView } from './profiler' -import { applyGroupColorOverrides } from './arena3d' -import { cy } from './cy' -import { loadData } from './hulls' -import { plural, setStatus } from './layouts/controls' -import { shownEdges, shownNodes } from './metrics' -import { switchTab } from './wiring' - -/* ---------- group network ---------- */ -export function openGroupNetwork() { - if (!cy.nodes().length) return - const order = getUsedGroups().filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - if (order.length < 2) { - setStatus('gaStatus', [ - { level: 'error', text: 'The group network needs at least two ticked groups.' }, - ]) - return - } - const nodes = shownNodes() - const groupsOf = new Map( - nodes.map((n) => [n.id(), effectiveGroupsFor(n).filter((g) => g !== UNGROUPED)]) - ) - const size = new Map(order.map((g) => [g, 0])) - groupsOf.forEach((gs) => gs.forEach((g) => size.set(g, (size.get(g) || 0) + 1))) - const inside = new Map(order.map((g) => [g, 0])) - const between = new Map() - const seenPairs = new Set() - shownEdges(true).forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s === t || !groupsOf.has(s) || !groupsOf.has(t)) return - const pk = s < t ? s + '\t' + t : t + '\t' + s - if (seenPairs.has(pk)) return // parallel channels count once - seenPairs.add(pk) - const gs = groupsOf.get(s), - gt = groupsOf.get(t) - gs.forEach((a) => - gt.forEach((b) => { - if (a === b) { - inside.set(a, inside.get(a) + 1) - return - } - const key = a < b ? a + '\u0000' + b : b + '\u0000' + a - between.set(key, (between.get(key) || 0) + 1) - }) - ) - }) - const maxSize = Math.max(...size.values()) - const data = { - nodes: order - .filter((g) => size.get(g)) - .map((g) => ({ - id: groupLabel(g), - groups: [groupLabel(g)], - size: Math.round(30 + 70 * Math.sqrt(size.get(g) / maxSize)), - members: size.get(g), - 'edges inside': inside.get(g), - })), - edges: [...between].map(([key, w]) => { - const [a, b] = key.split('\u0000') - return { - source: groupLabel(a), - target: groupLabel(b), - weight: w, - type: 'connections', - connections: w, - } - }), - groupOrder: order.filter((g) => size.get(g)).map((g) => groupLabel(g)), - nodeColors: Object.fromEntries( - order.map((g) => [groupLabel(g), S.nodeColorMap[g] || '#888888']) - ), - edgeColors: { connections: '#64748b' }, - config: { - layoutSelect: 'fr', - edgeWidthMode: 'weight', - edgeWidthMin: '1', - edgeWidthMax: '10', - sizeMetric: 'fixed', - showEdgeLabels: true, - edgeLabelContent: 'attr:connections', - edgeLabelSize: '11', - }, - } - const v = activeView() - openInNewView(`${v ? v.name : 'View'}: group network`, () => { - loadData(data) - applyGroupColorOverrides(data.nodeColors) - }) - switchTab('network') - setStatus('gaStatus', [ - { - level: 'ok', - text: `Opened the group network: ${plural(data.nodes.length, 'group')}, ${plural(data.edges.length, 'connection')} between groups. Node size follows group size; edge width follows the number of connections.`, - }, - ]) -} diff --git a/frontend/src/hulls.test.ts b/frontend/src/hulls.test.ts deleted file mode 100644 index 1f9d904..0000000 --- a/frontend/src/hulls.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect, test } from 'vitest' -import { convexHull, hexToRgba, inflateHull } from './hulls' - -type Pt = { x: number; y: number } - -test('convexHull drops interior points and keeps counter-clockwise corners', () => { - const pts = [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - { x: 2, y: 2 }, - { x: 0, y: 2 }, - { x: 1, y: 1 }, - { x: 1, y: 0.5 }, - ] - const hull = convexHull(pts) - expect(hull).toHaveLength(4) - expect(hull.map((p: Pt) => `${p.x},${p.y}`)).toEqual(['0,0', '2,0', '2,2', '0,2']) -}) - -test('convexHull leaves fewer than three points alone', () => { - expect(convexHull([{ x: 1, y: 1 }])).toEqual([{ x: 1, y: 1 }]) - expect(convexHull([])).toEqual([]) -}) - -test('inflateHull pushes every vertex away from the centroid by the padding', () => { - const square = [ - { x: -1, y: -1 }, - { x: 1, y: -1 }, - { x: 1, y: 1 }, - { x: -1, y: 1 }, - ] - const big = inflateHull(square, 2) - big.forEach((p: Pt, i: number) => { - const before = Math.hypot(square[i].x, square[i].y) - expect(Math.hypot(p.x, p.y)).toBeCloseTo(before + 2) - }) -}) - -test('hexToRgba', () => { - expect(hexToRgba('#ff8000', 0.5)).toBe('rgba(255,128,0,0.5)') - expect(hexToRgba('#zz', 1)).toBe('rgba(0,0,0,1)') -}) diff --git a/frontend/src/hulls.ts b/frontend/src/hulls.ts deleted file mode 100644 index 04fb50d..0000000 --- a/frontend/src/hulls.ts +++ /dev/null @@ -1,787 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { - EDGE_PALETTES, - EDGE_TYPES, - NODE_PALETTES, - ORIGINAL_EDGE_TYPES, - colorAtIndex, -} from './palette' -import { - LARGE_NETWORK_NODES, - applyNodeSizing, - cancelFrJobs, - invalidateFullMetrics, - updateStats, -} from './metrics' -import { - MAX_NETWORK_NODES, - RESERVED_EDGE_KEYS, - RESERVED_NODE_KEYS, - UNGROUPED, - UNGROUPED_LABEL, - buildAttrSchema, - cleanValues, - collectAttrs, - colorForGroup, - computeNodeVisualFields, - effectiveGroupsFor, - escapeHtml, - getUsedGroups, - groupDescription, - groupLabel, - nextAutoEdgeColor, - refreshNodeVisual, - renderAttrSchema, - sanitizeColor, - setGroupOrder, -} from './network_state' -import { S } from './state' -import { - SHAPE_BY_KEY, - applyEdgeCurveStyle, - applyEdgeOpacity, - applyEdgeWidth, - closeShapePopover, - hideEdgePopup, - hideInfo, - invalidateViewBox, - openShapePopover, - scheduleBundling, - shapePopoverGroup, - shapeSvg, - showGroupInfo, - updateLabelStyle, -} from './profiler' -import { applyEdgeDirection } from './export/dialog' -import { applyEdgeMerge, applyTypeVisibility } from './parallel_edges' -import { applyTheme } from './themes' -import { applyValueColors } from './clustering/mapping' -import { bumpDataVersion } from './demo_downloads' -import { currentTab, switchTab } from './wiring' -import { cy } from './cy' -import { drawBubbleSets } from './contours' -import { layoutMode, runLayout, runStrategyLayout } from './layouts/run' -import { plural, updateRefreshState, updateStrategyUI } from './layouts/controls' -import { reset3dForNewData } from './view3d/tab' -import { takeNextLoadLayout } from './examples' -import { toast } from './enrichment' -import { updateValueScalePreview } from './clustering/wiring' - -/* ---------- group highlighting: convex hulls / fog clouds ---------- */ -const hullCanvas = document.getElementById('groupHullCanvas') - -const hullCtx = hullCanvas.getContext('2d') - -// The shading canvas must match Cytoscape's drawing area and the current -// pixel ratio. The pixel ratio can change without a 'resize' event (a window -// dragged between a Retina and a standard display on macOS), and the canvas -// area can change size without one too, so drawGroupHulls() re-checks both. -export let hullCanvasCss = { w: 0, h: 0, dpr: 0 } - -export function resizeHullCanvas() { - const rect = document.getElementById('canvas').getBoundingClientRect() - const w = rect.width || cy.width(), - h = rect.height || cy.height() - const dpr = window.devicePixelRatio || 1 - hullCanvas.width = Math.max(1, Math.round(w * dpr)) - hullCanvas.height = Math.max(1, Math.round(h * dpr)) - hullCanvas.style.width = w + 'px' - hullCanvas.style.height = h + 'px' - hullCanvasCss = { w, h, dpr } - hullCtx.setTransform(dpr, 0, 0, dpr, 0, 0) -} - -function hullCanvasStale() { - return ( - hullCanvasCss.dpr !== (window.devicePixelRatio || 1) || - Math.abs(hullCanvasCss.w - cy.width()) > 0.5 || - Math.abs(hullCanvasCss.h - cy.height()) > 0.5 - ) -} - -// Andrew's monotone chain convex hull. -export function convexHull(points) { - const pts = points.slice().sort((a, b) => a.x - b.x || a.y - b.y) - const n = pts.length - if (n < 3) return pts - const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) - const lower = [] - for (const p of pts) { - while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) - lower.pop() - lower.push(p) - } - const upper = [] - for (let i = n - 1; i >= 0; i--) { - const p = pts[i] - while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) - upper.pop() - upper.push(p) - } - upper.pop() - lower.pop() - return lower.concat(upper) -} - -// Pushes each hull vertex outward from the hull's centroid so the shape -// clears the nodes themselves rather than hugging their centers. -export function inflateHull(hull, padding) { - const cx = hull.reduce((s, p) => s + p.x, 0) / hull.length - const cy0 = hull.reduce((s, p) => s + p.y, 0) / hull.length - return hull.map((p) => { - const dx = p.x - cx, - dy = p.y - cy0 - const len = Math.sqrt(dx * dx + dy * dy) || 1 - return { x: p.x + (dx / len) * padding, y: p.y + (dy / len) * padding } - }) -} - -export function hexToRgba(hex, alpha) { - const r = parseInt(hex.slice(1, 3), 16) || 0 - const g = parseInt(hex.slice(3, 5), 16) || 0 - const b = parseInt(hex.slice(5, 7), 16) || 0 - return `rgba(${r},${g},${b},${alpha})` -} - -function paintHullPath(ctx, color, opacity, style) { - ctx.fillStyle = hexToRgba(color, opacity) - if (style === 'fog') { - ctx.filter = 'blur(16px)' - ctx.fill() - ctx.filter = 'none' - } else { - ctx.fill() - ctx.lineWidth = 1.5 - ctx.strokeStyle = hexToRgba(color, Math.min(1, opacity + 0.35)) - ctx.stroke() - } -} - -// Shading margin in screen pixels: full size from 100% zoom up, shrinking -// with the network when zoomed out so neighbouring groups stay apart. -let hullMarginScale = 1 - -export function drawGroupHulls() { - hullMarginScale = Math.max(0.15, Math.min(1, cy.zoom())) - if (!hullCtx) return - if (hullCanvasStale()) resizeHullCanvas() - hullCtx.setTransform(hullCanvasCss.dpr, 0, 0, hullCanvasCss.dpr, 0, 0) - hullCtx.clearRect(0, 0, hullCanvasCss.w, hullCanvasCss.h) - if (!document.getElementById('showGroupHulls').checked) return - - const style = document.getElementById('hullStyle').value - const opacity = parseFloat(document.getElementById('hullOpacity').value) || 0.25 - if (style === 'bubble') { - drawBubbleSets(hullCtx, opacity) - return - } - const groups = getUsedGroups().filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - - groups.forEach((g) => { - const nodes = cy - .nodes() - .filter((n) => !n.hasClass('hidden-group') && effectiveGroupsFor(n).includes(g)) - if (!nodes.length) return - const color = S.nodeColorMap[g] || '#888888' - const points = nodes.map((n) => { - const rp = n.renderedPosition() - return { x: rp.x, y: rp.y, r: (n.renderedWidth() || n.width() * cy.zoom()) / 2 } - }) - - hullCtx.save() - if (points.length === 1) { - const p = points[0] - hullCtx.beginPath() - hullCtx.arc(p.x, p.y, p.r + 28 * hullMarginScale, 0, Math.PI * 2) - hullCtx.closePath() - paintHullPath(hullCtx, color, opacity, style) - } else if (points.length === 2) { - const [a, b] = points - const dx = b.x - a.x, - dy = b.y - a.y - const dist = Math.sqrt(dx * dx + dy * dy) || 1 - const nx = -dy / dist, - ny = dx / dist - const pad = Math.max(a.r, b.r) + 30 * hullMarginScale - hullCtx.beginPath() - hullCtx.moveTo(a.x + nx * pad, a.y + ny * pad) - hullCtx.lineTo(b.x + nx * pad, b.y + ny * pad) - // end caps bulge outward: around b away from a, around a away from b - hullCtx.arc(b.x, b.y, pad, Math.atan2(ny, nx), Math.atan2(-ny, -nx), true) - hullCtx.lineTo(a.x - nx * pad, a.y - ny * pad) - hullCtx.arc(a.x, a.y, pad, Math.atan2(-ny, -nx), Math.atan2(ny, nx), true) - hullCtx.closePath() - paintHullPath(hullCtx, color, opacity, style) - } else { - const maxR = Math.max(...points.map((p) => p.r || 0)) - const hull = inflateHull(convexHull(points), 30 * hullMarginScale + maxR) - // Both styles must actually contain every member node, so trace the - // true polygon through the inflated hull points rather than rounding - // the corners: a rounded corner falls short of the padded vertex by - // an amount that grows with its distance from its neighbors, so a - // far-dragged node ends up outside the shape (too far for "fog"'s - // blur in paintHullPath to hide). "fog" gets its soft look from that - // blur filter, not from rounding this path. - hullCtx.beginPath() - hullCtx.moveTo(hull[0].x, hull[0].y) - for (let i = 1; i < hull.length; i++) hullCtx.lineTo(hull[i].x, hull[i].y) - hullCtx.closePath() - paintHullPath(hullCtx, color, opacity, style) - } - hullCtx.restore() - }) -} - -export function applyGroupVisibility() { - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - invalidateViewBox() - invalidateFullMetrics() - if (document.getElementById('sizeMetric').value !== 'fixed') applyNodeSizing() - updateStats() - if (typeof scheduleBundling === 'function') scheduleBundling() -} - -function groupFilterText() { - const el = document.getElementById('groupFilter') - return el ? el.value.trim().toLowerCase() : '' -} - -// Groups whose label or key contains the filter text (all groups if empty). -// A–Z with numbers in order (Group-2 before Group-10), ignoring case and accents -const NAME_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) - -export const byName = (a, b) => NAME_COLLATOR.compare(String(a), String(b)) - -export function sortedByName(list, nameOf = (x) => x) { - return [...list].sort((a, b) => byName(nameOf(a), nameOf(b))) -} - -function groupSortMode() { - const el = document.getElementById('groupSortSelect') - return el ? el.value : 'az' -} - -// nodes per group, as the Node groups list counts them -function groupNodeCounts() { - const counts = {} - cy.nodes().forEach((n) => - (n.data('groups') || []).forEach((g) => { - counts[g] = (counts[g] || 0) + 1 - }) - ) - return counts -} - -// groups in the order lists and legends show them (colors keep their own order): -// A–Z, by number of nodes (either way, ties A–Z), or as in the file -export function displayGroups(groups) { - const mode = groupSortMode() - if (mode === 'file') return groups - let rest = sortedByName( - groups.filter((g) => g !== UNGROUPED), - groupLabel - ) - if (mode === 'size-desc' || mode === 'size-asc') { - const counts = groupNodeCounts() - const dir = mode === 'size-desc' ? -1 : 1 - rest = rest.sort( - (a, b) => dir * ((counts[a] || 0) - (counts[b] || 0)) || byName(groupLabel(a), groupLabel(b)) - ) - } - return groups.includes(UNGROUPED) ? rest.concat([UNGROUPED]) : rest -} - -export function filteredGroups() { - const q = groupFilterText() - const groups = displayGroups(getUsedGroups()) - return q - ? groups.filter((g) => groupLabel(g).toLowerCase().includes(q) || g.toLowerCase().includes(q)) - : groups -} - -function updateGroupButtons() { - const filtered = !!groupFilterText() - document.getElementById('btnGroupsAll').textContent = filtered - ? 'Activate matches' - : 'Activate all' - document.getElementById('btnGroupsNone').textContent = filtered - ? 'Deactivate matches' - : 'Deactivate all' -} - -export function buildGroupLegend() { - const el = document.getElementById('nodeGroupLegend') - if (!el) return - el.innerHTML = '' - const allGroups = getUsedGroups() - updateGroupButtons() - if (!allGroups.length) { - el.innerHTML = - '

    Load a network to see its groups.

    ' - return - } - const groups = filteredGroups() - if (!groups.length) { - el.innerHTML = - '

    No group matches the filter.

    ' - return - } - const counts = {} - cy.nodes().forEach((n) => - (n.data('groups') || []).forEach((g) => { - counts[g] = (counts[g] || 0) + 1 - }) - ) - groups.forEach((g) => { - const count = counts[g] || 0 - const label = groupLabel(g) - const description = groupDescription(g) - const row = document.createElement('label') - row.className = 'toggle-row' - if (description) row.title = description - row.innerHTML = ` - - - - ${escapeHtml(label)} - ${count} - - ` - row.querySelector('.ginfo').addEventListener('click', (ev) => { - ev.preventDefault() - ev.stopPropagation() - showGroupInfo(g) - }) - row.querySelector('.gshape').addEventListener('click', (ev) => { - ev.preventDefault() - ev.stopPropagation() - if (shapePopoverGroup === g) closeShapePopover(true) - else openShapePopover(g, ev.currentTarget) - }) - row.querySelector('input[type=checkbox]').addEventListener('change', (ev) => { - if (ev.target.checked) S.activeGroups.add(g) - else S.activeGroups.delete(g) - applyGroupVisibility() - }) - const picker = row.querySelector('.swatch-picker') - picker.addEventListener('click', (ev) => ev.stopPropagation()) - picker.addEventListener('input', (ev) => { - ev.stopPropagation() - S.nodeColorMap[g] = ev.target.value - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - }) - el.appendChild(row) - }) -} - -/* ============================================================ - CONFIG MODULE - Every user-adjustable visual/style control is listed once here. - getCurrentConfig() / applyConfig() are generated from this single - table, so adding a new control to the "settings" file format is a - one-line change here rather than hand-wiring an export and an - import path separately. This is what backs the "Settings" section - (export/import a standalone settings JSON) and the optional - top-level "config" object a network-data file may also carry. - ============================================================ */ -export const CONFIG_FIELDS = [ - { id: 'themeSelect', prop: 'value' }, - { id: 'nodePaletteSelect', prop: 'value' }, - { id: 'nodeLabelSize', prop: 'value' }, - { id: 'nodeLabelColorMode', prop: 'value' }, - { id: 'nodeLabelColor', prop: 'value' }, - { id: 'edgeLabelColorMode', prop: 'value' }, - { id: 'edgeLabelColor', prop: 'value' }, - { id: 'labelScaleWithNode', prop: 'checked' }, - { id: 'edgeLabelContent', prop: 'value' }, - { id: 'edgeLabelOrientation', prop: 'value' }, - { id: 'edgeLabelSize', prop: 'value' }, - { id: 'labelMinScreenSize', prop: 'value' }, - { id: 'edgeOpacity', prop: 'value' }, - { id: 'bundleStrength', prop: 'value' }, - { id: 'valueColumn', prop: 'value' }, - { id: 'valueTransform', prop: 'value' }, - { id: 'valueScale', prop: 'value' }, - { id: 'valueCenter', prop: 'value' }, - { id: 'valueRange', prop: 'value' }, - { id: 'valueMin', prop: 'value' }, - { id: 'valueMax', prop: 'value' }, - { id: 'valueMissing', prop: 'value' }, - { id: 'valueSameRange', prop: 'checked' }, - { id: 'groupSortSelect', prop: 'value' }, - { id: 'legendShow', prop: 'checked' }, - { id: 'legendScale', prop: 'checked' }, - { id: 'legendGroups', prop: 'checked' }, - { id: 'legendShapes', prop: 'checked' }, - { id: 'legendChannels', prop: 'checked' }, - { id: 'legendTitle', prop: 'value' }, - { id: 'layout3d', prop: 'value' }, - { id: 'layout3dChannels', prop: 'checked' }, - { id: 'layer3dSpacing', prop: 'value' }, - { id: 'persp3d', prop: 'value' }, - { id: 'style3d', prop: 'value' }, - { id: 'fog3d', prop: 'value' }, - { id: 'grid3d', prop: 'checked' }, - { id: 'autoRotate3d', prop: 'checked' }, - { id: 'autoSpeed3d', prop: 'value' }, - { id: 'edgeDirection', prop: 'value' }, - { id: 'arrowShape', prop: 'value' }, - { id: 'arrowScale', prop: 'value' }, - { id: 'exportShownOnly', prop: 'checked' }, - { id: 'nodeFillSelect', prop: 'value' }, - { id: 'edgePaletteSelect', prop: 'value' }, - { id: 'showNodeLabels', prop: 'checked' }, - { id: 'showEdgeLabels', prop: 'checked' }, - { id: 'labelPosition', prop: 'value' }, - { id: 'edgeCurveStyle', prop: 'value' }, - { id: 'edgeMergeMode', prop: 'value' }, - { id: 'edgeCurvature', prop: 'value' }, - { id: 'edgeWidthMode', prop: 'value' }, - { id: 'edgeWidthFixed', prop: 'value' }, - { id: 'edgeWidthMin', prop: 'value' }, - { id: 'edgeWidthMax', prop: 'value' }, - { id: 'sizeMetric', prop: 'value' }, - { id: 'sizeMin', prop: 'value' }, - { id: 'sizeMax', prop: 'value' }, - { id: 'sizeChannelOnly', prop: 'checked' }, - { id: 'nodeScale', prop: 'value' }, - { id: 'showGroupHulls', prop: 'checked' }, - { id: 'hullStyle', prop: 'value' }, - { id: 'hullOpacity', prop: 'value' }, - { id: 'layoutSelect', prop: 'value' }, - { id: 'layoutOnActiveOnly', prop: 'checked' }, - { id: 'layoutMode', prop: 'value' }, - { id: 'groupArrangement', prop: 'value' }, - { id: 'strategyAlgorithm', prop: 'value' }, - { id: 'localGroupLayout', prop: 'value' }, - { id: 'groupForce', prop: 'value' }, - { id: 'groupClusterRadius', prop: 'value' }, - { id: 'searchMode', prop: 'value' }, - { id: 'searchCase', prop: 'checked' }, - { id: 'searchAttrs', prop: 'checked' }, -] - -export function getCurrentConfig() { - const cfg = {} - CONFIG_FIELDS.forEach(({ id, prop }) => { - const el = document.getElementById(id) - if (!el) return - cfg[id] = prop === 'checked' ? el.checked : el.value - }) - return cfg -} - -// Sets every control named in cfg, then re-runs each style/layout -// function once so the change actually takes visual effect -- a control's -// own change-listener only fires on real user interaction, not on a value -// set programmatically, so this step can't be skipped. -// Settings files saved before the three NORMA-2.0 strategies had a single -// "layout by groups" toggle (the super-node strategy) with its own global -// layout and a 0.5-4 spacing slider. -function upgradeLegacyConfig(cfg) { - const out = { ...cfg } - if (out.groupStrategy === undefined && 'groupedLayoutToggle' in out) { - out.groupStrategy = out.groupedLayoutToggle ? 'supernodes' : 'none' - if (out.groupedLayoutToggle && out.globalGroupLayout && out.layoutSelect === undefined) { - out.layoutSelect = out.globalGroupLayout - } - } - // settings from before "by connections / by groups": a strategy select - // whose algorithm was the main layout choice - if (out.layoutMode === undefined && out.groupStrategy !== undefined) { - if (out.groupStrategy === 'none') { - out.layoutMode = 'connections' - } else { - out.layoutMode = 'groups' - out.groupArrangement = out.groupStrategy - if (out.layoutSelect) out.strategyAlgorithm = out.layoutSelect - } - } - if (out.groupForce === undefined && out.groupSpacing !== undefined) { - const spacing = parseFloat(out.groupSpacing) - if (Number.isFinite(spacing)) - out.groupForce = String(Math.min(20, Math.max(1, Math.round(spacing / 0.16)))) - } - return out -} - -export function applyConfig(cfg) { - if (!cfg || typeof cfg !== 'object') return - cfg = upgradeLegacyConfig(cfg) - CONFIG_FIELDS.forEach(({ id, prop }) => { - if (!(id in cfg)) return - const el = document.getElementById(id) - if (!el) return - if (prop === 'checked') el.checked = !!cfg[id] - else el.value = cfg[id] - }) - refreshAllDerivedUI() -} - -export function refreshAllDerivedUI() { - applyTheme(document.getElementById('themeSelect').value) - if (typeof applyValueColors === 'function') { - updateValueScalePreview() - applyValueColors() - } - applyNodePalette(document.getElementById('nodePaletteSelect').value, true) - applyEdgePalette(document.getElementById('edgePaletteSelect').value) - updateLabelStyle() - applyEdgeCurveStyle() - applyEdgeWidth() - applyNodeSizing() - applyEdgeOpacity() - applyEdgeDirection() - document.getElementById('hullControls').style.display = document.getElementById('showGroupHulls') - .checked - ? 'block' - : 'none' - updateStrategyUI() - drawGroupHulls() -} - -// Cuts data with too many nodes to the first MAX_NETWORK_NODES. -function capNetworkData(data) { - if (!data || !Array.isArray(data.nodes) || data.nodes.length <= MAX_NETWORK_NODES) return data - const total = data.nodes.length - const nodes = data.nodes.slice(0, MAX_NETWORK_NODES) - const keep = new Set(nodes.map((n) => n.id)) - const edges = (data.edges || []).filter((e) => keep.has(e.source) && keep.has(e.target)) - const text = `This network has ${total.toLocaleString('en-US')} nodes; NORMA shows up to ${MAX_NETWORK_NODES.toLocaleString('en-US')}, so only the first ${MAX_NETWORK_NODES.toLocaleString('en-US')} and the ${plural(edges.length, 'edge')} among them are shown.` - setTimeout(() => { - if (typeof toast === 'function') toast(text, 'warn') - const el = document.getElementById('normaStatus') - if (el) { - const note = document.createElement('div') - note.className = 'note warn' - note.textContent = text - el.prepend(note) - } - }, 0) - return { ...data, nodes, edges } -} - -// Extra legend sections that come with generated views (e.g. the comparison). -export let legendExtra = null - -export function loadData(data, opts = {}) { - data = capNetworkData(data) - legendExtra = Array.isArray(data.legendExtra) ? data.legendExtra : null - bumpDataVersion() - S.currentLibView = null - S.nodeColorMap = {} - S.groupShapes = - data.groupShapes && typeof data.groupShapes === 'object' ? { ...data.groupShapes } : {} - S.groupAttrs = - data.groupAttrs && typeof data.groupAttrs === 'object' ? { ...data.groupAttrs } : {} - setGroupOrder(data.groupOrder) - // give groups their palette colors in display order - if (Array.isArray(data.groupOrder)) { - data.groupOrder.forEach((g) => { - const explicit = sanitizeColor( - (data.nodeColors && data.nodeColors[g]) || (S.groupAttrs[g] && S.groupAttrs[g].color) - ) - if (explicit) S.nodeColorMap[g] = explicit - else colorForGroup(g) - }) - } - cy.elements().remove() - - const usedTypes = new Set() - const els = [] - - data.nodes.forEach((n) => { - const groupsArr = n.groups && n.groups.length ? n.groups : [n.group || UNGROUPED] - if (groupsArr.includes(UNGROUPED) && !S.groupAttrs[UNGROUPED]) { - S.groupAttrs[UNGROUPED] = { - label: UNGROUPED_LABEL, - description: 'Nodes that no group lists as a member', - } - } - groupsArr.forEach((g) => { - // explicit color precedence: nodeColors[g], then groupAttrs[g].color - const explicit = sanitizeColor( - (data.nodeColors && data.nodeColors[g]) || (S.groupAttrs[g] && S.groupAttrs[g].color) - ) - if (explicit) S.nodeColorMap[g] = explicit - }) - // a node's own "color" is kept separately and used when - // Colors -> Node fill is set to "Node colors from data" - const visual = computeNodeVisualFields(groupsArr) - els.push({ - group: 'nodes', - data: { - id: n.id, - size: n.size || 42, - baseSize: n.size || 42, - group: groupsArr.join(', '), - groups: groupsArr, - nodeColor: n.color || null, - values: n.values && typeof n.values === 'object' ? cleanValues(n.values) : null, - valueColor: null, - labelPx: 12, - labelColor: '#111827', - shape: 'ellipse', - attrs: collectAttrs(n, RESERVED_NODE_KEYS), - ...visual, - }, - }) - }) - - data.edges.forEach((e, i) => { - const type = e.type || 'link' - usedTypes.add(type) - const explicit = sanitizeColor((data.edgeColors && data.edgeColors[type]) || e.color) - if (!EDGE_TYPES[type]) { - EDGE_TYPES[type] = { color: explicit || nextAutoEdgeColor(), label: type } - } else if (explicit) { - EDGE_TYPES[type].color = explicit - } - els.push({ - group: 'edges', - data: { - id: e.id || 'e' + i, - source: e.source, - target: e.target, - type, - color: EDGE_TYPES[type].color, - elabel: '', - directed: e.directed === undefined ? !!data.directed : !!e.directed, - arrow: 'none', - attrs: collectAttrs(e, RESERVED_EDGE_KEYS), - ...(typeof e.weight === 'number' ? { weight: e.weight } : {}), - }, - }) - }) - - cy.add(els) - S.activeTypes = new Set(usedTypes.size ? usedTypes : Object.keys(EDGE_TYPES)) - S.activeGroups = new Set(getUsedGroups()) - buildLegend([...usedTypes]) - buildGroupLegend() - applyTypeVisibility() - applyGroupVisibility() - invalidateFullMetrics() - buildAttrSchema() - renderAttrSchema() - hideInfo() - hideEdgePopup() - if (currentTab === 'welcome' && typeof switchTab === 'function') switchTab('network') - if (data.config) { - applyConfig(data.config) // also covers applyEdgeWidth() and applyNodeSizing() - } else { - applyEdgeWidth() - applyNodeSizing() - } - const nodeCount = cy.nodes().length - updateRefreshState() - if (!opts.positions && typeof reset3dForNewData === 'function') reset3dForNewData() - if (opts.positions) { - // restoring a saved view: put nodes back where they were - cancelFrJobs() - S.layoutRunSeq++ - cy.nodes().positions((n) => opts.positions[n.id()] || n.position()) - } else if (layoutMode() === 'groups' && nodeCount <= LARGE_NETWORK_NODES) { - runStrategyLayout() - } else { - // a data file's settings may pick the opening layout - const preferred = (data.config && data.config.layoutSelect) || takeNextLoadLayout() - runLayout(nodeCount > LARGE_NETWORK_NODES ? 'grid' : preferred || 'cose') - } - applyEdgeDirection() // also merges parallel edges - if (typeof applyValueColors === 'function') applyValueColors() - updateStats() -} - -export function buildLegend(typesInData) { - const el = document.getElementById('edgeLegend') - el.innerHTML = '' - const list = typesInData.length ? typesInData : Object.keys(EDGE_TYPES) - list.forEach((type) => { - const meta = EDGE_TYPES[type] || { color: '#6c7793', label: type } - const count = cy.edges().filter((e) => e.data('type') === type).length - const row = document.createElement('label') - row.className = 'toggle-row' - row.innerHTML = ` - - - ${escapeHtml(meta.label)} - ${count} - ` - row.querySelector('input[type=checkbox]').addEventListener('change', (ev) => { - if (ev.target.checked) S.activeTypes.add(type) - else S.activeTypes.delete(type) - applyTypeVisibility() - }) - const picker = row.querySelector('.swatch-picker') - picker.addEventListener('click', (ev) => ev.stopPropagation()) - picker.addEventListener('input', (ev) => { - ev.stopPropagation() - const newColor = ev.target.value - if (!EDGE_TYPES[type]) EDGE_TYPES[type] = { color: newColor, label: type } - else EDGE_TYPES[type].color = newColor - cy.edges() - .filter((ed) => ed.data('type') === type) - .forEach((ed) => ed.data('color', newColor)) - applyEdgeMerge() - }) - el.appendChild(row) - }) -} - -export function getUsedTypes() { - return [...new Set(cy.edges().map((e) => e.data('type')))] -} - -// keepDataColors: groups whose data gives a color keep it (settings applied -// from files or the API); choosing a palette by hand recolors every group. -export function applyNodePalette(name, keepDataColors = false) { - S.currentNodePalette = name - const palette = NODE_PALETTES[name] || NODE_PALETTES.vivid - const groups = Object.keys(S.nodeColorMap).filter((g) => g !== UNGROUPED) - groups.forEach((g, i) => { - const own = keepDataColors && sanitizeColor(S.groupAttrs[g] && S.groupAttrs[g].color) - S.nodeColorMap[g] = own || colorAtIndex(palette, i) - }) - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - buildGroupLegend() -} - -export function applyEdgePalette(name) { - const types = getUsedTypes() - if (name === 'classic') { - types.forEach((t) => { - EDGE_TYPES[t] = ORIGINAL_EDGE_TYPES[t] - ? { ...ORIGINAL_EDGE_TYPES[t] } - : EDGE_TYPES[t] || { color: nextAutoEdgeColor(), label: t } - }) - } else { - const palette = EDGE_PALETTES[name] || EDGE_PALETTES.categorical - types.forEach((t, i) => { - const label = (EDGE_TYPES[t] && EDGE_TYPES[t].label) || t - EDGE_TYPES[t] = { color: colorAtIndex(palette, i), label } - }) - } - cy.edges().forEach((e) => { - const meta = EDGE_TYPES[e.data('type')] - if (meta) e.data('color', meta.color) - }) - buildLegend(types) - applyEdgeMerge() -} - -// page wiring, run by main.ts in the original order -export function init() { - window.addEventListener('resize', () => { - resizeHullCanvas() - drawGroupHulls() - }) - - if (typeof ResizeObserver !== 'undefined') { - new ResizeObserver(() => { - resizeHullCanvas() - drawGroupHulls() - }).observe(document.getElementById('canvas')) - } -} diff --git a/frontend/src/label_colors.ts b/frontend/src/label_colors.ts deleted file mode 100644 index c0b5a82..0000000 --- a/frontend/src/label_colors.ts +++ /dev/null @@ -1,332 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { UNGROUPED, escapeHtml } from './network_state' -import { currentTheme } from './themes' -import { edgeIsDirected } from './export/dialog' -import { libEntry, nextPaint, normaLibrary, plural, setStatus } from './layouts/controls' -import { profileDirected } from './directed_stats' -import { profileGraph, simpleGraph } from './wiring' -import { renderComparison } from './export/draw' -import { setStyle } from './cy' -import { shownEdges, shownNodes } from './metrics' -import { sortedByName } from './hulls' -import { updateLabelStyle, views } from './profiler' - -/* ---------- label colors ---------- */ -function isLightColor(hex) { - const m = /^#?([0-9a-f]{6})$/i.exec(hex || '') - if (!m) return false - const n = parseInt(m[1], 16) - const r = (n >> 16) & 255, - g = (n >> 8) & 255, - b = n & 255 - return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.72 -} - -function chosenLabelColor(modeId, pickerId) { - const mode = document.getElementById(modeId).value - const picker = document.getElementById(pickerId) - picker.hidden = mode !== 'custom' - if (mode === 'custom') return picker.value - return mode // 'auto', 'match' or a hex color -} - -export function applyLabelColors() { - const node = chosenLabelColor('nodeLabelColorMode', 'nodeLabelColor') - const edge = chosenLabelColor('edgeLabelColorMode', 'edgeLabelColor') - const theme = currentTheme - // light text gets a dark halo so it stays readable on a light canvas - const halo = (c) => (c !== 'auto' && c !== 'match' && isLightColor(c) ? '#1b2333' : theme.bg) - setStyle('node', { - color: node === 'auto' ? theme.text : node === 'match' ? 'data(labelColor)' : node, - 'text-outline-color': halo(node), - }) - setStyle('edge', { - color: edge === 'auto' ? theme.text : edge === 'match' ? 'data(color)' : edge, - 'text-background-color': isLightColor(edge) ? '#1b2333' : theme.bg, - }) -} - -/* ============================================================ - NETWORK COMPARISON - Up to ten networks, taken from open views or from the file library, - compared by node and edge overlap (Venn diagrams for two or three - networks, UpSet plots for any number), pairwise Jaccard similarity, - degree agreement on shared nodes, degree distributions, and the - profiler's topology statistics side by side. Nodes are matched by - name; edges by their unordered node pair, ignoring channels. - ============================================================ */ -const COMPARE_MAX = 10 - -const COMPARE_COLORS = [ - '#2563eb', - '#e0782b', - '#16a34a', - '#c026d3', - '#0891b2', - '#dc2626', - '#7c3aed', - '#ca8a04', - '#db2777', - '#475569', -] - -export const UPSET_MAX_COLUMNS = 40 - -export const compareState = { results: null } - -function compareSourceOptions() { - const opts = [] - views.forEach((v) => { - const n = v.id === S.activeViewId ? shownNodes().length : v.data ? v.data.nodes.length : 0 - if (n) - opts.push({ - value: 'view:' + v.id, - label: v.name, - meta: `view, ticked groups and channels`, - group: 'Open views', - }) - }) - sortedByName(normaLibrary.network, (e) => e.name).forEach((e) => { - opts.push({ - value: 'lib:' + e.id, - label: e.name, - meta: e.parsed.summary, - group: 'Network files', - }) - }) - return opts -} - -export function renderCompareList() { - const el = document.getElementById('cmpNetList') - if (!el) return - const previous = new Set([...el.querySelectorAll('input:checked')].map((i) => i.value)) - el.innerHTML = '' - const opts = compareSourceOptions() - if (!opts.length) { - el.innerHTML = '

    Open an example or upload networks first.

    ' - updateCompareLimit() - return - } - let lastGroup = null - opts.forEach((o) => { - if (o.group !== lastGroup) { - const h = document.createElement('div') - h.className = 'cmp-group-head' - h.textContent = o.group - el.appendChild(h) - lastGroup = o.group - } - const label = document.createElement('label') - label.className = 'cmp-option' - const input = document.createElement('input') - input.type = 'checkbox' - input.value = o.value - input.checked = previous.has(o.value) - input.addEventListener('change', updateCompareLimit) - const text = document.createElement('span') - text.innerHTML = `${escapeHtml(o.label)}${escapeHtml(o.meta)}` - label.append(input, text) - el.appendChild(label) - }) - updateCompareLimit() -} - -function updateCompareLimit() { - const boxes = [...document.querySelectorAll('#cmpNetList input')] - const checked = boxes.filter((b) => b.checked) - boxes.forEach((b) => { - b.disabled = !b.checked && checked.length >= COMPARE_MAX - }) - const btn = document.getElementById('btnCompare') - btn.disabled = checked.length < 2 - document.getElementById('cmpCount').textContent = - `${checked.length} of ${COMPARE_MAX} chosen` + - (checked.length < 2 ? ' (choose at least 2)' : '') -} - -function compareSource(value) { - if (value.startsWith('view:')) { - const v = views.find((x) => x.id === value.slice(5)) - if (!v) return null - let ids, pairs - if (v.id === S.activeViewId) { - ids = shownNodes().map((n) => n.id()) - pairs = shownEdges(true).map((e) => [e.data('source'), e.data('target'), edgeIsDirected(e)]) - } else { - if (!v.data) return null - // the part that view shows: its ticked groups and channels - const st = v.state || {} - const groupsOn = st.activeGroups ? new Set(st.activeGroups) : null - const typesOn = st.activeTypes ? new Set(st.activeTypes) : null - const shown = new Set( - v.data.nodes - .filter((n) => { - if (!groupsOn) return true - const gs = n.groups && n.groups.length ? n.groups : n.group ? [n.group] : [UNGROUPED] - return gs.some((g) => groupsOn.has(g)) - }) - .map((n) => n.id) - ) - ids = [...shown] - const mode = (st.config && st.config.edgeDirection) || 'data' - pairs = v.data.edges - .filter( - (e) => - shown.has(e.source) && - shown.has(e.target) && - (!typesOn || typesOn.has(e.type || 'link')) - ) - .map((e) => [ - e.source, - e.target, - mode === 'directed' ? true : mode === 'undirected' ? false : !!e.directed, - ]) - } - return { name: v.name, kind: 'view', ids, pairs } - } - const entry = libEntry('network', value.slice(4)) - if (!entry) return null - return { - name: entry.name, - kind: 'file', - ids: entry.parsed.nodes, - pairs: entry.parsed.edges.map((e) => [e.source, e.target, !!e.directed]), - } -} - -// Edge identity: an unordered pair, or with direction on, an ordered pair -// for directed edges (so A->B, B->A and undirected A-B all differ). -function pairKey(a, b, directed) { - if (directed) return a + '\t' + b + '\tdirected' - return (a < b ? a + '\t' + b : b + '\t' + a) + '\tundirected' -} - -// element -> bitmask of the sets containing it -export function membership(sets) { - const mask = new Map() - sets.forEach((set, i) => set.forEach((x) => mask.set(x, (mask.get(x) || 0) | (1 << i)))) - return mask -} - -export function exclusiveCounts(mask) { - const counts = new Map() - mask.forEach((m) => counts.set(m, (counts.get(m) || 0) + 1)) - return counts -} - -export function jaccard(a, b) { - let inter = 0 - const [small, big] = a.size < b.size ? [a, b] : [b, a] - small.forEach((x) => { - if (big.has(x)) inter++ - }) - const union = a.size + b.size - inter - return { inter, union, j: union ? inter / union : 0 } -} - -export function spearman(xs, ys) { - const rank = (arr) => { - const idx = arr.map((v, i) => [v, i]).sort((p, q) => p[0] - q[0]) - const r = new Array(arr.length) - for (let i = 0; i < idx.length;) { - let j = i - while (j + 1 < idx.length && idx[j + 1][0] === idx[i][0]) j++ - const avg = (i + j) / 2 + 1 - for (let k = i; k <= j; k++) r[idx[k][1]] = avg - i = j + 1 - } - return r - } - const n = xs.length - if (n < 3) return NaN - const rx = rank(xs), - ry = rank(ys) - const mx = (n + 1) / 2 - let num = 0, - dx = 0, - dy = 0 - for (let i = 0; i < n; i++) { - num += (rx[i] - mx) * (ry[i] - mx) - dx += (rx[i] - mx) ** 2 - dy += (ry[i] - mx) ** 2 - } - return dx && dy ? num / Math.sqrt(dx * dy) : NaN -} - -export function runComparison() { - const chosen = [...document.querySelectorAll('#cmpNetList input:checked')] - .map((i) => i.value) - .slice(0, COMPARE_MAX) - if (chosen.length < 2) { - setStatus('cmpStatus', [{ level: 'error', text: 'Choose at least two networks to compare.' }]) - return - } - // steps: one per network, then the overlaps and drawing - const cmpShow = (text, f) => setStatus('cmpStatus', [{ level: 'busy', text, progress: f }]) - cmpShow('Comparing networks…', 0) - document.getElementById('btnCompare').disabled = true - ;(async () => { - await nextPaint() - try { - const started = performance.now() - const nets = chosen - .map((value) => { - const n = compareSource(value) - if (n) n.sourceValue = value - return n - }) - .filter(Boolean) - const useDir = document.getElementById('cmpDirected').checked - for (const [i, net] of nets.entries()) { - cmpShow(`Profiling network ${i + 1} of ${nets.length}…`, i / (nets.length + 1)) - await nextPaint() - net.color = COMPARE_COLORS[i] - net.nodeSet = new Set(net.ids) - net.edgeSet = new Set() - net.pairs.forEach(([a, b, d]) => { - if (a !== b && net.nodeSet.has(a) && net.nodeSet.has(b)) - net.edgeSet.add(pairKey(a, b, useDir && d)) - }) - net.graph = simpleGraph( - net.ids, - net.pairs.map((p) => [p[0], p[1]]) - ) - net.stats = profileGraph(net.graph) - net.directed = - useDir && net.pairs.some((p) => p[2]) ? profileDirected(net.ids, net.pairs) : null - net.degreeOf = new Map(net.ids.map((id, k) => [id, net.stats.degree[k]])) - } - cmpShow('Computing overlaps and drawing…', nets.length / (nets.length + 1)) - await nextPaint() - compareState.results = nets - renderComparison(nets) - const secs = ((performance.now() - started) / 1000).toFixed(2) - setStatus('cmpStatus', [ - { level: 'ok', text: `Compared ${plural(nets.length, 'network')} in ${secs} s.` }, - ]) - document.getElementById('btnCompareTsv').disabled = false - document.getElementById('btnCompareNet').disabled = false - document.getElementById('cmpArena').hidden = false - } catch (err) { - setStatus('cmpStatus', [ - { level: 'error', text: `The comparison couldn't be computed: ${err.message}` }, - ]) - } finally { - updateCompareLimit() - } - })() -} - -// page wiring, run by main.ts in the original order -export function init() { - ;['nodeLabelColorMode', 'edgeLabelColorMode'].forEach((id) => { - document.getElementById(id).addEventListener('change', updateLabelStyle) - }) - - ;['nodeLabelColor', 'edgeLabelColor'].forEach((id) => { - document.getElementById(id).addEventListener('input', updateLabelStyle) - }) -} diff --git a/frontend/src/layouts/controls.ts b/frontend/src/layouts/controls.ts deleted file mode 100644 index 5aa19c7..0000000 --- a/frontend/src/layouts/controls.ts +++ /dev/null @@ -1,1079 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { MAX_NETWORK_NODES, UNGROUPED, getUsedGroups } from '../network_state' -import { S } from '../state' -import { SAMPLE_GENERATORS } from '../sample_data' -import { STRATEGIES, groupArrangement, layoutMode, runActiveLayout } from './run' -import { - applyConfig, - applyGroupVisibility, - buildGroupLegend, - filteredGroups, - getCurrentConfig, - loadData, - sortedByName, -} from '../hulls' -import { askDelete, deleteLibEntries } from '../api/tester' -import { clearCanvas, fitView, openInNewView, renderProfilerNetworkList } from '../profiler' -import { cy } from '../cy' -import { edgeIsDirected } from '../export/dialog' -import { exportEdges, exportNodes } from '../metrics' -import { loadNormaExampleSet } from '../examples' -import { numericShare, parseNumericValues } from '../clustering/mcl' - -/* ---------- layout controls ---------- */ -const ARRANGEMENT_CAPTIONS = { - fr: 'Groups are placed by how strongly they connect to each other (weighted force-directed), then pushed apart so none overlap.', - cose: 'Groups are placed by how they connect to each other (Cytoscape force-directed), then pushed apart so none overlap.', - circle: 'Groups sit side by side on a ring, in legend order.', - grid: 'Groups are packed in rows, in legend order.', - breadthfirst: - 'Groups are stacked in levels: the best-connected group on top, the groups it links to below it, and so on.', - concentric: 'The largest group sits in the middle, with the others in rings around it by size.', - virtual: - 'Each group gets a hidden hub tied to all its members by heavy links (dashed), so the layout pulls each group together. The hubs are removed afterwards.', - gravity: - 'Links inside a group are strengthened by the force and links between groups weakened by it. Layout-only links (dashed) join every pair in a group.', - supernodes: - 'Each group is collapsed into one node and the collapsed network is laid out. Groups are then pushed apart by the force, and each group\u2019s nodes are arranged around its position. Nodes in several groups sit between them.', -} - -export function setLayoutMode(mode) { - document.getElementById('layoutMode').value = mode === 'groups' ? 'groups' : 'connections' - updateStrategyUI() -} - -export function updateStrategyUI() { - const mode = layoutMode() - const arrangement = groupArrangement() - const isStrategy = STRATEGIES.has(arrangement) - document.querySelectorAll('#layoutModeSwitch [role="radio"]').forEach((btn) => { - const on = btn.dataset.mode === mode - btn.setAttribute('aria-checked', on ? 'true' : 'false') - btn.tabIndex = on ? 0 : -1 - }) - document.getElementById('connLayoutSet').disabled = mode !== 'connections' - document.getElementById('groupLayoutSet').disabled = mode !== 'groups' - - const fig = document.getElementById('strategyFig') - fig - .querySelectorAll('svg') - .forEach((svg) => svg.classList.toggle('on', svg.dataset.strategy === arrangement)) - document.getElementById('strategyCaption').textContent = ARRANGEMENT_CAPTIONS[arrangement] || '' - document.getElementById('strategyAlgorithmRow').hidden = !isStrategy - document.getElementById('strategyAlgorithmLabel').textContent = - arrangement === 'supernodes' - ? 'Algorithm for the collapsed network' - : 'Algorithm the strategy runs' - - const keepOpt = document.querySelector('#localGroupLayout option[value="keep"]') - const keepAllowed = arrangement === 'virtual' || arrangement === 'gravity' - keepOpt.disabled = !keepAllowed - keepOpt.hidden = !keepAllowed - const localSel = document.getElementById('localGroupLayout') - if (!keepAllowed && localSel.value === 'keep') localSel.value = 'circle' - - document.getElementById('forceLabelText').textContent = isStrategy - ? 'Force strength' - : 'Space between groups' - document.getElementById('forceRow').hidden = - arrangement === 'virtual' && localSel.value === 'keep' - document.getElementById('clusterRow').hidden = localSel.value === 'keep' - document.getElementById('groupForceValue').textContent = - document.getElementById('groupForce').value - document.getElementById('groupClusterRadiusValue').textContent = ( - parseFloat(document.getElementById('groupClusterRadius').value) || 1 - ).toFixed(1) - document.getElementById('btnRunLayout').textContent = - mode === 'groups' ? 'Run group layout' : 'Run layout' -} - -/* ============================================================ - FILES: NORMA-FORMAT LIBRARY - Reads and writes the three tab-delimited inputs used by NORMA - (Koutrouli et al. 2022; Karatzas et al. 2022): - network -- header "SourceTarget[Weight][Type]", - undirected; self-loops and repeats are dropped. The - optional Type column is this tool's extension: it keeps - parallel edges, one per type. - annotation -- no header; "groupnode1,node2,..." - expression -- no header; "nodecolor" (name or hex) - Every uploaded file is kept in an in-memory library. Any set of networks - can be viewed together (each becomes an edge channel) with one - annotation and one expression file. Annotation and expression entries - for nodes missing from the chosen networks are discarded at view time - (the job of NORMA's companion R script); the corrected annotation can be - downloaded. - ============================================================ */ -export const NORMA_KINDS = ['network', 'annotation', 'colors'] - -export const NORMA_KIND_NAMES = { - network: 'network', - annotation: 'annotation', - colors: 'expression file', -} - -const LIB_LIST_IDS = { - network: 'libNetworks', - annotation: 'libAnnotations', - colors: 'libExpressions', -} - -export const normaLibrary = { network: [], annotation: [], colors: [] } - -export const libSelection = { networks: new Set(), annotation: '', colors: '' } - -export function plural(n, one, many) { - return `${n} ${n === 1 ? one : many || one + 's'}` -} - -export function listSample(items, max = 6) { - const shown = items.slice(0, max).join(', ') - return items.length > max ? `${shown} and ${items.length - max} more` : shown -} - -export function articleFor(word) { - return /^[aeiou]/i.test(word) ? 'an' : 'a' -} - -export function normaLines(text) { - return String(text) - .replace(/^\uFEFF/, '') - .split(/\r\n|\n|\r/) -} - -// Tab-separated columns. Files saved with spaces instead of tabs (common -// after copy-pasting) fall back to splitting on whitespace. -export function normaCols(line) { - let cols = line.split('\t') - if (cols.length === 1) cols = line.trim().split(/\s+/) - cols = cols.map((c) => c.trim()) - while (cols.length && cols[cols.length - 1] === '') cols.pop() - return cols -} - -const NETWORK_TYPE_HEADERS = new Set(['type', 'channel', 'layer', 'interaction']) - -const NETWORK_DIRECTION_HEADERS = new Set(['direction', 'directed', 'dir', 'arrow']) - -const DIRECTED_WORDS = new Set(['directed', 'yes', 'y', 'true', '1', '->', '-->', '→', 'forward']) - -const UNDIRECTED_WORDS = new Set([ - 'undirected', - 'no', - 'n', - 'false', - '0', - '-', - '--', - '—', - 'both', - 'none', - '', -]) - -// options.directed: how to read rows without a Direction value -export function parseNormaNetwork(text, options = {}) { - const defaultDirected = !!options.directed - const lines = normaLines(text) - const headerIdx = lines.findIndex((l) => l.trim() !== '') - if (headerIdx < 0) throw new Error('The file is empty.') - const header = normaCols(lines[headerIdx]).map((h) => h.toLowerCase()) - if (header[0] !== 'source' || header[1] !== 'target') { - throw new Error( - 'The first line must be the header "Source, Target" (optionally followed by Weight, Type and Direction), separated by tabs.' - ) - } - let weightCol = -1, - typeCol = -1, - dirCol = -1 - for (let c = 2; c < header.length; c++) { - if (header[c] === 'weight' && weightCol < 0) weightCol = c - else if (NETWORK_TYPE_HEADERS.has(header[c]) && typeCol < 0) typeCol = c - else if (NETWORK_DIRECTION_HEADERS.has(header[c]) && dirCol < 0) dirCol = c - else - throw new Error( - `The header column "${header[c]}" isn't recognised. After Source and Target, only Weight, Type and Direction are allowed.` - ) - } - const minCols = Math.max(2, weightCol + 1, typeCol + 1) - const badDirections = [] - - const nodes = new Set() - const seen = new Set() - const edges = [] - const types = new Set() - let selfLoops = 0, - repeats = 0, - directedCount = 0 - const shortLines = [], - badWeights = [] - for (let i = headerIdx + 1; i < lines.length; i++) { - if (!lines[i].trim()) continue - const cols = normaCols(lines[i]) - if (cols.length < minCols) { - shortLines.push(i + 1) - continue - } - const [source, target] = cols - nodes.add(source) - nodes.add(target) - if (source === target) { - selfLoops++ - continue - } - const type = typeCol >= 0 ? cols[typeCol] : '' - let directed = defaultDirected - if (dirCol >= 0 && cols[dirCol] !== undefined) { - const word = cols[dirCol].trim().toLowerCase() - if (DIRECTED_WORDS.has(word)) directed = true - else if (UNDIRECTED_WORDS.has(word)) directed = false - else badDirections.push(i + 1) - } - // A directed edge A->B differs from B->A; an undirected A-B doesn't. - const pair = directed - ? 'D\t' + source + '\t' + target - : 'U\t' + (source < target ? source + '\t' + target : target + '\t' + source) - const key = pair + '\t' + type - if (seen.has(key)) { - repeats++ - continue - } - seen.add(key) - const edge = { source, target } - if (directed) { - edge.directed = true - directedCount++ - } - if (type) { - edge.type = type - types.add(type) - } - if (weightCol >= 0) { - const w = Number(cols[weightCol]) - if (Number.isFinite(w)) edge.weight = w - else badWeights.push(i + 1) - } - edges.push(edge) - } - if (!edges.length) throw new Error('No connections were found below the header row.') - - const notes = [] - if (nodes.size > MAX_NETWORK_NODES) { - // keep the first nodes in the order they appear, and the edges among them - const keep = new Set([...nodes].slice(0, MAX_NETWORK_NODES)) - const total = nodes.size, - before = edges.length - const kept = edges.filter((e) => keep.has(e.source) && keep.has(e.target)) - edges.length = 0 - edges.push(...kept) - nodes.clear() - keep.forEach((n) => nodes.add(n)) - types.clear() - directedCount = 0 - edges.forEach((e) => { - if (e.type) types.add(e.type) - if (e.directed) directedCount++ - }) - notes.push( - `This network has ${total.toLocaleString('en-US')} nodes; NORMA shows up to ${MAX_NETWORK_NODES.toLocaleString('en-US')}, so only the first ${MAX_NETWORK_NODES.toLocaleString('en-US')} (in file order) and the ${plural(edges.length, 'edge')} among them were kept (${(before - edges.length).toLocaleString('en-US')} edges left out).` - ) - if (!edges.length) - throw new Error( - `The first ${MAX_NETWORK_NODES.toLocaleString('en-US')} nodes of this network have no connections among them.` - ) - } - if (selfLoops) notes.push(`Removed ${plural(selfLoops, 'self-loop')}.`) - if (repeats) - notes.push( - directedCount === edges.length - ? `Removed ${plural(repeats, 'repeated connection')} with the same source, target${typeCol >= 0 ? ' and type' : ''}.` - : typeCol >= 0 - ? `Removed ${plural(repeats, 'repeated connection')} with the same pair and type${directedCount ? ' (for undirected rows, A–B and B–A are the same pair)' : ''}.` - : `Removed ${plural(repeats, 'repeated connection')} (for undirected rows, A–B and B–A count as the same connection).` - ) - if (badDirections.length) - notes.push( - `Read ${plural(badDirections.length, 'Direction value')} that ${badDirections.length === 1 ? "isn't" : "aren't"} "directed" or "undirected" as ${defaultDirected ? 'directed' : 'undirected'}: line ${listSample(badDirections)}.` - ) - if (shortLines.length) - notes.push( - `Skipped ${plural(shortLines.length, 'line')} with fewer than ${minCols} columns: line ${listSample(shortLines)}.` - ) - if (badWeights.length) - notes.push( - `Left out ${plural(badWeights.length, 'weight')} that ${badWeights.length === 1 ? "isn't a number" : "aren't numbers"}: line ${listSample(badWeights)}.` - ) - const commaNames = [...nodes].filter((n) => n.includes(',')) - if (commaNames.length) - notes.push( - `${plural(commaNames.length, 'node name contains', 'node names contain')} a comma, so annotation files can't list ${commaNames.length === 1 ? 'it' : 'them'}: ${listSample(commaNames)}.` - ) - - const summaryParts = [plural(nodes.size, 'node'), plural(edges.length, 'edge')] - if (types.size) summaryParts.push(plural(types.size, 'type')) - if (weightCol >= 0) summaryParts.push('weighted') - const direction = - directedCount === 0 ? 'undirected' : directedCount === edges.length ? 'directed' : 'mixed' - if (direction !== 'undirected') - summaryParts.push( - direction === 'mixed' ? `mixed (${directedCount.toLocaleString()} directed)` : 'directed' - ) - return { - nodes: [...nodes], - edges, - weighted: weightCol >= 0, - types: [...types], - notes, - direction, - directedCount, - summary: summaryParts.join(', '), - } -} - -const ANNOTATION_HEADER_WORDS = - /^(groups?|annotations?|terms?|pathways?|names?|modules?|clusters?)$/i - -function parseNormaAnnotation(text) { - const lines = normaLines(text) - const groups = new Map() - let spacesStripped = 0, - merged = 0, - headerSkipped = false - const shortLines = [] - let firstDataLine = true - lines.forEach((raw, i) => { - if (!raw.trim()) return - let name, list - const tab = raw.indexOf('\t') - if (tab >= 0) { - name = raw.slice(0, tab).trim() - list = raw.slice(tab + 1).replace(/\t/g, '') - } else { - // no tab: only accept the unambiguous "namelist" shape - const parts = raw.trim().split(/\s+/) - if (parts.length === 2) { - name = parts[0] - list = parts[1] - } - } - if (!name || !list || !list.trim()) { - shortLines.push(i + 1) - firstDataLine = false - return - } - if (firstDataLine && ANNOTATION_HEADER_WORDS.test(name) && !list.includes(',')) { - headerSkipped = true - firstDataLine = false - return - } - firstDataLine = false - if (/\s/.test(list.trim())) spacesStripped++ - const members = list - .split(',') - .map((m) => m.trim()) - .filter(Boolean) - if (groups.has(name)) merged++ - const set = groups.get(name) || new Set() - members.forEach((m) => set.add(m)) - groups.set(name, set) - }) - if (!groups.size) { - throw new Error( - 'No groups were found. Each line should be a group name, a tab, then node names separated by commas.' - ) - } - const notes = [] - if (headerSkipped) - notes.push( - 'Skipped the first line because it looks like a header. Annotation files have no header row.' - ) - if (shortLines.length) - notes.push( - `Skipped ${plural(shortLines.length, 'line')} without a group name and node list: line ${listSample(shortLines)}.` - ) - if (spacesStripped) - notes.push( - `Removed spaces from the node lists on ${plural(spacesStripped, 'line')}. NORMA expects commas only.` - ) - if (merged) notes.push(`Merged ${plural(merged, 'repeated group name')} into one group each.`) - const list = [...groups].map(([name, set]) => ({ name, members: [...set] })) - return { groups: list, notes, summary: plural(list.length, 'group') } -} - -const colorProbeCtx = document.createElement('canvas').getContext('2d') - -// Returns a canvas-normalized color ("#rrggbb" or "rgba(...)"), or null -// if the browser doesn't recognise the value as a CSS color. -export function normalizeCssColor(value) { - const probe = new Option().style - probe.color = value - if (!probe.color) return null - colorProbeCtx.fillStyle = '#000000' - colorProbeCtx.fillStyle = value - return colorProbeCtx.fillStyle -} - -function parseNormaColors(text) { - // numbers instead of colors: a numeric expression file - const sample = normaLines(text) - .filter((l) => l.trim()) - .slice(0, 300) - if (sample.length && numericShare(sample.slice(1)) >= 0.8) return parseNumericValues(text) - const lines = normaLines(text) - const colors = new Map() - const invalid = [], - shortLines = [] - let headerSkipped = false, - firstDataLine = true - lines.forEach((raw, i) => { - if (!raw.trim()) return - const cols = normaCols(raw) - if (cols.length < 2) { - shortLines.push(i + 1) - firstDataLine = false - return - } - const [node, value] = cols - const color = normalizeCssColor(value) - if (!color) { - if (firstDataLine && /^(nodes?|genes?|names?|ids?|proteins?)$/i.test(node)) { - headerSkipped = true - } else invalid.push(`${node} (${value})`) - firstDataLine = false - return - } - firstDataLine = false - colors.set(node, color) - }) - if (!colors.size) { - throw new Error( - 'No node colors were found. Each line should be a node name, a tab, then a color such as red or #ff0000.' - ) - } - const notes = [] - if (headerSkipped) - notes.push( - 'Skipped the first line because it looks like a header. Node-color files have no header row.' - ) - if (shortLines.length) - notes.push( - `Skipped ${plural(shortLines.length, 'line')} without both a node and a color: line ${listSample(shortLines)}.` - ) - if (invalid.length) - notes.push( - `Ignored ${plural(invalid.length, 'unrecognised color')}: ${listSample(invalid)}. Use a color name like red or a hex code like #ff0000.` - ) - return { colors, notes, summary: plural(colors.size, 'colored node') } -} - -export const NORMA_PARSERS = { - network: parseNormaNetwork, - annotation: parseNormaAnnotation, - colors: parseNormaColors, -} - -// Guesses a file's kind: a Source/Target header means a network; a second -// column that is (almost) always a color means an expression file; -// anything else is read as an annotation. -export function detectNormaKind(text) { - const lines = normaLines(text) - .filter((l) => l.trim()) - .slice(0, 300) - if (!lines.length) return null - const head = normaCols(lines[0]).map((c) => c.toLowerCase()) - if (head[0] === 'source' && head[1] === 'target') return 'network' - if (numericShare(lines.slice(1)) >= 0.8) return 'colors' - let rows = 0, - colorish = 0, - commas = 0 - lines.forEach((line) => { - const tab = line.indexOf('\t') - const second = (tab >= 0 ? line.slice(tab + 1) : line.trim().split(/\s+/)[1] || '').trim() - if (!second) return - rows++ - if (second.includes(',')) commas++ - else if (normalizeCssColor(second)) colorish++ - }) - if (rows && !commas && colorish >= rows * 0.8) return 'colors' - return 'annotation' -} - -function uniqueNormaName(kind, name) { - const taken = new Set(normaLibrary[kind].map((e) => e.name)) - if (!taken.has(name)) return name - let n = 2 - while (taken.has(`${name} (${n})`)) n++ - return `${name} (${n})` -} - -// Parses and stores a file. Throws with a readable message if the file -// doesn't match the format; returns the new library entry otherwise. -export function addNormaEntry(kind, name, text, fileName, sourceKey, options) { - const parsed = NORMA_PARSERS[kind](text, options || {}) - const entry = { - id: 'lib' + ++S.normaEntrySeq, - kind, - name: uniqueNormaName(kind, name), - fileName: fileName || '', - sourceKey: sourceKey || null, - text, - options: options || {}, - parsed, - } - normaLibrary[kind].push(entry) - return entry -} - -export function libEntry(kind, id) { - return normaLibrary[kind].find((e) => e.id === id) || null -} - -export function selectedNetworks() { - return normaLibrary.network.filter((e) => libSelection.networks.has(e.id)) -} - -export function selectionKey() { - return { - nets: selectedNetworks() - .map((e) => e.id) - .join('|'), - annotation: libSelection.annotation, - colors: libSelection.colors, - } -} - -function selectionDiffersFromView() { - if (!S.currentLibView) return libSelection.networks.size > 0 - const k = selectionKey() - return ( - k.nets !== S.currentLibView.nets || - k.annotation !== S.currentLibView.annotation || - k.colors !== S.currentLibView.colors - ) -} - -export function updateRefreshState() { - const pending = selectionDiffersFromView() && libSelection.networks.size > 0 - document.getElementById('refreshHint').hidden = !pending - const btn = document.getElementById('btnRefreshView') - btn.classList.toggle('pending', pending) - btn.disabled = libSelection.networks.size === 0 - const nb = document.getElementById('btnOpenNewView') - if (nb) nb.disabled = libSelection.networks.size === 0 -} - -export function renderLibraryLists() { - const delAll = document.getElementById('btnDeleteAllFiles') - if (delAll) delAll.disabled = !NORMA_KINDS.some((k) => normaLibrary[k].length) - NORMA_KINDS.forEach((kind) => { - const el = document.getElementById(LIB_LIST_IDS[kind]) - el.innerHTML = '' - const entries = normaLibrary[kind] - const multi = kind === 'network' - const groupName = 'lib-' + kind - - if (!multi) { - el.appendChild( - libRow({ - kind, - inputType: 'radio', - groupName, - id: '', - name: 'None', - meta: '', - checked: (kind === 'annotation' ? libSelection.annotation : libSelection.colors) === '', - }) - ) - } - if (multi && !entries.length) { - const empty = document.createElement('div') - empty.className = 'lib-empty' - empty.textContent = 'Upload a network or load an example.' - el.appendChild(empty) - } - sortedByName(entries, (e) => e.name).forEach((e) => { - const checked = multi - ? libSelection.networks.has(e.id) - : (kind === 'annotation' ? libSelection.annotation : libSelection.colors) === e.id - el.appendChild( - libRow({ - kind, - inputType: multi ? 'checkbox' : 'radio', - groupName, - id: e.id, - name: e.name, - meta: e.parsed.summary, - checked, - removable: true, - title: e.fileName ? `${e.name} (${e.fileName})` : e.name, - }) - ) - }) - }) - updateRefreshState() - if (typeof renderProfilerNetworkList === 'function') renderProfilerNetworkList() -} - -function libRow({ kind, inputType, groupName, id, name, meta, checked, removable, title }) { - const row = document.createElement('label') - row.className = 'lib-row' - if (title) row.title = title - const input = document.createElement('input') - input.type = inputType - input.name = groupName - input.checked = checked - input.value = id - input.addEventListener('change', () => { - if (kind === 'network') { - if (input.checked) libSelection.networks.add(id) - else libSelection.networks.delete(id) - } else if (input.checked) { - if (kind === 'annotation') libSelection.annotation = id - else libSelection.colors = id - } - updateRefreshState() - }) - const text = document.createElement('span') - text.className = 'lib-text' - const nameEl = document.createElement('span') - nameEl.className = 'lib-name' - nameEl.textContent = name - text.appendChild(nameEl) - if (meta) { - const metaEl = document.createElement('span') - metaEl.className = 'lib-meta' - metaEl.textContent = meta - text.appendChild(metaEl) - } - row.append(input, text) - if (removable) { - const rm = document.createElement('button') - rm.type = 'button' - rm.className = 'lib-remove' - rm.textContent = '✕' - rm.setAttribute('aria-label', `Delete ${name}`) - rm.title = 'Delete this file from NORMA' - rm.addEventListener('click', (ev) => { - ev.preventDefault() - ev.stopPropagation() - askDelete([{ kind, id }]) - }) - row.appendChild(rm) - } - return row -} - -// kept for scripts: deletes without asking -function removeLibEntry(kind, id) { - deleteLibEntries([{ kind, id }]) -} - -// items: [{ level: 'ok' | 'warn' | 'error' | 'busy', text, action?: { label, run } }] -// opens the folded sidebar section that holds `el` -function revealSection(el) { - const sec = el && el.closest && el.closest('.section.collapsible.collapsed') - if (!sec) return - sec.classList.remove('collapsed') - const h = sec.querySelector(':scope > h3') - if (h) h.setAttribute('aria-expanded', 'true') -} - -// A busy item may carry `progress`: a fraction from 0 to 1, or null while -// the amount of work is unknown (an animated bar). Repeated progress updates -// change the bar in place, so it moves smoothly and screen readers are not -// sent every percentage. -export function setStatus(elId, items) { - const el = document.getElementById(elId) - if (!el) return - const withProgress = items.length === 1 && items[0].progress !== undefined - if (!withProgress && progressTasks && progressTasks.has(elId)) progressTasks.get(elId).stop() - if (withProgress) { - const note = el.children.length === 1 ? el.firstElementChild : null - if (note && note.classList.contains('busy') && note._progress) { - updateProgressNote(note, items[0]) - return - } - } - // sections start folded: an error opens the section so it is seen - if (items.some((i) => i.level === 'error')) revealSection(el) - el.innerHTML = '' - items.forEach((item) => { - const note = document.createElement('div') - note.className = 'note ' + (item.level || 'ok') - if (item.progress !== undefined) { - const label = document.createElement('span') - const row = document.createElement('div') - row.className = 'progress-row' - const bar = document.createElement('div') - bar.className = 'progress' - bar.setAttribute('role', 'progressbar') - bar.setAttribute('aria-valuemin', '0') - bar.setAttribute('aria-valuemax', '100') - bar.appendChild(document.createElement('span')) - const pct = document.createElement('span') - pct.className = 'progress-pct' - pct.setAttribute('aria-hidden', 'true') - row.append(bar, pct) - note.append(label, row) - note._progress = { label, bar, pct } - updateProgressNote(note, item) - el.appendChild(note) - return - } - note.textContent = item.text - if (item.action) { - const btn = document.createElement('button') - btn.type = 'button' - btn.textContent = item.action.label - btn.addEventListener('click', item.action.run) - note.appendChild(btn) - } - el.appendChild(note) - }) -} - -function updateProgressNote(note, item) { - const { label, bar, pct } = note._progress - if (label.textContent !== item.text) { - label.textContent = item.text - bar.setAttribute('aria-label', item.text) - } - const f = item.progress - if (typeof f === 'number' && Number.isFinite(f)) { - const v = Math.max(0, Math.min(1, f)) - bar.classList.remove('indeterminate') - bar.firstElementChild.style.width = (v * 100).toFixed(1) + '%' - bar.setAttribute('aria-valuenow', String(Math.round(v * 100))) - pct.textContent = Math.floor(v * 100) + '%' - } else { - bar.classList.add('indeterminate') - bar.firstElementChild.style.width = '' - bar.removeAttribute('aria-valuenow') - pct.textContent = '' - } -} - -/* ---------- progress of a task made of weighted steps ---------- - const task = startProgress('stringStatus', [1, 2, 5]); - task.step(0, 'Checking the version…'); // moves to a step - task.sub(0.4); // measured progress in the step - task.say('Reading 3 of 8…'); // new text, same step - task.bytes(received, total); // download progress in the step - Until a step reports measured progress, its bar creeps slowly toward the - end of the step (never reaching it), so a long single request still shows - that work goes on. Any status that is not a progress update, such as the - result or an error, ends the task. */ -var progressTasks = new Map() - -function formatBytes(n) { - if (n < 1024) return `${n} B` - if (n < 1048576) return `${(n / 1024).toFixed(0)} kB` - return `${(n / 1048576).toFixed(n < 10485760 ? 1 : 0)} MB` -} - -export function startProgress(elId, weights) { - const old = progressTasks.get(elId) - if (old) old.stop() - const w = weights.map((x) => Math.max(0, +x || 0)) - const total = w.reduce((a, b) => a + b, 0) || 1 - const starts = w.map((_, i) => w.slice(0, i).reduce((a, b) => a + b, 0)) - const task = { - index: 0, - text: '', - measured: null, - stepStart: performance.now(), - received: 0, - active: true, - timer: null, - last: 0, - fraction() { - const i = Math.min(this.index, w.length - 1) - let sub = this.measured - if (sub === null) { - // creep: about half of the step after ~6 s, never more than 85% - const t = (performance.now() - this.stepStart) / 1000 - sub = 0.85 * (1 - Math.exp(-t / 8)) - } - return (starts[i] + w[i] * Math.max(0, Math.min(1, sub))) / total - }, - render(force) { - if (!this.active) return - const now = performance.now() - if (!force && now - this.last < 100) return - this.last = now - const extra = this.received ? ` (${formatBytes(this.received)} received)` : '' - setStatus(elId, [{ level: 'busy', text: this.text + extra, progress: this.fraction() }]) - }, - step(i, text) { - this.index = i - this.text = text - this.measured = null - this.received = 0 - this.stepStart = performance.now() - this.render(true) - }, - say(text) { - this.text = text - this.render(true) - }, - sub(f) { - this.measured = Math.max(0, Math.min(1, f)) - this.render() - }, - bytes(received, totalBytes) { - this.received = received - if (totalBytes > 0 && received <= totalBytes) this.measured = received / totalBytes - this.render() - }, - stop() { - this.active = false - clearInterval(this.timer) - if (progressTasks.get(elId) === this) progressTasks.delete(elId) - }, - } - progressTasks.set(elId, task) - task.timer = setInterval(() => task.render(true), 400) - return task -} - -// Resolves once the browser has had a chance to paint (for example a -// progress note) before synchronous work; does not wait in a hidden tab. -export function nextPaint() { - return new Promise((resolve) => { - let done = false - const finish = () => { - if (!done) { - done = true - resolve() - } - } - requestAnimationFrame(() => setTimeout(finish, 0)) - setTimeout(finish, 60) - }) -} - -// Reads a response body as text, reporting the bytes received so far and, -// when the server gives it, the total. -export async function readTextWithProgress(response, onBytes) { - if (!onBytes || !response.body || typeof response.body.getReader !== 'function') - return response.text() - const total = Number(response.headers.get('Content-Length')) || 0 - const reader = response.body.getReader() - const decoder = new TextDecoder() - let received = 0, - out = '' - for (;;) { - const { done, value } = await reader.read() - if (done) break - received += value.length - out += decoder.decode(value, { stream: true }) - onBytes(received, total) - } - return out + decoder.decode() -} - -export function downloadText(fileName, text) { - const blob = new Blob([text], { type: 'text/plain' }) - const a = document.createElement('a') - a.href = URL.createObjectURL(blob) - a.download = fileName - document.body.appendChild(a) - a.click() - a.remove() - setTimeout(() => URL.revokeObjectURL(a.href), 1000) -} - -export function annotationText(groups) { - return groups.map((g) => `${g.name}\t${g.members.join(',')}`).join('\n') + '\n' -} - -export function fileStem(name) { - return ( - String(name) - .replace(/\.[^.]+$/, '') - .replace(/[^\w.-]+/g, '_') - .replace(/^_+|_+$/g, '') || 'file' - ) -} - -// page wiring, run by main.ts in the original order -export function init() { - document.getElementById('btnRunLayout').addEventListener('click', runActiveLayout) - - document.querySelectorAll('#layoutModeSwitch [role="radio"]').forEach((btn) => { - btn.addEventListener('click', () => setLayoutMode(btn.dataset.mode)) - btn.addEventListener('keydown', (e) => { - if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return - e.preventDefault() - const next = btn.dataset.mode === 'groups' ? 'connections' : 'groups' - setLayoutMode(next) - document.querySelector(`#layoutModeSwitch [data-mode="${next}"]`).focus() - }) - }) - - ;['groupArrangement', 'localGroupLayout'].forEach((id) => { - document.getElementById(id).addEventListener('change', updateStrategyUI) - }) - - ;['groupForce', 'groupClusterRadius'].forEach((id) => { - document.getElementById(id).addEventListener('input', updateStrategyUI) - }) - - document.getElementById('btnFit').addEventListener('click', () => fitView()) - - document.getElementById('btnSample').addEventListener('click', () => { - const sel = document.getElementById('sampleSelect') - const key = sel.value - const title = sel.options[sel.selectedIndex].text.replace(/\s*\([^)]*nodes\)\s*$/, '') - openInNewView(title, () => { - if (key.startsWith('norma:')) { - loadNormaExampleSet(key.slice(6)) - return - } - libSelection.networks = new Set() - libSelection.annotation = '' - libSelection.colors = '' - renderLibraryLists() - const gen = SAMPLE_GENERATORS[key] || SAMPLE_GENERATORS.trp - loadData(gen()) - setStatus('normaStatus', []) - }) - }) - - document.getElementById('btnClear').addEventListener('click', () => { - clearCanvas() - setStatus('normaStatus', []) - updateRefreshState() - buildGroupLegend() - }) - - // With a filter set, these act only on the matching groups. - document.getElementById('btnGroupsAll').addEventListener('click', () => { - filteredGroups().forEach((g) => S.activeGroups.add(g)) - buildGroupLegend() - applyGroupVisibility() - }) - - document.getElementById('btnGroupsNone').addEventListener('click', () => { - filteredGroups().forEach((g) => S.activeGroups.delete(g)) - buildGroupLegend() - applyGroupVisibility() - }) - - document.getElementById('groupFilter').addEventListener('input', buildGroupLegend) - - document.getElementById('btnImportToggle').addEventListener('click', () => { - const box = document.getElementById('importBox') - box.style.display = box.style.display === 'none' ? 'block' : 'none' - }) - - document.getElementById('btnImportGo').addEventListener('click', () => { - try { - const parsed = JSON.parse(document.getElementById('importArea').value) - if (!parsed.nodes || !parsed.edges) throw new Error('JSON needs "nodes" and "edges" arrays.') - openInNewView('Pasted JSON', () => loadData(parsed)) - document.getElementById('importBox').style.display = 'none' - } catch (err) { - alert('Could not parse JSON: ' + err.message) - } - }) - - document - .getElementById('btnFileTrigger') - .addEventListener('click', () => document.getElementById('fileInput').click()) - - document.getElementById('fileInput').addEventListener('change', (e) => { - const file = e.target.files[0] - if (!file) return - const reader = new FileReader() - reader.onload = (ev) => { - try { - const parsed = JSON.parse(ev.target.result) - if (!parsed.nodes || !parsed.edges) - throw new Error('JSON needs "nodes" and "edges" arrays.') - openInNewView(file.name.replace(/\.json$/i, ''), () => loadData(parsed)) - } catch (err) { - alert('Could not read file: ' + err.message) - } - } - reader.readAsText(file) - e.target.value = '' - }) - - document.getElementById('btnExport').addEventListener('click', () => { - const nodeColors = {} - cy.nodes().forEach((n) => { - ;(n.data('groups') || []).forEach((g) => { - nodeColors[g] = S.nodeColorMap[g] || nodeColors[g] - }) - }) - const edgeColors = {} - cy.edges().forEach((e) => { - edgeColors[e.data('type')] = e.data('color') - }) - const data = { - nodes: exportNodes().map((n) => ({ - id: n.data('id'), - groups: (n.data('groups') || []).filter((g) => g !== UNGROUPED), - size: n.data('baseSize'), - ...(n.data('nodeColor') ? { color: n.data('nodeColor') } : {}), - ...(n.data('values') ? { values: n.data('values') } : {}), - ...(n.data('attrs') || {}), - })), - edges: exportEdges().map((e) => { - const out = { - id: e.data('id'), - source: e.data('source'), - target: e.data('target'), - type: e.data('type'), - } - const w = e.data('weight') - if (typeof w === 'number') out.weight = w - if (edgeIsDirected(e)) out.directed = true - return { ...out, ...(e.data('attrs') || {}) } - }), - nodeColors, - edgeColors, - groupOrder: getUsedGroups().filter((g) => g !== UNGROUPED), - ...(Object.keys(S.groupShapes).length ? { groupShapes: S.groupShapes } : {}), - ...(Object.keys(S.groupAttrs).length ? { groupAttrs: S.groupAttrs } : {}), - } - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) - const a = document.createElement('a') - a.href = URL.createObjectURL(blob) - a.download = 'network.json' - a.click() - }) - - /* settings (config) export / import -- separate from network data */ - document.getElementById('btnExportConfig').addEventListener('click', () => { - const blob = new Blob([JSON.stringify(getCurrentConfig(), null, 2)], { - type: 'application/json', - }) - const a = document.createElement('a') - a.href = URL.createObjectURL(blob) - a.download = 'norma-settings.json' - a.click() - }) - - document - .getElementById('btnImportConfigTrigger') - .addEventListener('click', () => document.getElementById('configFileInput').click()) - - document.getElementById('configFileInput').addEventListener('change', (e) => { - const file = e.target.files[0] - if (!file) return - const reader = new FileReader() - reader.onload = (ev) => { - try { - const parsed = JSON.parse(ev.target.result) - applyConfig(parsed) - } catch (err) { - alert('Could not read settings file: ' + err.message) - } - } - reader.readAsText(file) - e.target.value = '' - }) -} diff --git a/frontend/src/layouts/cose.worker.ts b/frontend/src/layouts/cose.worker.ts deleted file mode 100644 index 4419df6..0000000 --- a/frontend/src/layouts/cose.worker.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Cytoscape's cose layout, headless, off the main thread. See coseInWorker in run.ts. -import cytoscape from 'cytoscape' - -self.onmessage = (e: MessageEvent) => { - const { elements, style, opts } = e.data - try { - const cy = cytoscape({ headless: true, styleEnabled: true, elements, style }) - cy.elements() - .layout({ ...opts, animate: false }) - .run() - const positions: Record = {} - cy.nodes().forEach((n) => { - positions[n.id()] = n.position() - }) - self.postMessage({ positions }) - } catch (err) { - self.postMessage({ error: String(err) }) - } -} diff --git a/frontend/src/layouts/input.ts b/frontend/src/layouts/input.ts deleted file mode 100644 index 1188648..0000000 --- a/frontend/src/layouts/input.ts +++ /dev/null @@ -1,592 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { - EDGE_BLIND_LAYOUTS, - FR_SEED, - computeSubLayoutAsync, - normalizeSpacing, - targetNodeSpacing, -} from '../metrics' -import { S } from '../state' -import { UNGROUPED, effectiveGroupsFor } from '../network_state' -import { cy } from '../cy' -import { mulberry32 } from '../sample_data' -import { nextPaint } from './controls' - -/* ---------- layout input: what the strategies see ---------- */ -// Visible nodes, their active groups, and one weighted edge per node pair -// (parallel channels collapse, keeping the highest weight, as NORMA's -// simplify(edge.attr.comb = "max") does). Missing weights count as 1. -export function collectLayoutGraph() { - const channelOnly = document.getElementById('layoutOnActiveOnly').checked - const nodes = cy.nodes().filter((n) => !n.hasClass('hidden-group')) - const nodeIds = nodes.map((n) => n.id()) - const visible = new Set(nodeIds) - const groupsOf = {} - const groupMembers = new Map() - nodes.forEach((n) => { - const gs = effectiveGroupsFor(n).filter((g) => g !== UNGROUPED) - groupsOf[n.id()] = gs - gs.forEach((g) => { - if (!groupMembers.has(g)) groupMembers.set(g, []) - groupMembers.get(g).push(n.id()) - }) - }) - const pairs = new Map() - cy.edges().forEach((e) => { - if (channelOnly && !S.activeTypes.has(e.data('type'))) return - const s = e.data('source'), - t = e.data('target') - if (s === t || !visible.has(s) || !visible.has(t)) return - const key = s < t ? s + '\t' + t : t + '\t' + s - const w = typeof e.data('weight') === 'number' && e.data('weight') > 0 ? e.data('weight') : 1 - const p = pairs.get(key) - if (p) p.weight = Math.max(p.weight, w) - else pairs.set(key, { source: s, target: t, weight: w }) - }) - return { nodeIds, groupsOf, groupMembers, edges: [...pairs.values()] } -} - -function sharesGroup(a, b) { - return a.length > 0 && b.length > 0 && a.some((g) => b.includes(g)) -} - -// All-vs-all pairs inside a group. Very large groups get a sparse random -// subset per member instead of a full clique, which would otherwise add -// hundreds of thousands of layout-only edges. -const CLIQUE_FULL_LIMIT = 400 - -const CLIQUE_SPARSE_DEGREE = 60 - -function groupPairs(members, rand) { - const out = [] - if (members.length <= CLIQUE_FULL_LIMIT) { - for (let i = 0; i < members.length; i++) { - for (let j = i + 1; j < members.length; j++) out.push([members[i], members[j]]) - } - } else { - members.forEach((a) => { - for (let k = 0; k < CLIQUE_SPARSE_DEGREE; k++) { - const b = members[Math.floor(rand() * members.length)] - if (a !== b) out.push([a, b]) - } - }) - } - return out -} - -/* ============================================================ - NORMA-2.0 GROUP LAYOUT STRATEGIES - Karatzas et al. (2022) Bioinformatics Advances 2(1):vbac036, - Figure 1. Weights and steps follow NORMA's own implementation - (strategy1_virtualNodes, strategy2_gravity, strategy3_superNodes in - github.com/PavlopoulosLab/NORMA, functions/annotations.R). - ============================================================ */ - -// Strategy 1 -- virtual nodes (Fig. 1A). One hidden hub per group, tied to -// every member with a heavy edge (50); real edges get weight 5 and group -// members get light all-pairs edges (0.1). The chosen layout runs on this -// enlarged graph and the hubs are then dropped. -export async function strategyVirtualNodes(graph, layoutName) { - const rand = mulberry32(FR_SEED) - const ids = [...graph.nodeIds] - const edges = graph.edges.map((e) => ({ source: e.source, target: e.target, weight: 5 })) - graph.groupMembers.forEach((members, g) => { - const hub = '\u0000virtual:' + g - ids.push(hub) - if (members.length >= 2 && !EDGE_BLIND_LAYOUTS.has(layoutName)) { - groupPairs(members, rand).forEach(([a, b]) => - edges.push({ source: a, target: b, weight: 0.1 }) - ) - } - members.forEach((m) => edges.push({ source: hub, target: m, weight: 50 })) - }) - const pos = await computeSubLayoutAsync(ids, edges, layoutName) - if (!pos) return null - const real = {} - graph.nodeIds.forEach((id) => { - real[id] = pos[id] - }) - return normalizeSpacing(real, targetNodeSpacing()) -} - -// Strategy 2 -- group gravity (Fig. 1B). Groups become cliques with -// layout-only edges at the network's highest weight; real edges inside a -// group are multiplied by the force and edges that leave a group (or touch -// an ungrouped node) are divided by it. Parallel pairs keep the max. -export async function strategyGravity(graph, layoutName, force) { - const rand = mulberry32(FR_SEED) - const maxW = graph.edges.reduce((m, e) => Math.max(m, e.weight), 1) - const pairs = new Map() - const put = (a, b, w) => { - const key = a < b ? a + '\t' + b : b + '\t' + a - const p = pairs.get(key) - if (p) p.weight = Math.max(p.weight, w) - else pairs.set(key, { source: a, target: b, weight: w }) - } - graph.edges.forEach((e) => { - const inside = sharesGroup(graph.groupsOf[e.source], graph.groupsOf[e.target]) - put(e.source, e.target, inside ? e.weight * force : e.weight / force) - }) - if (!EDGE_BLIND_LAYOUTS.has(layoutName)) { - graph.groupMembers.forEach((members) => { - groupPairs(members, rand).forEach(([a, b]) => put(a, b, maxW)) - }) - } - const pos = await computeSubLayoutAsync(graph.nodeIds, [...pairs.values()], layoutName) - return pos ? normalizeSpacing(pos, targetNodeSpacing()) : null -} - -/* ---------- shared group placement ---------- - Radius a group's local layout gets, in pixels. A ring must fit every - member at the usual node spacing, so circles of big groups get a larger - radius than the area-based estimate used for the other local layouts. */ -function localGroupRadius(k, localName, spacing, clusterScale) { - const area = 0.95 * spacing * Math.sqrt(Math.max(1, k)) - const ring = localName === 'circle' ? (k * spacing) / (2 * Math.PI) : 0 - return Math.max(spacing / 2, area, ring) * clusterScale -} - -// Lays each group's members out with `localName`, scaled to `radii[g]` and -// centred on `centers[g]`. Returns { nodeId: [candidate positions] } so that -// a node in several groups can be averaged afterwards. -async function placeGroupsLocally(graph, groups, centers, radii, localName, force) { - const rand = mulberry32(FR_SEED) - const candidates = {} - const lp0 = S.layoutProgress - if (lp0) lp0.begin([...groups].reduce((a, [, m]) => a + m.length, 0)) - let lastPaint = performance.now() - for (const [g, members] of groups) { - if (lp0) lp0.part(members.length) - if (performance.now() - lastPaint > 60) { - await nextPaint() - lastPaint = performance.now() - } - const center = centers[g] - const radius = radii[g] - if (members.length === 1) { - ;(candidates[members[0]] = candidates[members[0]] || []).push({ x: center.x, y: center.y }) - continue - } - const memberSet = new Set(members) - const local = graph.edges - .filter((e) => memberSet.has(e.source) && memberSet.has(e.target)) - .map((e) => ({ ...e })) - if (!EDGE_BLIND_LAYOUTS.has(localName)) { - const minW = local.length ? local.reduce((m, e) => Math.min(m, e.weight), Infinity) : 1 - groupPairs(members, rand).forEach(([a, b]) => - local.push({ source: a, target: b, weight: minW / Math.max(1, force) }) - ) - } - const lp = await computeSubLayoutAsync(members, local, localName) - if (!lp) return null - let x1 = Infinity, - x2 = -Infinity, - y1 = Infinity, - y2 = -Infinity - members.forEach((id) => { - x1 = Math.min(x1, lp[id].x) - x2 = Math.max(x2, lp[id].x) - y1 = Math.min(y1, lp[id].y) - y2 = Math.max(y2, lp[id].y) - }) - const mx = (x1 + x2) / 2, - my = (y1 + y2) / 2 - // fit the farthest member onto the disc's edge, so every shape - // (square grids included) stays inside the group's disc - const reach = members.reduce( - (m, id) => Math.max(m, Math.hypot(lp[id].x - mx, lp[id].y - my)), - 0 - ) - const scale = reach > 1e-9 ? radius / reach : 0 - members.forEach((id) => { - ;(candidates[id] = candidates[id] || []).push({ - x: center.x + (lp[id].x - mx) * scale, - y: center.y + (lp[id].y - my) * scale, - }) - }) - } - return candidates -} - -function averageCandidates(candidates, fallback) { - const out = { ...(fallback || {}) } - Object.entries(candidates).forEach(([id, list]) => { - out[id] = { - x: list.reduce((a, p) => a + p.x, 0) / list.length, - y: list.reduce((a, p) => a + p.y, 0) / list.length, - } - }) - return out -} - -// Pushes discs apart until none overlap (keeping `gap` between them). -// Smaller discs give way more than big ones. With `compact`, discs are -// first drawn toward the middle while being kept apart, which packs -// scattered groups (for example unconnected ones) into a tidy cluster. -function resolveOverlaps(keys, centers, radii, gap, { compact = false } = {}) { - const n = keys.length - if (n < 2) return - const pullPasses = compact ? 250 : 0 - for (let pass = 0; pass < pullPasses + 400; pass++) { - if (pass < pullPasses) { - let mx = 0, - my = 0 - keys.forEach((k) => { - mx += centers[k].x - my += centers[k].y - }) - mx /= n - my /= n - const pull = 0.04 * (1 - pass / pullPasses) - keys.forEach((k) => { - centers[k].x += (mx - centers[k].x) * pull - centers[k].y += (my - centers[k].y) * pull - }) - } - let worst = 0 - for (let i = 0; i < n; i++) { - const a = centers[keys[i]], - ra = radii[keys[i]] - for (let j = i + 1; j < n; j++) { - const b = centers[keys[j]], - rb = radii[keys[j]] - let dx = b.x - a.x, - dy = b.y - a.y - let d = Math.hypot(dx, dy) - const overlap = ra + rb + gap - d - if (overlap <= 0) continue - if (d < 1e-6) { - dx = Math.cos(i * 2.399 + j) - dy = Math.sin(i * 2.399 + j) - d = 1 - } - const shareA = rb / (ra + rb), - shareB = ra / (ra + rb) - a.x -= (dx / d) * overlap * shareA - a.y -= (dy / d) * overlap * shareA - b.x += (dx / d) * overlap * shareB - b.y += (dy / d) * overlap * shareB - worst = Math.max(worst, overlap) - } - } - if (pass >= pullPasses && worst < 0.5) break - } -} - -// Places discs around a ring so neighbours are exactly far enough apart -// (straight-line distance, not arc length). Returns the ring radius. -function ringRadiusFor(radii, gap, minR) { - const needAngle = (R) => - radii.reduce((acc, r) => acc + 2 * Math.asin(Math.min(1, (r + gap / 2) / R)), 0) - let lo = Math.max(minR, Math.max(...radii) + gap / 2), - hi = lo - while (needAngle(hi) > 2 * Math.PI) hi *= 1.5 - if (needAngle(lo) <= 2 * Math.PI) return lo - for (let i = 0; i < 40; i++) { - const mid = (lo + hi) / 2 - if (needAngle(mid) > 2 * Math.PI) lo = mid - else hi = mid - } - return hi -} - -function placeOnRing(keys, radii, gap, R, centers, startAngle) { - const halves = keys.map((g) => Math.asin(Math.min(1, (radii[g] + gap / 2) / R))) - const used = halves.reduce((a, h) => a + 2 * h, 0) - const slack = keys.length ? Math.max(0, 2 * Math.PI - used) / keys.length : 0 - let angle = startAngle - keys.forEach((g, i) => { - angle += halves[i] + slack / 2 - centers[g] = { x: R * Math.cos(angle), y: R * Math.sin(angle) } - angle += halves[i] + slack / 2 - }) -} - -// The collapsed "group network": one node per group, linked when real -// edges cross between groups, weighted by how many do. -function groupSuperGraph(graph, groups) { - const memberOf = {} - groups.forEach(([g, members]) => - members.forEach((id) => (memberOf[id] = memberOf[id] || []).push(g)) - ) - const pairs = new Map() - graph.edges.forEach((e) => { - ;(memberOf[e.source] || []).forEach((a) => - (memberOf[e.target] || []).forEach((b) => { - if (a === b) return - const key = a < b ? a + '\t' + b : b + '\t' + a - const p = pairs.get(key) - if (p) p.count += 1 - else pairs.set(key, { source: a, target: b, count: 1 }) - }) - ) - }) - // log scale: a pair of groups with hundreds of links shouldn't collapse - return [...pairs.values()].map((p) => ({ - source: p.source, - target: p.target, - weight: 1 + Math.log(p.count), - })) -} - -/* ---------- group blocks ---------- - Each group (plus one block for nodes in no group) is a disc sized to its - local layout; the discs are arranged with the chosen scheme and never - overlap. `spacing` sets the gap between discs. */ -const UNGROUPED_BLOCK = '\u0000ungrouped' - -export async function blockGroupLayout(graph, arrangement, localName, force, clusterScale) { - const spacing = targetNodeSpacing() - const groups = [...graph.groupMembers] - const ungrouped = graph.nodeIds.filter((id) => !graph.groupsOf[id].length) - if (ungrouped.length) groups.push([UNGROUPED_BLOCK, ungrouped]) - const keys = groups.map(([g]) => g) - const radii = {} - groups.forEach(([g, m]) => { - radii[g] = localGroupRadius(m.length, localName, spacing, clusterScale) - }) - // room for the group shading (it extends beyond the outermost nodes) - const gap = spacing * 2.5 * (force / 10) - const centers = {} - - if (arrangement === 'circle') { - if (keys.length === 1) { - centers[keys[0]] = { x: 0, y: 0 } - } else { - const R = ringRadiusFor( - keys.map((g) => radii[g]), - gap, - 0 - ) - placeOnRing(keys, radii, gap, R, centers, -Math.PI / 2) - } - } else if (arrangement === 'grid') { - const cells = keys.map((g) => 2 * radii[g] + gap) - const area = cells.reduce((a, c) => a + c * c, 0) - const rowWidth = Math.max(Math.max(...cells), Math.sqrt(area) * 1.15) - let x = 0, - y = 0, - rowH = 0 - keys.forEach((g, i) => { - if (x > 0 && x + cells[i] > rowWidth) { - x = 0 - y += rowH - rowH = 0 - } - centers[g] = { x: x + cells[i] / 2, y: y + cells[i] / 2 } - x += cells[i] - rowH = Math.max(rowH, cells[i]) - }) - } else if (arrangement === 'concentric') { - // biggest group in the middle, then rings of the next biggest - const order = keys.slice().sort((x, y) => radii[y] - radii[x]) - centers[order[0]] = { x: 0, y: 0 } - let inner = radii[order[0]] - let i = 1, - ringIndex = 0 - while (i < order.length) { - const ringR = inner + gap + radii[order[i]] - const ring = [] - let usedAngle = 0 - while (i < order.length) { - const need = 2 * Math.asin(Math.min(1, (radii[order[i]] + gap / 2) / ringR)) - if (ring.length && usedAngle + need > 2 * Math.PI) break - ring.push(order[i]) - usedAngle += need - i++ - } - placeOnRing(ring, radii, gap, ringR, centers, ringIndex * 0.7) - inner = ringR + Math.max(...ring.map((g) => radii[g])) - ringIndex++ - } - } else if (arrangement === 'breadthfirst') { - // levels by distance in the group network from its best-connected group - const edges = groupSuperGraph(graph, groups) - const adj = {} - keys.forEach((g) => { - adj[g] = new Set() - }) - edges.forEach((e) => { - adj[e.source].add(e.target) - adj[e.target].add(e.source) - }) - const level = {} - const byDegree = keys.slice().sort((a, b) => adj[b].size - adj[a].size || radii[b] - radii[a]) - let depthBase = 0 - byDegree.forEach((root) => { - if (level[root] !== undefined) return - level[root] = depthBase - const queue = [root] - let maxDepth = depthBase - while (queue.length) { - const g = queue.shift() - adj[g].forEach((h) => { - if (level[h] === undefined) { - level[h] = level[g] + 1 - maxDepth = Math.max(maxDepth, level[h]) - queue.push(h) - } - }) - } - depthBase = maxDepth + 1 - }) - const rows = [] - keys.forEach((g) => { - ;(rows[level[g]] = rows[level[g]] || []).push(g) - }) - let y = 0 - rows.filter(Boolean).forEach((row) => { - const h = Math.max(...row.map((g) => 2 * radii[g])) + gap - const width = row.reduce((a, g) => a + 2 * radii[g] + gap, 0) - let x = -width / 2 - row.forEach((g) => { - centers[g] = { x: x + radii[g] + gap / 2, y: y + h / 2 } - x += 2 * radii[g] + gap - }) - y += h - }) - } else { - // force-directed arrangement of the group network, then de-overlap - const algorithm = arrangement === 'cose' ? 'cose' : 'fr' - const pos = - keys.length > 1 - ? await computeSubLayoutAsync(keys, groupSuperGraph(graph, groups), algorithm) - : { [keys[0]]: { x: 0, y: 0 } } - if (!pos) return null - const mx = keys.reduce((a, g) => a + pos[g].x, 0) / keys.length - const my = keys.reduce((a, g) => a + pos[g].y, 0) / keys.length - const spread = - keys.reduce((a, g) => a + Math.hypot(pos[g].x - mx, pos[g].y - my), 0) / keys.length || 1 - const target = keys.reduce((a, g) => a + radii[g] + gap / 2, 0) / Math.PI - keys.forEach((g) => { - centers[g] = { - x: ((pos[g].x - mx) / spread) * target, - y: ((pos[g].y - my) / spread) * target, - } - }) - resolveOverlaps(keys, centers, radii, gap, { compact: true }) - } - - const candidates = await placeGroupsLocally(graph, groups, centers, radii, localName, force) - return candidates ? averageCandidates(candidates) : null -} - -// Strategies 1 and 2 place every node themselves. With a local layout -// chosen, each group is then rearranged around where the strategy put it. -export async function applyLocalLayoutsAfter(graph, positions, localName, force, clusterScale) { - if (!positions || localName === 'keep') return positions - const spacing = targetNodeSpacing() - const groups = [...graph.groupMembers] - const keys = groups.map(([g]) => g) - const centers = {}, - radii = {} - groups.forEach(([g, members]) => { - centers[g] = { - x: members.reduce((a, id) => a + positions[id].x, 0) / members.length, - y: members.reduce((a, id) => a + positions[id].y, 0) / members.length, - } - radii[g] = localGroupRadius(members.length, localName, spacing, clusterScale) - }) - resolveOverlaps(keys, centers, radii, spacing * 1.5) - const candidates = await placeGroupsLocally(graph, groups, centers, radii, localName, force) - return candidates ? averageCandidates(candidates, positions) : null -} - -// Strategy 3 -- super nodes (Fig. 1C). Each group collapses into one -// super-node; ungrouped nodes stay as themselves. The chosen algorithm runs -// on that small super-network, the result is pushed outward by the force, -// and each group's members are then laid out locally (with light all-pairs -// edges, min weight / force) around their super-node. A node in several -// groups is placed at the average of its positions. -export async function strategySuperNodes(graph, globalName, localName, force, clusterScale) { - const spacing = targetNodeSpacing() - const superOf = (id) => - graph.groupsOf[id].length ? graph.groupsOf[id].map((g) => 'g\u0000' + g) : ['n\u0000' + id] - - const superIds = new Set() - graph.nodeIds.forEach((id) => superOf(id).forEach((s) => superIds.add(s))) - const superPairs = new Map() - graph.edges.forEach((e) => { - superOf(e.source).forEach((a) => - superOf(e.target).forEach((b) => { - if (a === b) return - const key = a < b ? a + '\t' + b : b + '\t' + a - const p = superPairs.get(key) - if (p) p.weight = Math.max(p.weight, e.weight) - else superPairs.set(key, { source: a, target: b, weight: e.weight }) - }) - ) - }) - const superList = [...superIds] - const superPos = await computeSubLayoutAsync(superList, [...superPairs.values()], globalName) - if (!superPos) return null - - const radiusOf = (s) => - s.startsWith('n\u0000') - ? spacing / 2 - : localGroupRadius( - graph.groupMembers.get(s.slice(2)).length, - localName, - spacing, - clusterScale - ) - const cx = superList.reduce((a, s) => a + superPos[s].x, 0) / superList.length - const cyc = superList.reduce((a, s) => a + superPos[s].y, 0) / superList.length - let normalize = 1 - if (superList.length > 1) { - const radii = superList.map(radiusOf) - const ring = radii.reduce((a, r) => a + r, 0) / Math.PI - const disk = (2 / 3) * Math.sqrt(2 * radii.reduce((a, r) => a + r * r, 0)) - const targetSpread = Math.min(ring, disk) - const meanSpread = - superList.reduce((a, s) => a + Math.hypot(superPos[s].x - cx, superPos[s].y - cyc), 0) / - superList.length - if (meanSpread > 1e-9) { - const spreadFactor = targetSpread / meanSpread - const groupSupers = superList.filter((s) => s.startsWith('g\u0000')) - let pairFactor = 0 - for (let i = 0; i < groupSupers.length; i++) { - for (let j = i + 1; j < groupSupers.length; j++) { - const a = superPos[groupSupers[i]], - b = superPos[groupSupers[j]] - const d = Math.hypot(a.x - b.x, a.y - b.y) - if (d > 1e-9) - pairFactor = Math.max( - pairFactor, - (radiusOf(groupSupers[i]) + radiusOf(groupSupers[j])) / d - ) - } - } - normalize = Math.min(Math.max(spreadFactor, pairFactor), spreadFactor * 2.5) - } else { - superList.forEach((s, i) => { - const angle = (2 * Math.PI * i) / superList.length - superPos[s] = { x: cx + Math.cos(angle), y: cyc + Math.sin(angle) } - }) - normalize = targetSpread - } - } - // NORMA multiplies the super-node coordinates by the force (default 10); - // here force 10 gives 1.6x the "just touching" arrangement. - const repel = normalize * force * 0.16 - const placed = {} - superList.forEach((s) => { - placed[s] = { x: (superPos[s].x - cx) * repel, y: (superPos[s].y - cyc) * repel } - }) - - const groups = [...graph.groupMembers] - const centers = {}, - radii = {} - groups.forEach(([g]) => { - centers[g] = placed['g\u0000' + g] - radii[g] = radiusOf('g\u0000' + g) - }) - const candidates = await placeGroupsLocally(graph, groups, centers, radii, localName, force) - if (!candidates) return null - const ungrouped = {} - graph.nodeIds.forEach((id) => { - if (!graph.groupsOf[id].length) ungrouped[id] = placed['n\u0000' + id] - }) - return averageCandidates(candidates, ungrouped) -} diff --git a/frontend/src/layouts/run.ts b/frontend/src/layouts/run.ts deleted file mode 100644 index b288679..0000000 --- a/frontend/src/layouts/run.ts +++ /dev/null @@ -1,260 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { - applyLocalLayoutsAfter, - blockGroupLayout, - collectLayoutGraph, - strategyGravity, - strategySuperNodes, - strategyVirtualNodes, -} from './input' -import { - applyPositions, - cancelFrJobs, - computeSubLayoutAsync, - layoutOptsFor, - makeLayoutProgress, - normalizeSpacing, - shownEdges, - shownNodes, - targetNodeSpacing, -} from '../metrics' -import { cy } from '../cy' -import { setStatus } from './controls' -import CoseWorker from './cose.worker?worker&inline' - -/* ---------- running layouts ---------- */ -export const STRATEGIES = new Set(['virtual', 'gravity', 'supernodes']) - -export function layoutMode() { - return document.getElementById('layoutMode').value === 'groups' ? 'groups' : 'connections' -} - -export function groupArrangement() { - return document.getElementById('groupArrangement').value -} - -export function setLayoutBusy(busy) { - document.getElementById('btnRunLayout').disabled = busy - setStatus( - 'layoutStatus', - busy ? [{ level: 'busy', text: 'Computing layout…', progress: null }] : [] - ) -} - -async function runComputedLayout(compute) { - cancelFrJobs() - cancelCoseJob() - const run = ++S.layoutRunSeq - const nodeCount = cy.nodes().length - setLayoutBusy(true) - S.layoutProgress = makeLayoutProgress((f) => { - if (run === S.layoutRunSeq) - setStatus('layoutStatus', [{ level: 'busy', text: 'Computing layout…', progress: f }]) - }) - // let the busy note paint before any work on the page starts - await new Promise((r) => setTimeout(r, 0)) - try { - const positions = await compute() - if (run !== S.layoutRunSeq || cy.nodes().length !== nodeCount) return - if (positions) applyPositions(positions) - } catch (err) { - if (run === S.layoutRunSeq) - setStatus('layoutStatus', [ - { level: 'error', text: `The layout couldn't be computed: ${err.message}` }, - ]) - return - } finally { - if (run === S.layoutRunSeq) { - document.getElementById('btnRunLayout').disabled = false - S.layoutProgress = null - } - } - if (run === S.layoutRunSeq) setStatus('layoutStatus', []) -} - -// Layout by connections: groups play no part. -export function runLayout(name) { - if (name === 'fr' || name === 'kk' || name === 'stress') { - runComputedLayout(async () => { - const graph = collectLayoutGraph() - if (!graph.nodeIds.length) return null - const pos = await computeSubLayoutAsync(graph.nodeIds, graph.edges, name) - return pos ? normalizeSpacing(pos, targetNodeSpacing()) : null - }) - return - } - // a Cytoscape layout replaces any computed one still running - cancelFrJobs() - cancelCoseJob() - S.layoutRunSeq++ - setLayoutBusy(false) - // Animating every force-directed iteration is slow on bigger graphs; - // there, compute first and animate only to the final positions. - const nodes = shownNodes() - const animate = name === 'cose' && nodes.length > 150 ? 'end' : true - const opts = { ...layoutOptsFor(name), animate, animationDuration: 500, padding: 50 } - - // Only the shown part is arranged: nodes of ticked groups and, with - // "Use checked channels only", edges of ticked channels. Hidden nodes - // keep their positions. Shown nodes without a counted edge still take - // part (repulsion only). - const channelOnly = document.getElementById('layoutOnActiveOnly').checked - if (!nodes.length) return - const eles = nodes.union(shownEdges(channelOnly)) - if (animate !== 'end') { - eles.layout(opts).run() - return - } - // Computed in one go, in a worker, then animated to the final positions. - // Once the page has painted a frame, Cytoscape's cose runs 5-7x slower on - // the main thread (a V8 effect, reproducible with plain Cytoscape); a - // worker is a fresh isolate, so it stays fast and the page stays usable. - const run = S.layoutRunSeq - document.getElementById('btnRunLayout').disabled = true - setStatus('layoutStatus', [{ level: 'busy', text: 'Computing layout…', progress: null }]) - const finish = () => { - if (run !== S.layoutRunSeq) return - document.getElementById('btnRunLayout').disabled = false - setStatus('layoutStatus', []) - } - coseInWorker(eles, opts) - .then((positions) => { - if (run !== S.layoutRunSeq) return - if (positions) { - eles - .nodes() - .layout({ - name: 'preset', - positions, - fit: true, - padding: opts.padding, - animate: true, - animationDuration: opts.animationDuration, - }) - .run() - } else eles.layout(opts).run() // no worker (very old browser): as before - }) - .finally(finish) -} - -/* ---------- cose in a worker ---------- */ -let coseWorker = null -let coseJob = null // { resolve } of the run in progress - -function coseInWorker(eles, opts) { - cancelCoseJob() - if (!coseWorker) { - try { - coseWorker = new CoseWorker() - } catch (e) { - return Promise.resolve(null) - } - coseWorker.onmessage = (e) => { - const job = coseJob - coseJob = null - if (job) job.resolve(e.data.error ? null : e.data.positions) - } - coseWorker.onerror = (e) => { - e.preventDefault() - const job = coseJob - coseJob = null - coseWorker = null - if (job) job.resolve(null) - } - } - // what cose reads: node ids, positions and outer sizes, edges, the viewport - const elements = eles.nodes().map((n) => ({ - data: { id: n.id(), w: n.outerWidth(), h: n.outerHeight() }, - position: n.position(), - })) - eles - .edges() - .forEach((e) => - elements.push({ data: { id: e.id(), source: e.source().id(), target: e.target().id() } }) - ) - const { animate, animationDuration, ...rest } = opts - const layout = { ...rest, boundingBox: { x1: 0, y1: 0, w: cy.width(), h: cy.height() } } - return new Promise((resolve) => { - coseJob = { resolve } - coseWorker.postMessage({ - elements, - style: [{ selector: 'node', style: { width: 'data(w)', height: 'data(h)' } }], - opts: layout, - }) - }) -} - -// Stops a cose computation still running in the worker; its caller sees null. -export function cancelCoseJob() { - if (!coseJob) return - const job = coseJob - coseJob = null - coseWorker.terminate() - coseWorker = null - job.resolve(null) -} - -// Layout by groups: block arrangements or a NORMA-2.0 strategy, each -// followed by the chosen layout inside every group. -export function runGroupLayout() { - const graph = collectLayoutGraph() - if (!graph.nodeIds.length) return - if (!graph.groupMembers.size) { - setStatus('layoutStatus', [ - { - level: 'warn', - text: 'This view has no active groups to arrange, so the nodes were arranged by their connections instead.', - }, - ]) - runLayout('fr') - return - } - const arrangement = groupArrangement() - const algorithm = document.getElementById('strategyAlgorithm').value - const force = parseFloat(document.getElementById('groupForce').value) || 10 - const clusterScale = parseFloat(document.getElementById('groupClusterRadius').value) || 1 - let localName = document.getElementById('localGroupLayout').value - const keepAllowed = arrangement === 'virtual' || arrangement === 'gravity' - if (localName === 'keep' && !keepAllowed) localName = 'circle' - runComputedLayout(async () => { - if (arrangement === 'virtual') { - return applyLocalLayoutsAfter( - graph, - await strategyVirtualNodes(graph, algorithm), - localName, - force, - clusterScale - ) - } - if (arrangement === 'gravity') { - return applyLocalLayoutsAfter( - graph, - await strategyGravity(graph, algorithm, force), - localName, - force, - clusterScale - ) - } - if (arrangement === 'supernodes') { - return strategySuperNodes(graph, algorithm, localName, force, clusterScale) - } - return blockGroupLayout(graph, arrangement, localName, force, clusterScale) - }) -} - -// Runs whichever layout mode is active. -export function runActiveLayout() { - if (layoutMode() === 'groups') runGroupLayout() - else runLayout(document.getElementById('layoutSelect').value) -} - -// Older names still called elsewhere (loading data, switching annotations). -export function runStrategyLayout() { - runActiveLayout() -} - -function runGroupedLayout() { - runActiveLayout() -} diff --git a/frontend/src/library.ts b/frontend/src/library.ts deleted file mode 100644 index a011816..0000000 --- a/frontend/src/library.ts +++ /dev/null @@ -1,298 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { - UNGROUPED, - UNGROUPED_LABEL, - buildAttrSchema, - colorForGroup, - getUsedGroups, - renderAttrSchema, - setGroupOrder, -} from './network_state' -import { - annotationText, - downloadText, - fileStem, - libEntry, - libSelection, - listSample, - plural, - selectedNetworks, - selectionKey, - setStatus, - updateRefreshState, -} from './layouts/controls' -import { applyGroupVisibility, buildGroupLegend, drawGroupHulls, loadData } from './hulls' -import { applyValueColors } from './clustering/mapping' -import { bumpDataVersion, noteChange } from './demo_downloads' -import { cy } from './cy' -import { hideEdgePopup, hideInfo } from './profiler' -import { layoutMode, runGroupLayout } from './layouts/run' -import { nextLoadPositions } from './api/tester' -import { updateContextInfo } from './recording' - -/* ---------- combining library files into a view ---------- */ -// Merges the chosen networks into one node/edge set. With one network its -// edges keep their Type (or the network name) as channel; with several, -// every network is its own channel ("name" or "name: type"), so pairs -// found in more than one network become parallel edges. -function combineNetworks(netEntries) { - const multi = netEntries.length > 1 - const nodeOrder = [] - const nodeNetworks = new Map() - const edges = [] - netEntries.forEach((ne) => { - ne.parsed.nodes.forEach((id) => { - if (!nodeNetworks.has(id)) { - nodeNetworks.set(id, []) - nodeOrder.push(id) - } - nodeNetworks.get(id).push(ne.name) - }) - ne.parsed.edges.forEach((e, i) => { - const type = e.type ? (multi ? `${ne.name}: ${e.type}` : e.type) : ne.name - edges.push({ - id: `${ne.id}-${i}`, - source: e.source, - target: e.target, - type, - ...(e.weight !== undefined ? { weight: e.weight } : {}), - ...(e.directed ? { directed: true } : {}), - ...(multi ? { network: ne.name } : {}), - }) - }) - }) - return { nodeOrder, nodeNetworks, edges, multi } -} - -// Resolves an annotation and an expression file against a set of node ids. -function resolveGroupsAndColors(nodeIds, viewName, annEntry, colEntry) { - const nodeSet = new Set(nodeIds) - const notes = [] - const groupsOf = {} - let groupCount = 0 - let groupOrder = null - - if (annEntry) { - const missing = new Set() - const emptied = [] - const kept = [] - annEntry.parsed.groups.forEach(({ name, members }) => { - const present = members.filter((m) => nodeSet.has(m)) - members.forEach((m) => { - if (!nodeSet.has(m)) missing.add(m) - }) - if (!present.length) { - emptied.push(name) - return - } - kept.push({ name, members: present }) - present.forEach((m) => (groupsOf[m] = groupsOf[m] || []).push(name)) - }) - groupCount = kept.length - groupOrder = kept.map((g) => g.name) - if (!kept.length) { - notes.push({ - level: 'error', - text: `None of the names in "${annEntry.name}" are in ${viewName}, so no groups are shown. Check that the files belong together.`, - }) - } else { - if (missing.size || emptied.length) { - const parts = [] - if (missing.size) - parts.push( - `Dropped ${plural(missing.size, 'annotation name')} not found in ${viewName}: ${listSample([...missing])}.` - ) - if (emptied.length) - parts.push( - `Left out ${plural(emptied.length, 'group')} with no nodes in the view: ${listSample(emptied)}.` - ) - const text = annotationText(kept) - notes.push({ - level: 'warn', - text: parts.join(' '), - action: { - label: 'Download corrected annotation', - run: () => downloadText(`${fileStem(annEntry.name)}-corrected.txt`, text), - }, - }) - } - const ungrouped = nodeIds.filter((n) => !groupsOf[n]).length - if (ungrouped) - notes.push({ - level: 'ok', - text: `${plural(ungrouped, 'node is', 'nodes are')} in no group and listed as "${UNGROUPED_LABEL}".`, - }) - } - } - - const colorOf = new Map() - const valuesOf = new Map() - if (colEntry && colEntry.parsed.numeric) { - const unknown = [] - colEntry.parsed.values.forEach((row, node) => { - if (nodeSet.has(node)) valuesOf.set(node, row) - else unknown.push(node) - }) - if (unknown.length) - notes.push({ - level: 'warn', - text: `Ignored values for ${plural(unknown.length, 'node')} not found in ${viewName}: ${listSample(unknown)}.`, - }) - const without = nodeIds.length - valuesOf.size - if (without) - notes.push({ - level: 'ok', - text: `${plural(without, 'node has', 'nodes have')} no value and ${without === 1 ? 'is' : 'are'} drawn in the "no value" color.`, - }) - } else if (colEntry) { - const unknown = [] - colEntry.parsed.colors.forEach((color, node) => { - if (nodeSet.has(node)) colorOf.set(node, color) - else unknown.push(node) - }) - if (unknown.length) - notes.push({ - level: 'warn', - text: `Ignored colors for ${plural(unknown.length, 'node')} not found in ${viewName}: ${listSample(unknown)}.`, - }) - const uncolored = nodeIds.length - colorOf.size - if (uncolored) - notes.push({ - level: 'ok', - text: `${plural(uncolored, 'node has', 'nodes have')} no expression color and ${uncolored === 1 ? 'is' : 'are'} gray.`, - }) - } - return { groupsOf, colorOf, valuesOf, notes, groupCount, groupOrder } -} - -// NORMA's look: node fill from expression colors with groups shaded behind; -// without an expression file, fill nodes with their group colors. -function applyLibraryDisplayDefaults(annEntry, colEntry) { - document.getElementById('nodeFillSelect').value = colEntry - ? colEntry.parsed.numeric - ? 'values' - : 'data' - : 'groups' - if (colEntry && annEntry) { - document.getElementById('showGroupHulls').checked = true - document.getElementById('hullControls').style.display = 'block' - } -} - -// Changes groups and colors of the nodes already on screen, keeping their -// positions (unless grouped layout is on, which depends on the groups). -export function applyGroupsAndColorsInPlace(groupsOf, colorOf, groupOrder, meta) { - bumpDataVersion() - S.nodeColorMap = {} - S.groupAttrs = meta ? JSON.parse(JSON.stringify(meta)) : {} - setGroupOrder(groupOrder) - let anyUngrouped = false - cy.batch(() => { - cy.nodes().forEach((n) => { - const own = groupsOf[n.id()] - const groups = own && own.length ? own : [UNGROUPED] - if (!own || !own.length) anyUngrouped = true - n.data({ groups, group: groups.join(', '), nodeColor: colorOf.get(n.id()) || null }) - }) - }) - if (anyUngrouped) - S.groupAttrs[UNGROUPED] = { - label: UNGROUPED_LABEL, - description: 'Nodes that no group lists as a member', - } - const used = getUsedGroups() - used.forEach((g) => colorForGroup(g)) - S.activeGroups = new Set(used) - buildGroupLegend() - applyGroupVisibility() - buildAttrSchema() - renderAttrSchema() - hideInfo() - hideEdgePopup() - cy.elements().removeClass('dimmed highlighted') - if (layoutMode() === 'groups') runGroupLayout() - drawGroupHulls() -} - -export function refreshLibraryView() { - noteChange() - const nets = selectedNetworks() - if (!nets.length) { - setStatus('normaStatus', [ - { - level: 'error', - text: 'Tick at least one network, then show it in this view or open it in a new one.', - }, - ]) - return - } - const annEntry = libEntry('annotation', libSelection.annotation) - const colEntry = libEntry('colors', libSelection.colors) - const key = selectionKey() - const sameNetworks = - S.currentLibView && S.currentLibView.nets === key.nets && cy.nodes().length > 0 - - const combined = combineNetworks(nets) - const viewName = nets.length === 1 ? `"${nets[0].name}"` : 'the selected networks' - const { groupsOf, colorOf, valuesOf, notes, groupCount, groupOrder } = resolveGroupsAndColors( - combined.nodeOrder, - viewName, - annEntry, - colEntry - ) - applyLibraryDisplayDefaults(annEntry, colEntry) - - // extra details carried by library entries (e.g. from STRING) - const groupMeta = annEntry && annEntry.groupMeta ? annEntry.groupMeta : null - const nodeExtra = (id) => { - const out = {} - nets.forEach((e) => { - if (e.nodeAttrs && e.nodeAttrs[id]) Object.assign(out, e.nodeAttrs[id]) - }) - return out - } - const keptPositions = nets.length === 1 && nets[0].nodePositions ? nets[0].nodePositions : null - if (sameNetworks) { - cy.batch(() => cy.nodes().forEach((n) => n.data('values', valuesOf.get(n.id()) || null))) - applyGroupsAndColorsInPlace(groupsOf, colorOf, groupOrder, groupMeta) - applyValueColors() - } else { - loadData( - { - groupOrder, - ...(groupMeta ? { groupAttrs: JSON.parse(JSON.stringify(groupMeta)) } : {}), - nodes: combined.nodeOrder.map((id) => ({ - ...nodeExtra(id), - id, - groups: groupsOf[id] || [], - ...(colorOf.has(id) ? { color: colorOf.get(id) } : {}), - ...(valuesOf.has(id) ? { values: valuesOf.get(id) } : {}), - ...(combined.multi ? { networks: combined.nodeNetworks.get(id) } : {}), - })), - edges: combined.edges, - }, - nextLoadPositions || keptPositions ? { positions: nextLoadPositions || keptPositions } : {} - ) - } - S.currentLibView = key - updateRefreshState() - updateContextInfo() - - const shownNets = - nets.length === 1 - ? `"${nets[0].name}" (${nets[0].parsed.summary})` - : `${nets.length} networks overlaid as channels: ${combined.nodeOrder.length} nodes, ${combined.edges.length} edges` - const parts = [`Showing ${shownNets}`] - if (annEntry) parts.push(`${plural(groupCount, 'group')} from "${annEntry.name}"`) - if (colEntry) parts.push(`colors from "${colEntry.name}"`) - const lead = (sameNetworks ? 'Updated groups and colors in place. ' : '') + parts.join(', ') + '.' - if (groupCount > 30 && document.getElementById('showGroupHulls').checked) { - notes.push({ - level: 'ok', - text: `With ${groupCount} groups shaded at once the view gets crowded. Type in the Node groups filter and use "Deactivate matches" / "Activate matches" to focus on a few.`, - }) - } - setStatus('normaStatus', [{ level: 'ok', text: lead }, ...notes]) -} diff --git a/frontend/src/main.ts b/frontend/src/main.ts deleted file mode 100644 index 03c6477..0000000 --- a/frontend/src/main.ts +++ /dev/null @@ -1,182 +0,0 @@ -// @ts-nocheck -// ponytail: boot order of the former single-file script; typed later with the rest -import { NORMA_CFG } from './config' -import { NORMA_KINDS, normaLibrary, renderLibraryLists, updateStrategyUI } from './layouts/controls' -import { S } from './state' -import { TAB_VIEWS, currentTab, switchTab } from './wiring' -import { - activeView, - applyEdgeCurveStyle, - applyEdgeOpacity, - applyEdgeWidth, - renderViewBar, - updateSpreadReadout, - updateZoomReadout, - viewSettings, - views, -} from './profiler' -import { apiAnnounceReady, apiFromLocation } from './api/wiring' -import { applyEdgeDirection } from './export/dialog' -import { applyTheme } from './themes' -import { askDelete, renderSiteInfo } from './api/tester' -import { cy } from './cy' -import { net3d } from './view3d/state' -import { resizeHullCanvas } from './hulls' -import { setHistoryBaseline, updateUndoButtons } from './demo_downloads' -import { updateEmptyState } from './welcome' -import { updateStats } from './metrics' - -import './style.css' -import './state' -import './config' -import './palette' -import './themes' -import './sample_data' -import './cy' -import './network_state' -import { init as initHulls } from './hulls' -import './parallel_edges' -import './metrics' -import './layouts/input' -import './layouts/run' -import { init as initLayoutsControls } from './layouts/controls' -import './library' -import './uploads' -import './examples' -import './export_norma' -import { init as initWiring } from './wiring' -import { init as initProfiler } from './profiler' -import { init as initViewbar } from './viewbar' -import { init as initSideTabs } from './side_tabs' -import './directed_stats' -import { init as initDemoDownloads } from './demo_downloads' -import { init as initRecording } from './recording' -import { init as initLabelColors } from './label_colors' -import { init as initExportDraw } from './export/draw' -import './export/shading' -import './export/raster' -import './export/svg' -import { init as initExportDialog } from './export/dialog' -import { init as initView3dState } from './view3d/state' -import './view3d/cache' -import './view3d/camera' -import './view3d/painters' -import './view3d/draw' -import './view3d/layouts' -import './view3d/tab' -import { init as initView3dInput } from './view3d/input' -import './view3d/export' -import './string/requests' -import './string/ui_state' -import './string/import' -import './string/groupings' -import { init as initStringWiring } from './string/wiring' -import { init as initWelcome } from './welcome' -import './clustering/leiden' -import './clustering/label_propagation' -import './clustering/walktrap' -import './clustering/mcl' -import { init as initClusteringMapping } from './clustering/mapping' -import './benchmark' -import { init as initClusteringWiring } from './clustering/wiring' -import { init as initContours } from './contours' -import './arena3d' -import './group_network' -import { init as initEnrichment } from './enrichment' -import { init as initApiWiring } from './api/wiring' -import { init as initApiTester } from './api/tester' -import { init as initSidePanel } from './side_panel' - -initHulls() -initLayoutsControls() -initWiring() -initProfiler() -initViewbar() -initSideTabs() -initDemoDownloads() -initRecording() -initLabelColors() -initExportDraw() -initExportDialog() -initView3dState() -initView3dInput() -initStringWiring() -initWelcome() -initClusteringMapping() -initClusteringWiring() -initContours() -initEnrichment() -initApiWiring() -initApiTester() - -document.getElementById('btnViewDelete').addEventListener('click', () => { - const lv = S.currentLibView - const items = [] - if (lv) { - String(lv.nets || '') - .split('|') - .filter(Boolean) - .forEach((id) => items.push({ kind: 'network', id })) - if (lv.annotation) items.push({ kind: 'annotation', id: lv.annotation }) - if (lv.colors) items.push({ kind: 'colors', id: lv.colors }) - } - askDelete(items, { view: activeView() }) -}) -document.getElementById('btnGroupingDelete').addEventListener('click', () => { - const id = S.currentLibView && S.currentLibView.annotation - if (id) askDelete([{ kind: 'annotation', id }]) -}) -document.getElementById('btnDeleteAllFiles').addEventListener('click', () => { - askDelete( - NORMA_KINDS.flatMap((kind) => normaLibrary[kind].map((e) => ({ kind, id: e.id }))), - { all: true } - ) -}) -/* boot */ -applyTheme(NORMA_CFG.app.theme || 'white') -if ( - document.getElementById('themeSelect') && - [...document.getElementById('themeSelect').options].some((o) => o.value === NORMA_CFG.app.theme) -) - document.getElementById('themeSelect').value = NORMA_CFG.app.theme -applyEdgeCurveStyle() -applyEdgeWidth() -applyEdgeOpacity() -applyEdgeDirection() -updateSpreadReadout() -initSidePanel() - -resizeHullCanvas() -updateStrategyUI() -updateZoomReadout() -renderLibraryLists() -S.DEFAULT_VIEW_CONFIG = viewSettings() -// NORMA starts with an empty view and the welcome page -views.push({ - id: 'view' + ++S.viewSeq, - name: 'Untitled view', - autoName: true, - data: null, - selection: { networks: [], annotation: '', colors: '' }, - state: { config: { ...S.DEFAULT_VIEW_CONFIG } }, -}) -S.activeViewId = views[0].id -updateStats() -renderViewBar() -updateEmptyState() -switchTab(TAB_VIEWS[NORMA_CFG.app.startTab] ? NORMA_CFG.app.startTab : 'welcome') -renderSiteInfo() -// the API: data in the address, and a hello to a page that opened NORMA -apiFromLocation() -apiAnnounceReady() -setHistoryBaseline() -updateUndoButtons() -// test hook: the canvas is opaque to the DOM, so end-to-end tests read state here -window.__norma = { - cy, - net3d, - views, - get currentTab() { - return currentTab - }, -} diff --git a/frontend/src/metrics.ts b/frontend/src/metrics.ts deleted file mode 100644 index 7f8085f..0000000 --- a/frontend/src/metrics.ts +++ /dev/null @@ -1,1338 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import cytoscape from 'cytoscape' -import { S } from './state' -import { UNGROUPED, getUsedGroups } from './network_state' -import { - anyEdgeDirected, - directionMode, - directionOf, - edgeIsDirected, - fr3dLayout, -} from './export/dialog' -import { applyBundleResult, applyLabelSizes } from './profiler' -import { cy } from './cy' -import { mulberry32 } from './sample_data' -import { scheduleLegend, scheduleSeparation, valueForSize } from './clustering/mapping' -import { updateContextInfo } from './recording' - -/* ---------- centrality metrics ---------- */ - -// Computes a raw value per node for the given metric, over either the whole -// graph or just the currently active (checked) edge channels. -// directed = true: out-neighbours only for directed edges (undirected -// edges still count both ways) -function buildAdjacency(activeEdges, nodes, directed = false) { - const adjSet = {} - nodes.forEach((n) => { - adjSet[n.id()] = new Set() - }) - activeEdges.forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s === t || !adjSet[s] || !adjSet[t]) return - adjSet[s].add(t) - if (!directed || !edgeIsDirected(e)) adjSet[t].add(s) - }) - const adj = {} - Object.keys(adjSet).forEach((k) => { - adj[k] = [...adjSet[k]] - }) - return adj -} - -// Runs a single unweighted BFS-based pass (Brandes' algorithm) to get both -// betweenness and closeness together, since they share the same shortest-path work. -function bfsCentralities(nodeIds, adj, directed = false) { - const betweenness = {} - const closenessSum = {} - const reached = {} - nodeIds.forEach((id) => { - betweenness[id] = 0 - closenessSum[id] = 0 - reached[id] = 0 - }) - - nodeIds.forEach((s) => { - const P = {}, - sigma = {}, - d = {} - nodeIds.forEach((v) => { - P[v] = [] - sigma[v] = 0 - d[v] = -1 - }) - sigma[s] = 1 - d[s] = 0 - const S = [] - const queue = [s] - while (queue.length) { - const v = queue.shift() - S.push(v) - ;(adj[v] || []).forEach((w) => { - if (d[w] < 0) { - d[w] = d[v] + 1 - queue.push(w) - } - if (d[w] === d[v] + 1) { - sigma[w] += sigma[v] - P[w].push(v) - } - }) - } - nodeIds.forEach((v) => { - if (d[v] > 0) { - closenessSum[s] += d[v] - reached[s]++ - } - }) - const delta = {} - nodeIds.forEach((v) => (delta[v] = 0)) - while (S.length) { - const w = S.pop() - P[w].forEach((v) => { - delta[v] += (sigma[v] / sigma[w]) * (1 + delta[w]) - }) - if (w !== s) betweenness[w] += delta[w] - } - }) - // each shortest path is counted from both endpoints in an undirected graph - if (!directed) - nodeIds.forEach((v) => { - betweenness[v] /= 2 - }) - - const closeness = {} - nodeIds.forEach((v) => { - closeness[v] = closenessSum[v] > 0 ? reached[v] / closenessSum[v] : 0 - }) - - return { betweenness, closeness } -} - -// Self-contained (no reliance on cytoscape's own centrality helpers, which -// resolve neighborhoods against the full core graph rather than a given -// subset) so "compute on selected channels only" is honored exactly. -/* ============================================================ - THE SHOWN PART OF A VIEW - NORMA works on what is ticked: nodes in at least one ticked group, - and edges of ticked channels whose two ends are shown. Layouts, node - sizing, node statistics, the Profiler, Compare, search, bundling, - group shading and (by default) exports all use this part only. - ============================================================ */ -export function shownNodes() { - return cy.nodes().filter((n) => !n.hasClass('hidden-group')) -} - -export function shownEdges(channelOnly = true) { - return cy - .edges() - .filter( - (e) => - (!channelOnly || S.activeTypes.has(e.data('type'))) && - !e.source().hasClass('hidden-group') && - !e.target().hasClass('hidden-group') - ) -} - -export function exportShownOnly() { - const el = document.getElementById('exportShownOnly') - return !el || el.checked -} - -export function exportNodes() { - return exportShownOnly() ? shownNodes() : cy.nodes() -} - -export function exportEdges() { - return exportShownOnly() ? shownEdges(true) : cy.edges() -} - -export function computeMetricValues(metric, channelOnly) { - const activeEdges = shownEdges(channelOnly) - const nodes = shownNodes() - const nodeIds = nodes.map((n) => n.id()) - const values = {} - - if (metric === 'value') { - nodes.forEach((n) => { - values[n.id()] = valueForSize(n) - }) - return values - } - if (metric === 'degree' || metric === 'indegree' || metric === 'outdegree') { - nodeIds.forEach((id) => { - values[id] = 0 - }) - const add = (id) => { - if (id in values) values[id]++ - } - activeEdges.forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (metric === 'degree' || !edgeIsDirected(e)) { - add(s) - add(t) - return - } - if (metric === 'outdegree') add(s) - else add(t) - }) - return values - } - - // shortest paths follow edge direction when the shown part has any - // directed edge; clustering always ignores direction - const directed = metric !== 'clustering' && anyEdgeDirected(activeEdges) - const adj = buildAdjacency(activeEdges, nodes, directed) - - if (metric === 'clustering') { - nodeIds.forEach((id) => { - const neigh = adj[id] - const k = neigh.length - if (k < 2) { - values[id] = 0 - return - } - let links = 0 - for (let i = 0; i < neigh.length; i++) { - for (let j = i + 1; j < neigh.length; j++) { - if (adj[neigh[i]].includes(neigh[j])) links++ - } - } - values[id] = links / ((k * (k - 1)) / 2) - }) - return values - } - - if (metric === 'betweenness' || metric === 'closeness') { - const { betweenness, closeness } = bfsCentralities(nodeIds, adj, directed) - return metric === 'betweenness' ? betweenness : closeness - } - - return values -} - -// Cache of all four metrics computed on the full graph, used by the node info -// popup. Computed lazily (on first node click after a load) rather than -// eagerly on every data load: betweenness/closeness are O(V*(V+E)), and with -// networks up to several thousand nodes that eager pass would stall the UI -// even when the person never opens the info popup. -let fullMetricsCache = null - -export function invalidateFullMetrics() { - fullMetricsCache = null -} - -export function ensureFullMetrics() { - if (!fullMetricsCache) { - fullMetricsCache = { - degree: computeMetricValues('degree', true), - indegree: computeMetricValues('indegree', true), - outdegree: computeMetricValues('outdegree', true), - betweenness: computeMetricValues('betweenness', true), - closeness: computeMetricValues('closeness', true), - clustering: computeMetricValues('clustering', true), - } - } - return fullMetricsCache -} - -export function applyNodeSizing() { - const metric = document.getElementById('sizeMetric').value - const scale = parseFloat(document.getElementById('nodeScale').value) || 1 - document.getElementById('nodeScaleValue').textContent = scale.toFixed(2).replace(/0$/, '') + '×' - - if (metric === 'fixed') { - cy.batch(() => cy.nodes().forEach((n) => n.data('size', n.data('baseSize') * scale))) - applyLabelSizes() - if (typeof applyBundleResult === 'function') applyBundleResult() - return - } - - const channelOnly = document.getElementById('sizeChannelOnly').checked - const minPx = parseFloat(document.getElementById('sizeMin').value) || 22 - const maxPx = parseFloat(document.getElementById('sizeMax').value) || 74 - const values = computeMetricValues(metric, channelOnly) - const nums = Object.values(values) - if (!nums.length) return - const lo = Math.min(...nums), - hi = Math.max(...nums) - - cy.nodes().forEach((n) => { - const v = values[n.id()] || 0 - const size = hi === lo ? (minPx + maxPx) / 2 : minPx + ((v - lo) / (hi - lo)) * (maxPx - minPx) - n.data('size', size * scale) - }) - applyLabelSizes() - if (typeof applyBundleResult === 'function') applyBundleResult() -} - -export function updateStats() { - document.getElementById('statNodes').textContent = cy.nodes().length.toLocaleString() - document.getElementById('statEdges').textContent = cy.edges().length.toLocaleString() - const types = new Set(cy.edges().map((e) => e.data('type'))) - document.getElementById('statTypes').textContent = types.size - const groups = getUsedGroups().filter((g) => g !== UNGROUPED) - const active = groups.filter((g) => S.activeGroups.has(g)).length - document.getElementById('statGroups').textContent = - active === groups.length ? groups.length : `${active}/${groups.length}` - const word = (id, n, one, many) => { - document.getElementById(id).textContent = n === 1 ? one : many - } - word('statNodesWord', cy.nodes().length, 'node', 'nodes') - word('statEdgesWord', cy.edges().length, 'edge', 'edges') - word('statTypesWord', types.size, 'channel', 'channels') - word('statGroupsWord', groups.length, 'group', 'groups') - const dirEl = document.getElementById('statDirection') - dirEl.classList.remove('stat-dir-off') - if (typeof directionOf === 'function' && cy.edges().length) { - const dir = directionOf(cy.edges()) - const dataDirected = cy.edges().filter((e) => e.data('directed')).length - if (dir === 'undirected' && dataDirected && directionMode() === 'undirected') { - // the data has direction but the view ignores it (the default) - dirEl.hidden = false - dirEl.textContent = 'direction ignored' - dirEl.classList.add('stat-dir-off') - dirEl.title = `${dataDirected.toLocaleString()} edges are marked as directed in the data. Click to show them as directed (Edges → Direction: As in the data).` - } else { - dirEl.hidden = dir === 'undirected' - dirEl.textContent = dir === 'mixed' ? 'mixed direction' : 'directed' - dirEl.title = - dir === 'mixed' - ? 'Some edges are directed, some are not' - : 'Edges point from source to target' - } - } else dirEl.hidden = true - if (typeof updateContextInfo === 'function') updateContextInfo() - if (typeof scheduleLegend === 'function') { - scheduleLegend() - scheduleSeparation() - } -} - -// Per-layout-name option tweaks shared between the normal single-graph -// layout and the headless sub-layouts used by the group strategies. -export function layoutOptsFor(name) { - const opts = { name } - if (name === 'concentric') { - opts.concentric = (n) => n.degree() - opts.levelWidth = () => 1 - } - if (name === 'breadthfirst') { - opts.directed = typeof anyEdgeDirected === 'function' && anyEdgeDirected(shownEdges(true)) - opts.spacingFactor = 1.1 - } - if (name === 'cose') { - opts.idealEdgeLength = 90 - opts.nodeRepulsion = 6000 - opts.gravity = 0.4 - } - return opts -} - -// Above this size a network loads with a grid layout instead of force-directed. -export const LARGE_NETWORK_NODES = 700 - -/* ============================================================ - FORCE-DIRECTED EDGE BUNDLING - Holten D, van Wijk JJ (2009) Force-directed edge bundling for graph - visualization. Computer Graphics Forum 28(3):983-990. - Each edge becomes a polyline whose inner points are pulled toward the - matching points of "compatible" edges (similar direction, length, - position and mutual visibility) and held in shape by springs. The - number of points doubles each cycle while the step size halves. - Input: [{sx, sy, tx, ty}] in model coordinates. Output: for each edge, - a flat array [x1, y1, x2, y2, ...] of its inner points, or null. - ============================================================ */ -// Progress of long computations (layouts, edge bundling), reported from -// inside their loops as a fraction of the part in workRange. In the worker, -// workSink posts it to the page; on the page itself it is not used. -var workSink = null, - workLast = 0 - -export function workStep(f) { - if (!workSink) return - const now = Date.now() - if (now - workLast < 80) return - workLast = now - workSink(S.workRange[0] + (S.workRange[1] - S.workRange[0]) * Math.max(0, Math.min(1, f))) -} - -function fdebBundle(segs, opts) { - const E = segs.length - const threshold = opts.threshold - const K = 0.1, - EPS = 1e-6 - // 4 cycles give 8 inner points per edge; Cytoscape stops drawing - // unbundled-bezier edges with more than 8 control points (tested) - const cycles = 4 - let iterations = opts.iterations - let S = 0.1 - const COMPAT_CAP = 150 - - const len = new Float64Array(E), - mx = new Float64Array(E), - my = new Float64Array(E) - const dx = new Float64Array(E), - dy = new Float64Array(E) - for (let i = 0; i < E; i++) { - const s = segs[i] - dx[i] = s.tx - s.sx - dy[i] = s.ty - s.sy - len[i] = Math.hypot(dx[i], dy[i]) - mx[i] = (s.sx + s.tx) / 2 - my[i] = (s.sy + s.ty) / 2 - } - // how much of segment q is "visible" from segment p (projection overlap) - function visibility(p, q) { - const s = segs[p], - t = segs[q] - const L2 = len[p] * len[p] - const u0 = ((t.sx - s.sx) * dx[p] + (t.sy - s.sy) * dy[p]) / L2 - const u1 = ((t.tx - s.sx) * dx[p] + (t.ty - s.sy) * dy[p]) / L2 - const i0x = s.sx + u0 * dx[p], - i0y = s.sy + u0 * dy[p] - const i1x = s.sx + u1 * dx[p], - i1y = s.sy + u1 * dy[p] - const il = Math.hypot(i1x - i0x, i1y - i0y) - if (il < EPS) return 0 - const imx = (i0x + i1x) / 2, - imy = (i0y + i1y) / 2 - return Math.max(0, 1 - (2 * Math.hypot(mx[p] - imx, my[p] - imy)) / il) - } - - // compatible partners per edge; `flip` marks partners running the other way - const partners = Array.from({ length: E }, () => []) - for (let p = 0; p < E; p++) { - if ((p & 31) === 0) workStep(0.2 * (1 - ((E - p) * (E - p)) / (E * E))) - if (len[p] < EPS) continue - for (let q = p + 1; q < E; q++) { - if (len[q] < EPS) continue - const dot = dx[p] * dx[q] + dy[p] * dy[q] - const angle = Math.abs(dot) / (len[p] * len[q]) - if (angle < threshold) continue - const lavg = (len[p] + len[q]) / 2 - const scale = 2 / (lavg / Math.min(len[p], len[q]) + Math.max(len[p], len[q]) / lavg) - let c = angle * scale - if (c < threshold) continue - c *= lavg / (lavg + Math.hypot(mx[p] - mx[q], my[p] - my[q])) - if (c < threshold) continue - c *= Math.min(visibility(p, q), visibility(q, p)) - if (c < threshold) continue - const flip = dot < 0 - partners[p].push({ e: q, c, flip }) - partners[q].push({ e: p, c, flip }) - if (partners[p].length > COMPAT_CAP * 4) { - partners[p].sort((a, b) => b.c - a.c) - partners[p].length = COMPAT_CAP - } - if (partners[q].length > COMPAT_CAP * 4) { - partners[q].sort((a, b) => b.c - a.c) - partners[q].length = COMPAT_CAP - } - } - } - partners.forEach((list) => { - if (list.length > COMPAT_CAP) { - list.sort((a, b) => b.c - a.c) - list.length = COMPAT_CAP - } - }) - - // polylines, stored as [x0, y0, ..., xn, yn] including both endpoints - let P = 1 - let pts = segs.map((s) => - Float64Array.from([s.sx, s.sy, (s.sx + s.tx) / 2, (s.sy + s.ty) / 2, s.tx, s.ty]) - ) - - function resample(line, segments) { - const n = line.length / 2 - let total = 0 - for (let i = 1; i < n; i++) - total += Math.hypot(line[2 * i] - line[2 * i - 2], line[2 * i + 1] - line[2 * i - 1]) - const out = new Float64Array((segments + 1) * 2) - out[0] = line[0] - out[1] = line[1] - out[segments * 2] = line[(n - 1) * 2] - out[segments * 2 + 1] = line[(n - 1) * 2 + 1] - if (total < EPS) { - for (let k = 1; k < segments; k++) { - out[2 * k] = line[0] - out[2 * k + 1] = line[1] - } - return out - } - const step = total / segments - let seg = 1, - acc = 0 - for (let k = 1; k < segments; k++) { - const target = step * k - while (seg < n) { - const sl = Math.hypot( - line[2 * seg] - line[2 * seg - 2], - line[2 * seg + 1] - line[2 * seg - 1] - ) - if (acc + sl >= target || seg === n - 1) { - const f = sl > EPS ? (target - acc) / sl : 0 - out[2 * k] = line[2 * seg - 2] + (line[2 * seg] - line[2 * seg - 2]) * f - out[2 * k + 1] = line[2 * seg - 1] + (line[2 * seg + 1] - line[2 * seg - 1]) * f - break - } - acc += sl - seg++ - } - } - return out - } - - // work per iteration grows with the points per edge (2, 4, 8, 16) - let planned = 0, - spent = 0 - for (let c = 0, its = opts.iterations; c < cycles; c++) { - if (c > 0) its = Math.max(3, Math.round((its * 2) / 3)) - planned += its * 2 ** c - } - for (let cycle = 0; cycle < cycles; cycle++) { - if (cycle > 0) { - P *= 2 - S /= 2 - iterations = Math.max(3, Math.round((iterations * 2) / 3)) - pts = pts.map((line) => resample(line, P + 1)) - } - const nPts = P + 2 // including endpoints - for (let it = 0; it < iterations; it++) { - workStep(0.2 + (0.8 * spent) / planned) - spent += 2 ** cycle - const next = pts.map((line) => Float64Array.from(line)) - for (let e = 0; e < E; e++) { - const line = pts[e] - if (len[e] < EPS) continue - const kP = K / (len[e] * (P + 1)) - const plist = partners[e] - for (let i = 1; i < nPts - 1; i++) { - const x = line[2 * i], - y = line[2 * i + 1] - let fx = kP * (line[2 * i - 2] - x + line[2 * i + 2] - x) - let fy = kP * (line[2 * i - 1] - y + line[2 * i + 3] - y) - for (let j = 0; j < plist.length; j++) { - const other = pts[plist[j].e] - const oi = plist[j].flip ? nPts - 1 - i : i - const ex = other[2 * oi] - x, - ey = other[2 * oi + 1] - y - const d = Math.hypot(ex, ey) - if (d > EPS) { - fx += ex / d - fy += ey / d - } - } - next[e][2 * i] = x + S * fx - next[e][2 * i + 1] = y + S * fy - } - } - pts = next - } - } - return pts.map((line, e) => (len[e] < EPS ? null : Array.from(line.slice(2, line.length - 2)))) -} - -/* ============================================================ - WEIGHTED FRUCHTERMAN-REINGOLD - NORMA computes its layouts with igraph, where layout_with_fr - multiplies the spring attraction along an edge by that edge's weight. - The group strategies depend on this: they work by adding heavy or - light edges. Cytoscape has no equivalent, so this is a direct - implementation with igraph's defaults: 500 iterations, a start - temperature of sqrt(n)/10 cooling linearly to zero, repulsion k²/d and - attraction w·d²/k with k = 1. It is seeded (NORMA uses set.seed(123)), - so the same input always gives the same picture. - Each connected component is laid out on its own and the components - are then packed side by side; otherwise small components drift - arbitrarily far from the main one. - ============================================================ */ -export const FR_SEED = 123 - -function connectedComponents(n, edges) { - const parent = Int32Array.from({ length: n }, (_, i) => i) - const find = (i) => { - while (parent[i] !== i) { - parent[i] = parent[parent[i]] - i = parent[i] - } - return i - } - edges.forEach((e) => { - const a = find(e.s), - b = find(e.t) - if (a !== b) parent[a] = b - }) - const byRoot = new Map() - for (let i = 0; i < n; i++) { - const r = find(i) - if (!byRoot.has(r)) byRoot.set(r, []) - byRoot.get(r).push(i) - } - return [...byRoot.values()] -} - -// n nodes (0..n-1), edges [{s, t, w}] -> { x: Float64Array, y: Float64Array } -function frComponent(n, edges, rand) { - const X = new Float64Array(n), - Y = new Float64Array(n) - if (n === 1) return { x: X, y: Y } - const side = Math.sqrt(n) - for (let i = 0; i < n; i++) { - X[i] = (rand() - 0.5) * side - Y[i] = (rand() - 0.5) * side - } - const iterations = n > 3000 ? 150 : n > 1000 ? 300 : 500 - const temp0 = Math.sqrt(n) / 10 - const DX = new Float64Array(n), - DY = new Float64Array(n) - const useGrid = n > 1500 - const CELL = 2 // FR's grid variant: repulsion only within 2k - - for (let it = 0; it < iterations; it++) { - if ((it & 3) === 0) workStep(it / iterations) - DX.fill(0) - DY.fill(0) - if (!useGrid) { - for (let i = 0; i < n; i++) { - for (let j = i + 1; j < n; j++) { - let dx = X[i] - X[j], - dy = Y[i] - Y[j] - let d2 = dx * dx + dy * dy - if (d2 < 1e-9) { - dx = (rand() - 0.5) * 1e-3 - dy = (rand() - 0.5) * 1e-3 - d2 = dx * dx + dy * dy - } - const f = 1 / d2 // (k²/d) along the unit vector - DX[i] += dx * f - DY[i] += dy * f - DX[j] -= dx * f - DY[j] -= dy * f - } - } - } else { - const grid = new Map() - for (let i = 0; i < n; i++) { - const key = Math.floor(X[i] / CELL) + ',' + Math.floor(Y[i] / CELL) - let cell = grid.get(key) - if (!cell) grid.set(key, (cell = [])) - cell.push(i) - } - for (let i = 0; i < n; i++) { - const cx = Math.floor(X[i] / CELL), - cyy = Math.floor(Y[i] / CELL) - for (let gx = cx - 1; gx <= cx + 1; gx++) { - for (let gy = cyy - 1; gy <= cyy + 1; gy++) { - const cell = grid.get(gx + ',' + gy) - if (!cell) continue - for (const j of cell) { - if (j <= i) continue - let dx = X[i] - X[j], - dy = Y[i] - Y[j] - let d2 = dx * dx + dy * dy - if (d2 > CELL * CELL * 4) continue - if (d2 < 1e-9) { - dx = (rand() - 0.5) * 1e-3 - dy = (rand() - 0.5) * 1e-3 - d2 = dx * dx + dy * dy - } - const f = 1 / d2 - DX[i] += dx * f - DY[i] += dy * f - DX[j] -= dx * f - DY[j] -= dy * f - } - } - } - } - } - for (const e of edges) { - const dx = X[e.s] - X[e.t], - dy = Y[e.s] - Y[e.t] - const d = Math.sqrt(dx * dx + dy * dy) - const f = e.w * d // (w·d²/k) along the unit vector - DX[e.s] -= dx * f - DY[e.s] -= dy * f - DX[e.t] += dx * f - DY[e.t] += dy * f - } - const temp = temp0 * (1 - it / iterations) - for (let i = 0; i < n; i++) { - const len = Math.sqrt(DX[i] * DX[i] + DY[i] * DY[i]) - if (len > temp) { - DX[i] *= temp / len - DY[i] *= temp / len - } - X[i] += DX[i] - Y[i] += DY[i] - } - } - return { x: X, y: Y } -} - -// Shelf-packs laid-out components, largest first, into a roughly square block. -function packComponents(parts) { - const PAD = 1.5 - parts.forEach((p) => { - let x1 = Infinity, - y1 = Infinity, - x2 = -Infinity, - y2 = -Infinity - for (let i = 0; i < p.x.length; i++) { - x1 = Math.min(x1, p.x[i]) - x2 = Math.max(x2, p.x[i]) - y1 = Math.min(y1, p.y[i]) - y2 = Math.max(y2, p.y[i]) - } - p.x1 = x1 - p.y1 = y1 - p.w = x2 - x1 + PAD - p.h = y2 - y1 + PAD - }) - parts.sort((a, b) => b.w * b.h - a.w * a.h) - const area = parts.reduce((s, p) => s + p.w * p.h, 0) - const rowWidth = Math.max(parts[0].w, Math.sqrt(area) * 1.2) - let cx = 0, - cyy = 0, - rowH = 0 - parts.forEach((p) => { - if (cx > 0 && cx + p.w > rowWidth) { - cx = 0 - cyy += rowH - rowH = 0 - } - p.ox = cx - p.x1 - p.oy = cyy - p.y1 - cx += p.w - rowH = Math.max(rowH, p.h) - }) -} - -// ids: string[], edges: [{source, target, weight}] -> { id: {x, y} } in FR units -function frLayout(ids, edges) { - const index = new Map(ids.map((id, i) => [id, i])) - const all = [] - edges.forEach((e) => { - const s = index.get(e.source), - t = index.get(e.target) - if (s === undefined || t === undefined || s === t) return - const w = Number.isFinite(e.weight) && e.weight > 0 ? e.weight : 1 - all.push({ s, t, w }) - }) - const rand = mulberry32(FR_SEED) - const comps = connectedComponents(ids.length, all) - const compOf = new Int32Array(ids.length) - const localIdx = new Int32Array(ids.length) - comps.forEach((members, c) => - members.forEach((g, i) => { - compOf[g] = c - localIdx[g] = i - }) - ) - const compEdges = comps.map(() => []) - all.forEach((e) => compEdges[compOf[e.s]].push({ s: localIdx[e.s], t: localIdx[e.t], w: e.w })) - let laidOut = 0 - const parts = comps.map((members, c) => { - S.workRange = [laidOut / ids.length, (laidOut + members.length) / ids.length] - laidOut += members.length - return { members, ...frComponent(members.length, compEdges[c], rand) } - }) - S.workRange = [0, 1] - packComponents(parts) - const out = {} - parts.forEach((p) => - p.members.forEach((g, i) => { - out[ids[g]] = { x: p.x[i] + p.ox, y: p.y[i] + p.oy } - }) - ) - return out -} - -/* ============================================================ - DISTANCE-BASED LAYOUTS - kkLayout Kamada & Kawai (1989): springs between all pairs, with - lengths equal to graph distances (hops), relaxed node by - node with Newton-Raphson steps. - stressLayout Stress majorization (Gansner, Koren & North 2004): - pivot MDS start, then weighted stress minimisation - (weights 1/d^2). - Both lay out each connected component separately and pack them, like - the weighted Fruchterman-Reingold layout, and run in the worker. - Edge weights are not used: distances are shortest paths in hops. - ============================================================ */ -const DIST_LAYOUT_LIMIT = 2500 -// nodes per component - -// all-pairs hop distances of one component (local indices) -function componentDistances(n, adj) { - const D = new Float64Array(n * n) - const q = new Int32Array(n) - for (let s = 0; s < n; s++) { - const row = s * n - for (let i = 0; i < n; i++) D[row + i] = -1 - D[row + s] = 0 - let head = 0, - tail = 0 - q[tail++] = s - while (head < tail) { - const v = q[head++] - const dv = D[row + v] - const a = adj[v] - for (let k = 0; k < a.length; k++) { - const w = a[k] - if (D[row + w] < 0) { - D[row + w] = dv + 1 - q[tail++] = w - } - } - } - } - return D -} - -// Pivot MDS (Brandes & Pich 2006): a quick, good starting layout. -function pivotMDS(n, D) { - const X = new Float64Array(n), - Y = new Float64Array(n) - if (n < 3) { - for (let i = 0; i < n; i++) { - X[i] = i - } - return { X, Y } - } - const k = Math.min(50, n) - const pivots = [0] - const minD = new Float64Array(n).fill(Infinity) - for (let p = 1; p < k; p++) { - const last = pivots[p - 1] - let best = 0, - bestD = -1 - for (let i = 0; i < n; i++) { - minD[i] = Math.min(minD[i], D[last * n + i]) - if (minD[i] > bestD) { - bestD = minD[i] - best = i - } - } - pivots.push(best) - } - // C: n x k squared distances, double centered - const C = new Float64Array(n * k) - const colMean = new Float64Array(k), - rowMean = new Float64Array(n) - let total = 0 - for (let i = 0; i < n; i++) - for (let j = 0; j < k; j++) { - const v = Math.pow(D[pivots[j] * n + i], 2) - C[i * k + j] = v - colMean[j] += v - rowMean[i] += v - total += v - } - for (let j = 0; j < k; j++) colMean[j] /= n - for (let i = 0; i < n; i++) rowMean[i] /= k - total /= n * k - for (let i = 0; i < n; i++) - for (let j = 0; j < k; j++) { - C[i * k + j] = -0.5 * (C[i * k + j] - rowMean[i] - colMean[j] + total) - } - // top two eigenvectors of C^T C by power iteration - const CtC = new Float64Array(k * k) - for (let a = 0; a < k; a++) - for (let b = a; b < k; b++) { - let s = 0 - for (let i = 0; i < n; i++) s += C[i * k + a] * C[i * k + b] - CtC[a * k + b] = s - CtC[b * k + a] = s - } - const vecs = [] - for (let e = 0; e < 2; e++) { - let v = Float64Array.from({ length: k }, (_, i) => Math.sin(i * (e + 1) + 1)) - for (let it = 0; it < 100; it++) { - const w = new Float64Array(k) - for (let a = 0; a < k; a++) { - let s = 0 - for (let b = 0; b < k; b++) s += CtC[a * k + b] * v[b] - w[a] = s - } - vecs.forEach((u) => { - let d = 0 - for (let a = 0; a < k; a++) d += w[a] * u[a] - for (let a = 0; a < k; a++) w[a] -= d * u[a] - }) - let norm = 0 - for (let a = 0; a < k; a++) norm += w[a] * w[a] - norm = Math.sqrt(norm) || 1 - for (let a = 0; a < k; a++) w[a] /= norm - v = w - } - vecs.push(v) - } - for (let i = 0; i < n; i++) { - let x = 0, - y = 0 - for (let j = 0; j < k; j++) { - x += C[i * k + j] * vecs[0][j] - y += C[i * k + j] * vecs[1][j] - } - X[i] = x - Y[i] = y - } - // scale to hop units - let spread = 0, - count = 0 - for (let i = 0; i < n; i++) - for (let j = i + 1; j < Math.min(n, i + 20); j++) { - const d = Math.hypot(X[i] - X[j], Y[i] - Y[j]) - if (d > 0) { - spread += D[i * n + j] / d - count++ - } - } - const s = count ? spread / count : 1 - for (let i = 0; i < n; i++) { - X[i] *= s - Y[i] *= s - } - // break exact ties - for (let i = 0; i < n; i++) { - X[i] += 1e-3 * Math.sin(i * 12.9898) - Y[i] += 1e-3 * Math.cos(i * 78.233) - } - return { X, Y } -} - -function kkComponent(n, adj) { - const X = new Float64Array(n), - Y = new Float64Array(n) - if (n === 1) return { x: X, y: Y } - const D = componentDistances(n, adj) - // start on a circle, as igraph does - let maxD = 0 - for (let i = 0; i < D.length; i++) if (D[i] > maxD) maxD = D[i] - for (let i = 0; i < n; i++) { - X[i] = (Math.cos((2 * Math.PI * i) / n) * maxD) / 2 - Y[i] = (Math.sin((2 * Math.PI * i) / n) * maxD) / 2 - } - const gx = new Float64Array(n), - gy = new Float64Array(n) - const term = (m, i) => { - // gradient contribution of pair (m, i) to m - const d = D[m * n + i] - if (d <= 0) return [0, 0] - const dx = X[m] - X[i], - dy = Y[m] - Y[i] - const dist = Math.sqrt(dx * dx + dy * dy) || 1e-9 - const k = 1 / (d * d) - return [k * (dx - (d * dx) / dist), k * (dy - (d * dy) / dist)] - } - for (let m = 0; m < n; m++) - for (let i = 0; i < n; i++) { - if (i === m) continue - const [a, b] = term(m, i) - gx[m] += a - gy[m] += b - } - const maxIter = Math.min(50 * n, 40000) - const eps = 1e-4 - for (let it = 0; it < maxIter; it++) { - if ((it & 63) === 0) workStep(it / maxIter) - let m = -1, - best = eps - for (let i = 0; i < n; i++) { - const g = gx[i] * gx[i] + gy[i] * gy[i] - if (g > best) { - best = g - m = i - } - } - if (m < 0) break - // remove m's old contributions from everybody else - for (let i = 0; i < n; i++) { - if (i === m) continue - const [a, b] = term(i, m) - gx[i] -= a - gy[i] -= b - } - // Newton-Raphson on node m - for (let step = 0; step < 20; step++) { - let dxx = 0, - dyy = 0, - dxy = 0, - ex = 0, - ey = 0 - for (let i = 0; i < n; i++) { - if (i === m) continue - const d = D[m * n + i] - if (d <= 0) continue - const k = 1 / (d * d) - const dx = X[m] - X[i], - dy = Y[m] - Y[i] - const dist2 = dx * dx + dy * dy || 1e-12 - const dist = Math.sqrt(dist2) - const dist3 = dist2 * dist - ex += k * (dx - (d * dx) / dist) - ey += k * (dy - (d * dy) / dist) - dxx += k * (1 - (d * dy * dy) / dist3) - dyy += k * (1 - (d * dx * dx) / dist3) - dxy += k * ((d * dx * dy) / dist3) - } - const det = dxx * dyy - dxy * dxy - if (Math.abs(det) < 1e-12) break - const sx = (-ex * dyy + ey * dxy) / det - const sy = (-ey * dxx + ex * dxy) / det - X[m] += sx - Y[m] += sy - gx[m] = ex - gy[m] = ey - if (sx * sx + sy * sy < 1e-8) break - } - // recompute m's gradient and add its new contributions to everybody - gx[m] = 0 - gy[m] = 0 - for (let i = 0; i < n; i++) { - if (i === m) continue - const [a, b] = term(m, i) - gx[m] += a - gy[m] += b - const [c, e] = term(i, m) - gx[i] += c - gy[i] += e - } - } - return { x: X, y: Y } -} - -function stressComponent(n, adj) { - if (n === 1) return { x: new Float64Array(1), y: new Float64Array(1) } - const D = componentDistances(n, adj) - const { X, Y } = pivotMDS(n, D) - const iters = Math.max(20, Math.min(300, Math.floor(4e7 / (n * n)))) - let prev = Infinity - for (let it = 0; it < iters; it++) { - workStep(it / iters) - let stress = 0 - for (let i = 0; i < n; i++) { - let sx = 0, - sy = 0, - sw = 0 - for (let j = 0; j < n; j++) { - if (j === i) continue - const d = D[i * n + j] - if (d <= 0) continue - const w = 1 / (d * d) - const dx = X[i] - X[j], - dy = Y[i] - Y[j] - const dist = Math.sqrt(dx * dx + dy * dy) || 1e-9 - sx += w * (X[j] + (d * dx) / dist) - sy += w * (Y[j] + (d * dy) / dist) - sw += w - stress += w * (dist - d) * (dist - d) - } - if (sw) { - X[i] = sx / sw - Y[i] = sy / sw - } - } - if (Math.abs(prev - stress) / (stress || 1) < 1e-5) break - prev = stress - } - return { x: X, y: Y } -} - -function distanceLayout(ids, edges, kind) { - const index = new Map(ids.map((id, i) => [id, i])) - const all = [] - edges.forEach((e) => { - const s = index.get(e.source), - t = index.get(e.target) - if (s === undefined || t === undefined || s === t) return - all.push({ s, t, w: 1 }) - }) - const comps = connectedComponents(ids.length, all) - const compOf = new Int32Array(ids.length), - localIdx = new Int32Array(ids.length) - comps.forEach((members, c) => - members.forEach((g, i) => { - compOf[g] = c - localIdx[g] = i - }) - ) - const adjs = comps.map((m) => m.map(() => [])) - all.forEach((e) => { - const c = compOf[e.s] - adjs[c][localIdx[e.s]].push(localIdx[e.t]) - adjs[c][localIdx[e.t]].push(localIdx[e.s]) - }) - let laidOut = 0 - const parts = comps.map((members, c) => { - S.workRange = [laidOut / ids.length, (laidOut + members.length) / ids.length] - laidOut += members.length - if (members.length > DIST_LAYOUT_LIMIT) - throw new Error( - `${kind === 'kk' ? 'Kamada–Kawai' : 'Stress majorization'} handles connected parts of up to ${DIST_LAYOUT_LIMIT.toLocaleString('en-US')} nodes; this network has one of ${members.length.toLocaleString('en-US')}. Use a force-directed layout instead.` - ) - const lay = - kind === 'kk' - ? kkComponent(members.length, adjs[c]) - : stressComponent(members.length, adjs[c]) - // hop units -> FR-like units so packing gaps look alike - return { members, x: lay.x, y: lay.y } - }) - S.workRange = [0, 1] - packComponents(parts) - const out = {} - parts.forEach((p) => - p.members.forEach((g, i) => { - out[ids[g]] = { x: p.x[i] + p.ox, y: p.y[i] + p.oy } - }) - ) - return out -} - -function distanceLayoutAsync(ids, edges, kind, onProgress) { - const worker = ids.length > 60 ? getFrWorker() : null - if (!worker) return Promise.resolve(distanceLayout(ids, edges, kind)) - const id = ++S.frRequestSeq - return new Promise((resolve, reject) => { - frPending.set(id, { - resolve, - reject, - onProgress, - local: () => distanceLayout(ids, edges, kind), - }) - worker.postMessage({ id, kind, ids, edges }) - }) -} - -// The weighted layout runs in a background worker when the browser allows -// it, so large networks don't freeze the page. The worker is built from -// the same functions as above; if it can't be created, or fails, the -// layout runs on the page instead. -let frWorker = null - -let frWorkerFailed = false - -export const frPending = new Map() - -export function getFrWorker() { - if (frWorker || frWorkerFailed) return frWorker - try { - const src = - [ - mulberry32, - connectedComponents, - frComponent, - packComponents, - frLayout, - fdebBundle, - fr3dLayout, - componentDistances, - pivotMDS, - kkComponent, - stressComponent, - distanceLayout, - workStep, - ] - .map((f) => f.toString()) - .join('\n') + - `\nconst FR_SEED = ${FR_SEED};\nconst DIST_LAYOUT_LIMIT = ${DIST_LAYOUT_LIMIT};\nvar workRange = [0, 1], workSink = null, workLast = 0;\n` + - // The dispatcher below used to call these by their literal source - // names, which broke once the production build's minifier renamed - // these (non-exported) functions: the worker still got their bodies - // via .toString(), but under whatever new name the minifier picked, - // so the hardcoded names threw "X is not defined". Using .name reads - // each function's *current* runtime name, so it always matches the - // identifier its .toString() source was declared under. - `onmessage = e => { const m = e.data; let result; workRange = [0, 1]; workLast = 0; workSink = f => postMessage({ id: m.id, progress: f }); try{ result = m.kind === "fdeb" ? ${fdebBundle.name}(m.segs, m.opts) : m.kind === "fr3d" ? ${fr3dLayout.name}(m.ids, m.edges, m.opts) : (m.kind === "kk" || m.kind === "stress") ? ${distanceLayout.name}(m.ids, m.edges, m.kind) : ${frLayout.name}(m.ids, m.edges); } catch(err){ postMessage({ id: m.id, error: String(err && err.message || err) }); return; } postMessage({ id: m.id, result }); };` - const url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' })) - frWorker = new Worker(url) - frWorker.onmessage = (e) => { - const job = frPending.get(e.data.id) - if (!job) return - if (e.data.progress !== undefined) { - if (job.onProgress) job.onProgress(e.data.progress) - return - } - frPending.delete(e.data.id) - if (e.data.error) { - if (job.reject) job.reject(new Error(e.data.error)) - else job.resolve(null) - } else job.resolve(e.data.result) - } - frWorker.onerror = (e) => { - e.preventDefault() - frWorkerFailed = true - frWorker = null - // finish anything still waiting on the page itself - frPending.forEach((job) => job.resolve(job.local())) - frPending.clear() - } - } catch (err) { - frWorkerFailed = true - frWorker = null - } - return frWorker -} - -// Stops any weighted layout still computing in the worker; its callers see -// null and, being superseded, discard it. -export function cancelFrJobs() { - if (!frWorker || !frPending.size) return - frWorker.terminate() - frWorker = null - frPending.forEach((job) => job.resolve(null)) - frPending.clear() -} - -export function frLayoutAsync(ids, edges, onProgress) { - const worker = ids.length > 60 ? getFrWorker() : null // small graphs: not worth the round trip - if (!worker) return Promise.resolve(frLayout(ids, edges)) - const id = ++S.frRequestSeq - return new Promise((resolve) => { - frPending.set(id, { resolve, onProgress, local: () => frLayout(ids, edges) }) - worker.postMessage({ id, kind: 'fr', ids, edges }) - }) -} - -export function bundleAsync(segs, opts, onProgress) { - const worker = segs.length > 150 ? getFrWorker() : null - if (!worker) return Promise.resolve(fdebBundle(segs, opts)) - const id = ++S.frRequestSeq - return new Promise((resolve) => { - frPending.set(id, { resolve, onProgress, local: () => fdebBundle(segs, opts) }) - worker.postMessage({ id, kind: 'fdeb', segs, opts }) - }) -} - -// Layouts that place nodes without looking at edges; strategies can skip -// adding layout-only links for these, since they change nothing. -export const EDGE_BLIND_LAYOUTS = new Set(['circle', 'grid', 'random']) - -// Runs a layout on a detached, invisible headless Cytoscape instance (or the -// weighted FR above) and returns a { nodeId: {x,y} } position map in -// arbitrary units; callers rescale. Edge weights are honored by 'fr' -// (attraction x weight) and by 'cose' (shorter, stiffer springs). -export async function computeSubLayoutAsync(nodeIds, edgeDefs, layoutName) { - const onProgress = S.layoutProgress ? (f) => S.layoutProgress && S.layoutProgress.report(f) : null - if (layoutName === 'fr' && nodeIds.length > 1) return frLayoutAsync(nodeIds, edgeDefs, onProgress) - if ((layoutName === 'kk' || layoutName === 'stress') && nodeIds.length > 1) - return distanceLayoutAsync(nodeIds, edgeDefs, layoutName, onProgress) - return computeSubLayout(nodeIds, edgeDefs, layoutName) -} - -export function makeLayoutProgress(show) { - return { - total: 1, - done: 0, - current: 1, - begin(total) { - this.total = Math.max(1e-9, total) - this.done = 0 - this.current = 0 - }, - part(weight) { - this.done += this.current - this.current = weight - this.report(0) - }, - report(f) { - show(Math.min(1, (this.done + this.current * Math.max(0, Math.min(1, f))) / this.total)) - }, - } -} - -export function computeSubLayout(nodeIds, edgeDefs, layoutName) { - if (nodeIds.length === 1) { - return { [nodeIds[0]]: { x: 0, y: 0 } } - } - if (layoutName === 'fr') return frLayout(nodeIds, edgeDefs) - if (layoutName === 'kk' || layoutName === 'stress') - return distanceLayout(nodeIds, edgeDefs, layoutName) - const size = Math.max(300, Math.sqrt(nodeIds.length) * 90) - const sub = cytoscape({ - headless: true, - styleEnabled: false, - elements: { - nodes: nodeIds.map((id) => ({ data: { id } })), - edges: edgeDefs.map((e, i) => ({ - data: { - id: 'sub' + i, - source: e.source, - target: e.target, - weight: Math.min(100, Math.max(0.02, Number.isFinite(e.weight) ? e.weight : 1)), - }, - })), - }, - }) - const opts = { - ...layoutOptsFor(layoutName), - animate: false, - fit: false, - boundingBox: { x1: 0, y1: 0, w: size, h: size }, - } - if (layoutName === 'cose') { - opts.idealEdgeLength = (e) => 90 / Math.sqrt(e.data('weight')) - opts.edgeElasticity = (e) => 32 / e.data('weight') - opts.randomize = true - } - sub.layout(opts).run() - const positions = {} - sub.nodes().forEach((n) => { - positions[n.id()] = { x: n.position('x'), y: n.position('y') } - }) - sub.destroy() - return positions -} - -// Typical spacing between neighboring nodes on screen, from current node sizes. -export function targetNodeSpacing() { - const nodes = cy.nodes(':visible') - if (!nodes.length) return 64 - const mean = nodes.reduce((s, n) => s + (n.data('size') || 42), 0) / nodes.length - return Math.max(24, mean * 1.6) -} - -// Rescales a position map so the median nearest-neighbor distance equals -// `spacing` pixels. Positions from different algorithms come in very -// different units; this puts them all on the same screen scale. -export function normalizeSpacing(positions, spacing) { - const ids = Object.keys(positions) - if (ids.length < 2) return positions - const sample = - ids.length > 1500 ? ids.filter((_, i) => i % Math.ceil(ids.length / 1500) === 0) : ids - const pts = sample.map((id) => positions[id]) - const nn = [] - for (let i = 0; i < pts.length; i++) { - let best = Infinity - for (let j = 0; j < pts.length; j++) { - if (i === j) continue - const d = Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y) - if (d > 1e-9 && d < best) best = d - } - if (Number.isFinite(best)) nn.push(best) - } - if (!nn.length) return positions - nn.sort((a, b) => a - b) - const scale = spacing / nn[Math.floor(nn.length / 2)] - const out = {} - ids.forEach((id) => { - out[id] = { x: positions[id].x * scale, y: positions[id].y * scale } - }) - return out -} - -export function applyPositions(positions) { - cy.layout({ - name: 'preset', - positions: (node) => positions[node.id()] || node.position(), - animate: cy.nodes().length <= 1500, - animationDuration: 500, - fit: true, - padding: 50, - }).run() -} diff --git a/frontend/src/network_state.ts b/frontend/src/network_state.ts deleted file mode 100644 index 7e8a6e6..0000000 --- a/frontend/src/network_state.ts +++ /dev/null @@ -1,402 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EDGE_TYPES, NODE_PALETTES, colorAtIndex } from './palette' -import { NORMA_CFG } from './config' -import { S } from './state' -import { cy } from './cy' -import { nodeShapeFor, refreshEdgeLabelOptions } from './profiler' -import { parseNumber } from './clustering/mcl' - -const AUTO_EDGE_COLORS = ['#6c7793', '#d68fc0', '#7fd68a', '#e8a15f', '#8fb8e8', '#e07a7a'] - -export const PIE_MAX_SLICES = 16 - -// Nodes that belong to no group are collected under this key so they still -// have something to toggle in the legend. It is never written out as a -// real group (JSON export omits it; NORMA export skips it). -export const UNGROUPED = '__ungrouped__' - -export const UNGROUPED_LABEL = 'Not in any group' - -// Fill for nodes without their own color when coloring nodes from data, -// matching NORMA's default gray. -const NO_NODE_COLOR = '#9aa0a6' - -// Keeps only numeric (or missing) entries of a JSON values object. -export function cleanValues(obj) { - const out = {} - let any = false - Object.entries(obj).forEach(([k, v]) => { - const x = typeof v === 'number' ? v : parseNumber(v) - if (x === undefined) return - out[k] = x - any = true - }) - return any ? out : null -} - -export function nodeFillMode() { - const el = document.getElementById('nodeFillSelect') - return el ? el.value : 'groups' -} - -/* ============================================================ - ATTRIBUTE MODULE - One model for all three levels of the network: - - level | structural (reserved) keys | where custom attrs live - -------+-------------------------------------+-------------------------------- - node | id, group, groups, size, color | extra fields, or node.attrs - edge | id, source, target, type, color, | extra fields, or edge.attrs - | weight | - group | label, description, color | extra fields in groupAttrs[g], - | | or groupAttrs[g].attrs - - Reserved keys drive rendering; everything else is carried through - untouched, shown in the inspectors via one shared formatter, summarised - per level in the sidebar "Attributes" section, and written back out - on export in the same shape it came in. - ============================================================ */ -// NORMA shows networks of up to this many nodes; larger ones are cut to -// their first nodes (in file order) with a message. -export const MAX_NETWORK_NODES = Math.max( - 100, - Math.min(50000, parseInt(NORMA_CFG.app.maxNodes, 10) || 10000) -) - -export const RESERVED_NODE_KEYS = new Set([ - 'id', - 'group', - 'groups', - 'size', - 'color', - 'values', - 'attrs', -]) - -export const RESERVED_EDGE_KEYS = new Set([ - 'id', - 'source', - 'target', - 'type', - 'color', - 'weight', - 'directed', - 'attrs', -]) - -const RESERVED_GROUP_KEYS = new Set(['label', 'description', 'color', 'attrs']) - -export function collectAttrs(obj, reservedKeys) { - if (!obj || typeof obj !== 'object') return {} - const attrs = { ...(obj.attrs && typeof obj.attrs === 'object' ? obj.attrs : {}) } - Object.keys(obj).forEach((k) => { - if (!reservedKeys.has(k)) attrs[k] = obj[k] - }) - return attrs -} - -// Colors from loaded network/session data (nodeColors, groupAttrs.color, ...) -// are untrusted and end up in style/attribute values, so only a strict CSS -// color syntax is let through; anything else (e.g. an attribute-breakout -// attempt) is rejected rather than escaped, since these values are used both -// in innerHTML templates and as literal Cytoscape style colors. -export function sanitizeColor(c) { - if (typeof c !== 'string') return null - const v = c.trim() - if (/^#[0-9a-fA-F]{3,4}$|^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{8}$/.test(v)) return v - if (/^(rgb|rgba|hsl|hsla)\(\s*[\d.]+%?(\s*,\s*[\d.]+%?){2,3}\s*\)$/.test(v)) return v - if (/^[a-zA-Z]{3,20}$/.test(v)) return v - return null -} - -export function escapeHtml(str) { - return String(str).replace( - /[&<>"']/g, - (ch) => - ({ - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - })[ch] - ) -} - -export function attrKind(v) { - if (v === null || v === undefined || v === '') return 'empty' - if (Array.isArray(v)) return 'list' - if (typeof v === 'number') return Number.isFinite(v) ? 'number' : 'text' - if (typeof v === 'boolean') return 'boolean' - if (typeof v === 'object') return 'object' - return 'text' -} - -export function formatAttrValue(v) { - switch (attrKind(v)) { - case 'empty': - return '—' - case 'number': - return Number.isInteger(v) ? String(v) : String(+v.toPrecision(4)) - case 'boolean': - return v ? 'yes' : 'no' - case 'list': - return v - .map((x) => { - const k = attrKind(x) - return k === 'list' || k === 'object' ? JSON.stringify(x) : formatAttrValue(x) - }) - .join(', ') - case 'object': - return JSON.stringify(v) - default: - return String(v) - } -} - -// Plain-text form used by search, so lists and objects are matched on -// their contents rather than on "[object Object]". -function attrSearchText(attrs) { - return Object.values(attrs || {}) - .map(formatAttrValue) - .join(' ') - .toLowerCase() -} - -export function attrRowsHtml(attrs, rowClass) { - return Object.entries(attrs || {}) - .map( - ([k, v]) => - `
    ${escapeHtml(k)}${escapeHtml(formatAttrValue(v))}
    ` - ) - .join('') -} - -export function groupLabel(g) { - return (S.groupAttrs[g] && S.groupAttrs[g].label) || g -} - -export function groupDescription(g) { - return (S.groupAttrs[g] && S.groupAttrs[g].description) || '' -} - -export function customGroupAttrs(g) { - return collectAttrs(S.groupAttrs[g], RESERVED_GROUP_KEYS) -} - -// Summary of which attribute keys the loaded network uses, per level: -// { node: { key: { count, kinds:Set, min, max, values:Set } }, edge: {...}, group: {...} } -export let attrSchema = { node: {}, edge: {}, group: {} } - -const SCHEMA_DISTINCT_CAP = 50 - -function recordAttrs(bucket, attrs) { - Object.entries(attrs || {}).forEach(([k, v]) => { - const entry = - bucket[k] || - (bucket[k] = { count: 0, kinds: new Set(), min: Infinity, max: -Infinity, values: new Set() }) - const kind = attrKind(v) - if (kind === 'empty') return - entry.count++ - entry.kinds.add(kind) - if (kind === 'number') { - entry.min = Math.min(entry.min, v) - entry.max = Math.max(entry.max, v) - } else if (kind === 'text' && entry.values.size <= SCHEMA_DISTINCT_CAP) { - entry.values.add(v) - } else if (kind === 'list' && entry.values.size <= SCHEMA_DISTINCT_CAP) { - v.forEach((x) => entry.values.add(formatAttrValue(x))) - } - }) -} - -export function buildAttrSchema() { - attrSchema = { node: {}, edge: {}, group: {} } - cy.nodes().forEach((n) => recordAttrs(attrSchema.node, n.data('attrs'))) - cy.edges().forEach((e) => recordAttrs(attrSchema.edge, e.data('attrs'))) - getUsedGroups().forEach((g) => recordAttrs(attrSchema.group, customGroupAttrs(g))) -} - -function describeSchemaEntry(entry) { - if (entry.kinds.size > 1) return 'mixed: ' + [...entry.kinds].join(', ') - const kind = [...entry.kinds][0] - if (kind === 'number') { - return entry.min === entry.max - ? `number, always ${formatAttrValue(entry.min)}` - : `number, ${formatAttrValue(entry.min)} to ${formatAttrValue(entry.max)}` - } - if (kind === 'text') { - const n = entry.values.size - if (n > SCHEMA_DISTINCT_CAP) return `text, over ${SCHEMA_DISTINCT_CAP} values` - return n <= 4 ? `text: ${[...entry.values].join(', ')}` : `text, ${n} values` - } - if (kind === 'boolean') return 'yes / no' - if (kind === 'list') { - const n = entry.values.size - return n > SCHEMA_DISTINCT_CAP - ? `list, over ${SCHEMA_DISTINCT_CAP} distinct items` - : `list, ${n} distinct ${n === 1 ? 'item' : 'items'}` - } - return kind || 'empty' -} - -export function renderAttrSchema() { - refreshEdgeLabelOptions() - const el = document.getElementById('attrSchema') - if (!el) return - const totals = { node: cy.nodes().length, edge: cy.edges().length, group: getUsedGroups().length } - if (!totals.node) { - el.innerHTML = '

    Load a network to see its attributes.

    ' - return - } - const titles = { node: 'Nodes', edge: 'Edges', group: 'Groups' } - el.innerHTML = ['node', 'edge', 'group'] - .map((level) => { - const entries = Object.entries(attrSchema[level]).filter(([, e]) => e.count > 0) - const rows = entries.length - ? entries - .map( - ([k, e]) => ` -
    - ${escapeHtml(k)} - ${e.count}/${totals[level]} - ${escapeHtml(describeSchemaEntry(e))} -
    ` - ) - .join('') - : '

    No custom attributes

    ' - return `
    -
    ${titles[level]}${totals[level]}
    - ${rows} -
    ` - }) - .join('') -} - -// "Not in any group" is always neutral gray and doesn't use up a palette color. -export function colorForGroup(g) { - if (!S.nodeColorMap[g]) { - if (g === UNGROUPED) { - S.nodeColorMap[g] = NO_NODE_COLOR - } else { - const palette = NODE_PALETTES[S.currentNodePalette] || NODE_PALETTES.vivid - const idx = Object.keys(S.nodeColorMap).filter((k) => k !== UNGROUPED).length - S.nodeColorMap[g] = colorAtIndex(palette, idx) - } - } - return S.nodeColorMap[g] -} - -export function nextAutoEdgeColor() { - const c = colorAtIndex(AUTO_EDGE_COLORS, S.autoEdgeIdx) - S.autoEdgeIdx++ - return c -} - -// Computes the color/pie-slice data fields for a node given its list of groups. -// A single-group node gets one 100% slice (looks like a plain colored circle); -// a multi-group node gets one evenly-sized slice per group, in order. -// Slices are equal-sized. A node in more groups than there are slices shows -// its first PIE_MAX_SLICES - 1 groups plus one gray slice sized for the rest. -export function computeNodeVisualFields(groupsArr) { - const all = groupsArr && groupsArr.length ? groupsArr : [UNGROUPED] - const overflow = all.length > PIE_MAX_SLICES - const shown = overflow ? all.slice(0, PIE_MAX_SLICES - 1) : all - const slices = shown.map((g) => ({ color: colorForGroup(g), size: 100 / all.length })) - if (overflow) - slices.push({ color: NO_NODE_COLOR, size: (100 * (all.length - shown.length)) / all.length }) - const fields = { color: slices[0].color } - for (let i = 0; i < PIE_MAX_SLICES; i++) { - const idx = i + 1 - fields['pieColor' + idx] = i < slices.length ? slices[i].color : '#000000' - fields['pieSize' + idx] = i < slices.length ? slices[i].size.toFixed(3) + '%' : '0%' - } - return fields -} - -// Optional display order for groups (e.g. the line order of an annotation -// file); groups not listed keep node order, and "Not in any group" is last. -let groupOrderHint = null - -export function getUsedGroups() { - const groups = [ - ...new Set( - cy - .nodes() - .map((n) => n.data('groups') || []) - .flat() - ), - ] - const rank = (g) => - g === UNGROUPED - ? Infinity - : groupOrderHint && groupOrderHint.has(g) - ? groupOrderHint.get(g) - : 1e9 - return groups - .map((g, i) => [g, i]) - .sort((a, b) => rank(a[0]) - rank(b[0]) || a[1] - b[1]) - .map((x) => x[0]) -} - -export function setGroupOrder(names) { - groupOrderHint = Array.isArray(names) ? new Map(names.map((g, i) => [g, i])) : null -} - -// A node's *effective* groups are whichever of its assigned groups are -// currently active. Deactivating one of a node's several groups doesn't just -// toggle visibility -- the node is redrawn using only its remaining active -// groups, so a 2-group node with one group turned off becomes a plain single -// color instead of staying a two-slice pie. -export function effectiveGroupsFor(node) { - const groups = node.data('groups') || [] - return groups.filter((g) => S.activeGroups.has(g)) -} - -// One solid color across the whole node, used when node fill comes from -// each node's own "color" field (e.g. a NORMA node-coloring file). -function solidVisualFields(color) { - const fields = { color, pieColor1: color, pieSize1: '100%' } - for (let i = 2; i <= PIE_MAX_SLICES; i++) { - fields['pieColor' + i] = '#000000' - fields['pieSize' + i] = '0%' - } - return fields -} - -export function refreshNodeVisual(node) { - const eff = effectiveGroupsFor(node) - if (!eff.length) { - node.addClass('hidden-group') - return - } - if (node.hasClass('hidden-group')) node.removeClass('hidden-group') - const fill = nodeFillMode() - const visual = - fill === 'data' - ? solidVisualFields(node.data('nodeColor') || NO_NODE_COLOR) - : fill === 'values' - ? solidVisualFields( - node.data('valueColor') || - document.getElementById('valueMissing').value || - NO_NODE_COLOR - ) - : computeNodeVisualFields(eff) - visual.shape = nodeShapeFor(eff) - visual.labelColor = visual.color - if (visual.shape !== 'ellipse') { - // a single-color shaped node: the fill carries the color, no pie - for (let i = 1; i <= PIE_MAX_SLICES; i++) visual['pieSize' + i] = '0%' - } - // only write fields that changed, in a single update - const changed = {} - let any = false - for (const k in visual) { - if (node.data(k) !== visual[k]) { - changed[k] = visual[k] - any = true - } - } - if (any) node.data(changed) -} diff --git a/frontend/src/palette.ts b/frontend/src/palette.ts deleted file mode 100644 index eacbce9..0000000 --- a/frontend/src/palette.ts +++ /dev/null @@ -1,106 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -/* ---------- edge type palette (STRING-style evidence channels) ---------- */ -export const EDGE_TYPES = { - neighborhood: { color: '#59c46b', label: 'Gene neighborhood' }, - fusion: { color: '#d9534f', label: 'Gene fusion' }, - cooccurrence: { color: '#3a6cd6', label: 'Co-occurrence' }, - coexpression: { color: '#111827', label: 'Co-expression' }, - experiments: { color: '#b464c9', label: 'Experiments' }, - database: { color: '#8fc7e8', label: 'Curated database' }, - textmining: { color: '#c9c14a', label: 'Text mining' }, - homology: { color: '#caa6ea', label: 'Homology' }, -} - -export const ORIGINAL_EDGE_TYPES = JSON.parse(JSON.stringify(EDGE_TYPES)) - -// Colorblind-safe palettes, shared between node and edge use. -// Okabe-Ito: Okabe,M. and Ito,K. (2008) Color Universal Design (CUD) - a widely -// used 8-color palette designed to stay distinguishable under the common forms -// of color vision deficiency (protanopia, deuteranopia, tritanopia). -const CB_OKABE_ITO = [ - '#E69F00', - '#56B4E9', - '#009E73', - '#F0E442', - '#0072B2', - '#D55E00', - '#CC79A7', - '#000000', -] - -// IBM Design Language's colorblind-safe categorical set. -const CB_IBM = ['#648FFF', '#785EF0', '#DC267F', '#FE6100', '#FFB000'] - -// Viridis-inspired perceptually-uniform sequence (also colorblind-safe by design). -const CB_VIRIDIS = [ - '#440154', - '#482878', - '#3E4A89', - '#31688E', - '#26828E', - '#1F9E89', - '#35B779', - '#FDE725', -] - -// Once a category count exceeds a curated palette's length (all palettes here -// have 5-8 hand-picked colors), further colors are generated on the fly using -// a golden-angle hue rotation. Successive hues stay well-spread around the -// color wheel however many categories are added -- this is what lets the -// legends stay usable up to ~100 distinct edge channels or node groups -// instead of the palette repeating every 6-8 entries. -function hslToHex(h, s, l) { - s /= 100 - l /= 100 - const k = (n) => (n + h / 30) % 12 - const a = s * Math.min(l, 1 - l) - const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))) - const toHex = (x) => - Math.round(255 * x) - .toString(16) - .padStart(2, '0') - return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}` -} - -function goldenAngleColor(index) { - const hue = (index * 137.508) % 360 // golden angle, avoids hue clustering - const s = 62, - l = index % 2 === 0 ? 52 : 62 // alternate lightness for extra separation - return hslToHex(hue, s, l) -} - -export function colorAtIndex(palette, index) { - return index < palette.length ? palette[index] : goldenAngleColor(index) -} - -export const NODE_PALETTES = { - vivid: ['#e8a15f', '#5fd3c4', '#e07a7a', '#8fb8e8', '#c9c14a', '#a97fd6', '#7fd68a', '#d68fc0'], - sunset: ['#f4a261', '#e76f51', '#e9c46a', '#f2a65a', '#bc6c25', '#dda15e', '#ffb703', '#fb8500'], - ocean: ['#264653', '#2a9d8f', '#8ecae6', '#219ebc', '#023047', '#48cae4', '#90e0ef', '#00b4d8'], - forest: ['#606c38', '#283618', '#a3b18a', '#588157', '#3a5a40', '#344e41', '#6a994e', '#a7c957'], - pastel: ['#ffd6ff', '#c8b6ff', '#bde0fe', '#a2d2ff', '#ffafcc', '#caffbf', '#fdffb6', '#ffc6ff'], - mono: ['#3a4a6b', '#4c5f8a', '#6478a8', '#7d91c0', '#96a9d3', '#b0bfe0', '#c9d3ea', '#e2e7f4'], - cbOkabeIto: CB_OKABE_ITO, - cbIBM: CB_IBM, - viridis: CB_VIRIDIS, -} - -export const EDGE_PALETTES = { - categorical: [ - '#e07a7a', - '#5fd3c4', - '#e8a15f', - '#8fb8e8', - '#c9c14a', - '#a97fd6', - '#7fd68a', - '#d68fc0', - ], - warm: ['#e63946', '#f4a261', '#e9c46a', '#f77f00', '#d62828', '#fb8500', '#c1121f', '#ee9b00'], - cool: ['#264653', '#2a9d8f', '#457b9d', '#1d3557', '#48cae4', '#219ebc', '#023047', '#90e0ef'], - mono: ['#3a4a6b', '#4c5f8a', '#6478a8', '#7d91c0', '#96a9d3', '#b0bfe0', '#c9d3ea', '#e2e7f4'], - cbOkabeIto: CB_OKABE_ITO, - cbIBM: CB_IBM, - viridis: CB_VIRIDIS, -} diff --git a/frontend/src/parallel_edges.ts b/frontend/src/parallel_edges.ts deleted file mode 100644 index 96fa9d3..0000000 --- a/frontend/src/parallel_edges.ts +++ /dev/null @@ -1,80 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { applyEdgeLabels, scheduleBundling } from './profiler' -import { applyNodeSizing, invalidateFullMetrics } from './metrics' -import { currentTheme } from './themes' -import { cy } from './cy' -import { edgeIsDirected } from './export/dialog' - -/* ---------- parallel edges: merge per node pair ---------- */ -// In merge mode each node pair keeps one visible edge (its first edge in an -// active channel). That edge is widened by the number of active channels -// on the pair and turns gray when they disagree; the popup lists them all. -// Uses per-edge style bypasses so it layers over whatever width and color -// rules the stylesheet currently has. -function edgeMergeOn() { - const el = document.getElementById('edgeMergeMode') - return !!el && el.value === 'merge' -} - -export function applyEdgeMerge() { - if (typeof cy === 'undefined') return - const on = edgeMergeOn() - cy.batch(() => { - cy.edges('.merged-rep').forEach((e) => { - e.removeStyle('width line-color target-arrow-color') - e.removeClass('merged-rep') - }) - cy.edges('.merged-hidden').removeClass('merged-hidden') - cy.edges().forEach((e) => { - if (e.data('mergedWith')) e.data('mergedWith', null) - }) - if (!on) return - const byPair = new Map() - cy.edges().forEach((e) => { - if (e.hasClass('hidden-type')) return - const s = e.data('source'), - t = e.data('target') - // directed edges merge only with edges running the same way - const key = edgeIsDirected(e) - ? 'D\t' + s + '\t' + t - : 'U\t' + (s < t ? s + '\t' + t : t + '\t' + s) - const list = byPair.get(key) - if (list) list.push(e) - else byPair.set(key, [e]) - }) - const baseFixed = parseFloat(document.getElementById('edgeWidthFixed').value) || 2 - const byWeight = document.getElementById('edgeWidthMode').value === 'weight' - byPair.forEach((list) => { - if (list.length < 2) return - const [rep, ...rest] = list - rest.forEach((e) => e.addClass('merged-hidden')) - rep.data( - 'mergedWith', - list.map((e) => e.id()) - ) - const colors = new Set(list.map((e) => e.data('color'))) - const base = byWeight ? rep.data('edgeWidth') || baseFixed : baseFixed - rep.addClass('merged-rep') - rep.style({ - width: Math.min(base * (1 + 0.7 * (list.length - 1)), base * 6), - 'line-color': colors.size === 1 ? rep.data('color') : currentTheme.muted, - 'target-arrow-color': colors.size === 1 ? rep.data('color') : currentTheme.muted, - }) - }) - }) - applyEdgeLabels() - if (typeof scheduleBundling === 'function') scheduleBundling() -} - -export function applyTypeVisibility() { - cy.edges().forEach((e) => { - if (S.activeTypes.has(e.data('type'))) e.removeClass('hidden-type') - else e.addClass('hidden-type') - }) - applyEdgeMerge() - invalidateFullMetrics() - // if node sizes are being driven by a channel-scoped metric, keep them live - if (document.getElementById('sizeChannelOnly').checked) applyNodeSizing() -} diff --git a/frontend/src/profiler.ts b/frontend/src/profiler.ts deleted file mode 100644 index de1a316..0000000 --- a/frontend/src/profiler.ts +++ /dev/null @@ -1,2630 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { COMMUNITY_ALGORITHMS, WALKTRAP_NODE_LIMIT } from './welcome' -import { - CONFIG_FIELDS, - applyEdgePalette, - applyGroupVisibility, - applyNodePalette, - buildGroupLegend, - buildLegend, - byName, - drawGroupHulls, - filteredGroups, - getCurrentConfig, - getUsedTypes, - legendExtra, - loadData, - refreshAllDerivedUI, - sortedByName, -} from './hulls' -import { EDGE_TYPES, NODE_PALETTES, ORIGINAL_EDGE_TYPES, colorAtIndex } from './palette' -import { PROFILE_DIRECTED_STATS, directedStatsTable, profileDirected } from './directed_stats' -import { - PROFILE_PATH_LIMIT, - PROFILE_STATS, - formatStat, - profileGraph, - simpleGraph, - switchTab, -} from './wiring' -import { S } from './state' -import { - UNGROUPED, - attrKind, - attrRowsHtml, - attrSchema, - buildAttrSchema, - customGroupAttrs, - escapeHtml, - formatAttrValue, - getUsedGroups, - groupDescription, - groupLabel, - nodeFillMode, - refreshNodeVisual, - renderAttrSchema, - sanitizeColor, -} from './network_state' -import { WEBGL_ACTIVE, cy, setStyle } from './cy' -import { - addNormaEntry, - annotationText, - downloadText, - libEntry, - libSelection, - listSample, - nextPaint, - normaLibrary, - plural, - renderLibraryLists, - selectedNetworks, - setStatus, -} from './layouts/controls' -import { applyEdgeMerge, applyTypeVisibility } from './parallel_edges' -import { applyGroupsAndColorsInPlace, refreshLibraryView } from './library' -import { applyLabelColors } from './label_colors' -import { - applyNodeSizing, - bundleAsync, - cancelFrJobs, - ensureFullMetrics, - invalidateFullMetrics, - shownEdges, - shownNodes, - updateStats, -} from './metrics' -import { applyTheme, currentTheme } from './themes' -import { applyValueColors, scheduleLegend } from './clustering/mapping' -import { arrowsShown, edgeIsDirected } from './export/dialog' -import { - bumpDataVersion, - dataVersion, - flushHistory, - historyTimer, - setHistoryBaseline, - updateUndoButtons, -} from './demo_downloads' -import { capture3d, restore3d } from './view3d/tab' -import { fit3d } from './view3d/camera' -import { net3d } from './view3d/state' -import { refreshAfterDelete } from './api/tester' -import { runCommunityAlgorithm } from './clustering/mcl' -import { setLayoutBusy } from './layouts/run' -import { updateContextInfo } from './recording' - -/* ---------- profiler UI ---------- */ -export const profilerState = { results: [] } - -export function renderProfilerNetworkList() { - const el = document.getElementById('profNetList') - if (!el) return - const previous = new Set([...el.querySelectorAll('input:checked')].map((i) => i.value)) - const firstRender = !el.dataset.rendered - el.dataset.rendered = '1' - el.innerHTML = '' - const cur = typeof activeView === 'function' && activeView() - const options = [ - { - value: 'view', - label: `Current view: ${cur ? cur.name : 'untitled'} (${shownNodes().length} shown nodes)`, - }, - ].concat( - sortedByName(normaLibrary.network, (e) => e.name).map((e) => ({ value: e.id, label: e.name })) - ) - options.forEach((o) => { - const label = document.createElement('label') - const input = document.createElement('input') - input.type = 'checkbox' - input.value = o.value - input.checked = firstRender ? o.value === 'view' : previous.has(o.value) - const span = document.createElement('span') - span.textContent = o.label - label.append(input, span) - el.appendChild(label) - }) -} - -function graphForSource(value) { - if (value === 'view') { - const ids = shownNodes().map((n) => n.id()) - const shownE = shownEdges(true) - const pairs = shownE.map((e) => [e.data('source'), e.data('target')]) - const dirEdges = shownE.map((e) => [e.data('source'), e.data('target'), edgeIsDirected(e)]) - const viewIds = S.currentLibView ? S.currentLibView.nets.split('|') : [] - const nets = normaLibrary.network.filter((e) => viewIds.includes(e.id)) - const viewLabel = nets.length ? nets.map((e) => e.name).join(' + ') : 'Current view' - const cur = typeof activeView === 'function' && activeView() - return { - name: cur ? `View: ${cur.name}` : 'Current view', - label: viewLabel, - graph: simpleGraph(ids, pairs), - isView: true, - ids, - dirEdges, - } - } - const entry = libEntry('network', value) - if (!entry) return null - return { - name: entry.name, - graph: simpleGraph( - entry.parsed.nodes, - entry.parsed.edges.map((e) => [e.source, e.target]) - ), - ids: entry.parsed.nodes, - dirEdges: entry.parsed.edges.map((e) => [e.source, e.target, !!e.directed]), - isView: false, - entry, - } -} - -function runProfiler() { - const chosen = [...document.querySelectorAll('#profNetList input:checked')].map((i) => i.value) - if (!chosen.length) { - setStatus('profStatus', [{ level: 'error', text: 'Tick at least one network to profile.' }]) - return - } - if (chosen.includes('view') && !cy.nodes().length) { - setStatus('profStatus', [ - { - level: 'error', - text: 'The current view is empty. Load a network first, or untick "Current view".', - }, - ]) - return - } - const profText = (i) => - chosen.length > 1 - ? `Computing statistics: network ${i + 1} of ${chosen.length}…` - : 'Computing statistics…' - setStatus('profStatus', [ - { level: 'busy', text: profText(0), progress: chosen.length > 1 ? 0 : null }, - ]) - document.getElementById('btnProfile').disabled = true - // one network at a time, letting the progress paint in between - ;(async () => { - await nextPaint() - try { - const started = performance.now() - const useDir = document.getElementById('profDirected').checked - const results = [] - for (const [i, value] of chosen.entries()) { - if (i > 0) { - setStatus('profStatus', [ - { level: 'busy', text: profText(i), progress: i / chosen.length }, - ]) - await nextPaint() - } - const g = graphForSource(value) - if (!g) continue - const src = { ...g, sourceValue: value } - results.push({ - ...src, - stats: profileGraph(src.graph), - directed: - useDir && src.dirEdges.some((d) => d[2]) - ? profileDirected(src.ids, src.dirEdges) - : null, - }) - } - profilerState.results = results - renderProfilerResults(results) - const secs = ((performance.now() - started) / 1000).toFixed(2) - const notes = [ - { level: 'ok', text: `Profiled ${plural(results.length, 'network')} in ${secs} s.` }, - ] - if (results.some((r) => r.stats.pathsSkipped)) - notes.push({ - level: 'warn', - text: `Path-based statistics were skipped for networks above ${PROFILE_PATH_LIMIT.toLocaleString('en-US')} nodes.`, - }) - setStatus('profStatus', notes) - document.getElementById('btnProfileTsv').disabled = false - } catch (err) { - setStatus('profStatus', [ - { level: 'error', text: `The statistics couldn't be computed: ${err.message}` }, - ]) - } finally { - document.getElementById('btnProfile').disabled = false - } - })() -} - -export function renderProfilerResults(results) { - const root = document.getElementById('profResults') - root.innerHTML = '' - - // statistics table - const sec = profSection('Statistics', '') - const wrap = document.createElement('div') - wrap.className = 'table-wrap' - const table = document.createElement('table') - table.className = 'data' - const head = document.createElement('tr') - head.innerHTML = - 'Statistic' + - results.map((r) => `${escapeHtml(r.name)}`).join('') - const thead = document.createElement('thead') - thead.appendChild(head) - const tbody = document.createElement('tbody') - PROFILE_STATS.forEach((st) => { - const tr = document.createElement('tr') - tr.innerHTML = - `${escapeHtml(st.label)}${escapeHtml(st.desc)}` + - results.map((r) => `${escapeHtml(formatStat(r.stats[st.key]))}`).join('') - tbody.appendChild(tr) - }) - table.append(thead, tbody) - wrap.appendChild(table) - sec.appendChild(wrap) - const split = results.filter((r) => r.stats.components > 1).map((r) => r.name) - if (split.length) { - sec.insertAdjacentHTML( - 'beforeend', - `

    ${escapeHtml(listSample(split, 4))} ${split.length === 1 ? 'has' : 'have'} more than one component. As in igraph, closeness is then computed within each component, so closeness centralization can exceed 1 and isn't comparable with connected networks.

    ` - ) - } - root.appendChild(sec) - - if (results.some((r) => r.directed)) { - const dsec = profSection( - 'Direction', - 'Statistics that follow edge directions, for networks with directed edges. Undirected edges can be walked both ways. A dash means the network has no directed edges.' - ) - dsec.insertAdjacentHTML('beforeend', directedStatsTable(results)) - root.appendChild(dsec) - } - - // degree distributions - const degSec = profSection('Degree distribution', 'Number of nodes with each degree.') - const controls = document.createElement('div') - controls.className = 'inline-controls' - controls.innerHTML = '' - degSec.appendChild(controls) - const degCards = document.createElement('div') - degCards.className = 'cards' - degSec.appendChild(degCards) - const drawDeg = () => { - const log = document.getElementById('degLogLog').checked - degCards.innerHTML = '' - results.forEach((r) => { - const card = document.createElement('div') - card.className = 'card chart' - card.innerHTML = - `

    ${escapeHtml(r.name)}

    ` + - degreeChartSvg(r.stats.degree, log) - degCards.appendChild(card) - }) - } - controls.querySelector('input').addEventListener('change', drawDeg) - root.appendChild(degSec) - drawDeg() - - // central nodes - const cSec = profSection('Most central nodes', 'Top ten nodes by the chosen measure.') - const cControls = document.createElement('div') - cControls.className = 'inline-controls' - cControls.innerHTML = `` - cSec.appendChild(cControls) - const cCards = document.createElement('div') - cCards.className = 'cards' - cCards.style.gridTemplateColumns = 'repeat(auto-fill, minmax(400px, 1fr))' - cSec.appendChild(cCards) - const drawCentral = () => { - const by = document.getElementById('centralBy').value - cCards.innerHTML = '' - results.forEach((r) => { - const s = r.stats - const values = - by === 'degree' - ? s.degree - : by === 'betweenness' - ? s.betweenness - : by === 'closeness' - ? s.closeness - : s.localClustering - const card = document.createElement('div') - card.className = 'card' - card.innerHTML = `

    ${escapeHtml(r.name)}

    ` - if (!values) { - card.insertAdjacentHTML( - 'beforeend', - '

    Not computed for a network this large.

    ' - ) - cCards.appendChild(card) - return - } - const idx = Array.from({ length: r.graph.n }, (_, i) => i) - .filter((i) => Number.isFinite(values[i])) - .sort((a, b) => values[b] - values[a]) - .slice(0, 10) - const rows = idx - .map((i) => { - const id = r.graph.ids[i] - const inView = !!cy.getElementById(id).length - const nameCell = inView - ? `` - : escapeHtml(id) - return `${nameCell}${escapeHtml(formatStat(s.degree[i]))}${s.betweenness ? escapeHtml(formatStat(s.betweenness[i])) : '—'}${s.closeness ? escapeHtml(formatStat(s.closeness[i])) : '—'}` - }) - .join('') - card.insertAdjacentHTML( - 'beforeend', - `
    ${rows}
    NodeDegreeBetweennessCloseness
    ` - ) - cCards.appendChild(card) - }) - cCards - .querySelectorAll('button[data-node]') - .forEach((b) => b.addEventListener('click', () => focusNodeInNetwork(b.dataset.node))) - } - cControls.querySelector('select').addEventListener('change', drawCentral) - root.appendChild(cSec) - drawCentral() - - // communities - const lSec = profSection( - 'Communities', - 'Find communities with one of five algorithms and add them to Files as an annotation to view them as groups. Modularity is computed the same way for every algorithm.' - ) - const algo = profilerState.cmAlgo || 'louvain' - lSec.insertAdjacentHTML( - 'beforeend', - `
    - - - - -
    ` - ) - const lCards = document.createElement('div') - lCards.className = 'cards' - lSec.appendChild(lCards) - const drawCommunities = () => { - lCards.innerHTML = '' - results.forEach((r) => { - const lv = r.cmResult || r.stats.louvain - const label = r.cmLabel || 'Louvain' - const card = document.createElement('div') - card.className = 'card' - if (r.cmError) { - card.innerHTML = `

    ${escapeHtml(r.name)}

    ${escapeHtml(r.cmError)}

    ` - lCards.appendChild(card) - return - } - const nonSingle = lv.sizes.filter((x) => x > 1) - const sorted = [...lv.sizes].sort((a, b) => b - a) - const strip = sorted - .slice(0, 40) - .map( - (sz, i) => - `` - ) - .join('') - card.innerHTML = ` -

    ${escapeHtml(r.name)}

    -

    ${escapeHtml(label)}: ${plural(lv.count, 'community', 'communities')} (${nonSingle.length} with more than one node), modularity ${escapeHtml(formatStat(lv.modularity))}${r.cmMs !== undefined ? `, ${Math.round(r.cmMs)} ms` : ''}. Largest: ${sorted.slice(0, 5).join(', ')} nodes.

    - ` - const btn = document.createElement('button') - btn.type = 'button' - btn.textContent = 'Add as annotation' - btn.disabled = !nonSingle.length - btn.addEventListener('click', () => addCommunitiesAsAnnotation(r)) - card.appendChild(btn) - lCards.appendChild(card) - }) - } - const paramFor = (a) => - ({ - louvain: [ - 'Resolution', - 1, - 0.05, - 10, - 0.1, - 'Higher resolution gives more, smaller communities.', - ], - leiden: [ - 'Resolution', - 1, - 0.05, - 10, - 0.1, - 'Like Louvain, but every community is guaranteed to be connected.', - ], - lpa: [ - null, - null, - 0, - 0, - 0, - 'Each node takes the most common label among its neighbours until nothing changes.', - ], - walktrap: [ - 'Walk length', - 4, - 2, - 10, - 1, - `Short random walks tend to stay inside communities. Up to ${WALKTRAP_NODE_LIMIT.toLocaleString('en-US')} nodes.`, - ], - mcl: ['Inflation', 2, 1.1, 6, 0.1, 'Higher inflation gives more, smaller clusters.'], - })[a] - const syncParam = () => { - const a = lSec.querySelector('#cmAlgo').value - const [name, def, min, max, step, hint] = paramFor(a) - const input = lSec.querySelector('#cmParam') - lSec.querySelector('#cmParamLabel').hidden = !name - if (name) { - lSec.querySelector('#cmParamName').textContent = name - Object.assign(input, { min, max, step }) - const saved = profilerState.cmParams && profilerState.cmParams[a] - input.value = saved ?? def - } - lSec.querySelector('#cmHint').textContent = hint - } - lSec.querySelector('#cmAlgo').addEventListener('change', syncParam) - lSec.querySelector('#btnCommunities').addEventListener('click', () => { - const a = lSec.querySelector('#cmAlgo').value - const value = parseFloat(lSec.querySelector('#cmParam').value) - const [name, def, min, max] = paramFor(a) - const v = name ? Math.min(max, Math.max(min, Number.isFinite(value) ? value : def)) : null - profilerState.cmAlgo = a - profilerState.cmParams = { ...(profilerState.cmParams || {}), [a]: v } - const params = - a === 'walktrap' - ? { steps: Math.round(v) } - : a === 'mcl' - ? { inflation: v } - : { resolution: v } - const cmText = `Finding communities with ${COMMUNITY_ALGORITHMS[a].label}…` - setStatus('profStatus', [ - { level: 'busy', text: cmText, progress: results.length > 1 ? 0 : null }, - ]) - ;(async () => { - await nextPaint() - for (const [i, r] of results.entries()) { - if (i > 0) { - setStatus('profStatus', [{ level: 'busy', text: cmText, progress: i / results.length }]) - await nextPaint() - } - const t0 = performance.now() - try { - r.cmResult = runCommunityAlgorithm(r.graph, a, params) - r.cmError = null - } catch (err) { - r.cmResult = null - r.cmError = err.message - } - r.cmMs = performance.now() - t0 - r.cmAlgo = a - r.cmLabel = `${COMMUNITY_ALGORITHMS[a].label}${name ? ` (${name.toLowerCase()} ${a === 'walktrap' ? Math.round(v) : v})` : ''}` - } - drawCommunities() - setStatus('profStatus', [ - { level: 'ok', text: `Found communities with ${COMMUNITY_ALGORITHMS[a].label}.` }, - ]) - })() - }) - syncParam() - drawCommunities() - root.appendChild(lSec) - - // channels of the current view - const viewResult = results.find((r) => r.isView) - const channels = viewResult ? channelProfile() : null - if (channels) root.appendChild(renderChannelSection(channels)) -} - -export function profSection(title, sub) { - const sec = document.createElement('section') - sec.className = 'prof-section' - sec.innerHTML = - `

    ${escapeHtml(title)}

    ` + (sub ? `

    ${escapeHtml(sub)}

    ` : '') - return sec -} - -function degreeChartSvg(deg, logScale) { - const W = 340, - H = 170, - L = 40, - R = 10, - T = 10, - B = 28 - const counts = new Map() - let maxDeg = 0 - deg.forEach((d) => { - counts.set(d, (counts.get(d) || 0) + 1) - if (d > maxDeg) maxDeg = d - }) - const pw = W - L - R, - ph = H - T - B - let body = '' - let xLabels = '', - yLabels = '' - if (logScale) { - const pts = [...counts].filter(([d]) => d > 0) - if (!pts.length) return '

    No connected nodes to plot.

    ' - const maxC = Math.max(...pts.map((p) => p[1])) - const lx = Math.log10(Math.max(maxDeg, 10)), - ly = Math.log10(Math.max(maxC, 10)) - pts.forEach(([d, c]) => { - const x = L + (Math.log10(d) / lx) * pw - const y = T + ph - (Math.log10(c) / ly) * ph - body += `degree ${d}: ${c} nodes` - }) - xLabels = `1${Math.round(Math.pow(10, lx))}` - yLabels = `1${Math.round(Math.pow(10, ly))}` - } else { - const binWidth = Math.max(1, Math.ceil((maxDeg + 1) / 60)) - const bins = new Array(Math.floor(maxDeg / binWidth) + 1).fill(0) - counts.forEach((c, d) => { - bins[Math.floor(d / binWidth)] += c - }) - const maxC = Math.max(...bins, 1) - const bw = pw / bins.length - bins.forEach((c, i) => { - if (!c) return - const h = (c / maxC) * ph - const lo = i * binWidth, - hi = lo + binWidth - 1 - body += `degree ${binWidth > 1 ? `${lo}–${hi}` : lo}: ${c} nodes` - }) - xLabels = `0${maxDeg}` - yLabels = `0${maxC}` - } - return ` - - - ${body}${xLabels}${yLabels} - degree - ` -} - -function focusNodeInNetwork(id) { - const node = cy.getElementById(id) - if (!node.length) return - switchTab('network') - if (node.hasClass('hidden-group')) { - ;(node.data('groups') || []).forEach((g) => S.activeGroups.add(g)) - buildGroupLegend() - applyGroupVisibility() - } - cy.animate( - { center: { eles: node }, zoom: Math.max(cy.zoom(), 1.2) }, - { duration: 300, complete: clampViewport } - ) - node.emit('tap') -} - -function addCommunitiesAsAnnotation(result) { - const lv = result.cmResult || result.stats.louvain - const algoName = COMMUNITY_ALGORITHMS[result.cmAlgo || 'louvain'].short - const members = new Map() - result.graph.ids.forEach((id, i) => { - const c = lv.membership[i] - if (!members.has(c)) members.set(c, []) - members.get(c).push(id) - }) - const groups = [...members.values()] - .filter((list) => list.length > 1) - .sort((a, b) => b.length - a.length) - .map((list, i) => ({ name: `Community-${i + 1}`, members: list })) - const skippedNodes = result.graph.n - groups.reduce((s, g) => s + g.members.length, 0) - const entry = addNormaEntry( - 'annotation', - `${result.label || result.name} ${algoName} communities`, - annotationText(groups), - '' - ) - libSelection.annotation = entry.id - renderLibraryLists() - const notes = [ - { - level: 'ok', - text: `Added the annotation "${entry.name}" with ${plural(groups.length, 'community', 'communities')} to Files and selected it.`, - }, - ] - if (skippedNodes) - notes.push({ - level: 'ok', - text: `${plural(skippedNodes, 'node')} that form a community on their own ${skippedNodes === 1 ? 'was' : 'were'} left out.`, - }) - if (result.isView && S.currentLibView) { - notes.push({ - level: 'ok', - text: 'Show them in this view to see the communities as groups.', - action: { - label: 'Show in this view', - run: () => { - refreshLibraryView() - switchTab('network') - }, - }, - }) - } else if (result.isView) { - notes.push({ - level: 'ok', - text: 'The current view came from an example or JSON rather than Files, so the communities can be applied to it here.', - action: { - label: 'Show as groups now', - run: () => { - applyCommunitiesToView(groups) - switchTab('network') - }, - }, - }) - } else { - notes.push({ - level: 'ok', - text: `Tick "${result.name}" under Files and choose Show in this view to see them.`, - }) - } - setStatus('profStatus', notes) -} - -// For views that didn't come from Files: set groups directly. -function applyCommunitiesToView(groups) { - const groupsOf = {} - groups.forEach((g) => g.members.forEach((id) => (groupsOf[id] = groupsOf[id] || []).push(g.name))) - document.getElementById('nodeFillSelect').value = 'groups' - applyGroupsAndColorsInPlace( - groupsOf, - new Map(), - groups.map((g) => g.name) - ) -} - -function channelProfile() { - const types = getUsedTypes() - if (types.length < 2) return null - const pairsByType = new Map(types.map((t) => [t, new Set()])) - const typesByPair = new Map() - cy.edges().forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (s === t) return - const key = s < t ? s + '\t' + t : t + '\t' + s - const type = e.data('type') - pairsByType.get(type).add(key) - if (!typesByPair.has(key)) typesByPair.set(key, new Set()) - typesByPair.get(key).add(type) - }) - const perPair = new Map() - typesByPair.forEach((set) => { - perPair.set(set.size, (perPair.get(set.size) || 0) + 1) - }) - return { - types, - pairsByType, - perPair, - totalPairs: typesByPair.size, - totalEdges: cy.edges().length, - } -} - -function renderChannelSection(ch) { - const sec = profSection( - 'Edge channels in the current view', - `${plural(ch.totalEdges, 'edge')} over ${plural(ch.totalPairs, 'connected node pair')}.` - ) - const byCount = [...ch.perPair] - .sort((a, b) => a[0] - b[0]) - .map( - ([k, v]) => - `${plural(k, 'channel')}${v.toLocaleString('en-US')}${((100 * v) / ch.totalPairs).toFixed(1)}%` - ) - .join('') - const shown = ch.types.slice(0, 12) - const matrixHead = - 'ChannelPairs' + - shown.map((t, i) => `${i + 1}`).join('') + - '' - const matrixRows = shown - .map((a, i) => { - const A = ch.pairsByType.get(a) - const cells = shown - .map((b, j) => { - if (i === j) return '—' - const B = ch.pairsByType.get(b) - let shared = 0 - const [small, big] = A.size < B.size ? [A, B] : [B, A] - small.forEach((k) => { - if (big.has(k)) shared++ - }) - const union = A.size + B.size - shared - const jac = union ? shared / union : 0 - return `${jac.toFixed(2)}` - }) - .join('') - return `${i + 1}. ${escapeHtml(a)}${A.size.toLocaleString('en-US')}${cells}` - }) - .join('') - sec.insertAdjacentHTML( - 'beforeend', - ` -
    -
    -

    Node pairs by number of channels

    -
    ${byCount}
    Carried byPairsShare
    -
    -
    -

    Channel overlap (Jaccard index of node pairs)

    -

    1 means two channels connect exactly the same pairs; 0 means they share none.${ch.types.length > shown.length ? ` Showing the first ${shown.length} of ${ch.types.length} channels.` : ''}

    -
    ${matrixHead}${matrixRows}
    -
    -
    ` - ) - return sec -} - -function profilerTsv() { - const results = profilerState.results - if (!results.length) return - const lines = [['Statistic', ...results.map((r) => r.name)].join('\t')] - PROFILE_STATS.forEach((st) => - lines.push([st.label, ...results.map((r) => formatStat(r.stats[st.key]))].join('\t')) - ) - if (results.some((r) => r.directed)) { - PROFILE_DIRECTED_STATS.forEach((st) => - lines.push( - [st.label, ...results.map((r) => (r.directed ? formatStat(r.directed[st.key]) : ''))].join( - '\t' - ) - ) - ) - } - downloadText('network-profile.tsv', lines.join('\n') + '\n') -} - -/* labels */ -// Node label size per node: fixed, or grown in proportion to node size. -export function applyLabelSizes() { - const base = parseFloat(document.getElementById('nodeLabelSize').value) || 12 - const grow = document.getElementById('labelScaleWithNode').checked - cy.batch(() => { - cy.nodes().forEach((n) => { - const px = grow ? Math.max(3, (base * (n.data('size') || 42)) / 42) : base - if (n.data('labelPx') !== px) n.data('labelPx', px) - }) - }) -} - -// Edge label text. A merged edge speaks for all the parallel edges it stands for. -function edgeLabelText(e, mode) { - const merged = e.data('mergedWith') - const group = merged && merged.length > 1 ? merged.map((id) => cy.getElementById(id)) : [e] - const channel = - group.length > 1 - ? `${group.length} channels` - : (EDGE_TYPES[e.data('type')] || {}).label || e.data('type') - const weights = group.map((x) => x.data('weight')).filter((w) => typeof w === 'number') - const weight = weights.length ? formatAttrValue(Math.max(...weights)) : '' - if (mode === 'weight') return weight - if (mode === 'type+weight') return weight ? `${channel} (${weight})` : channel - if (mode.startsWith('attr:')) { - const key = mode.slice(5) - const values = group - .map((x) => (x.data('attrs') || {})[key]) - .filter((v) => attrKind(v) !== 'empty') - return [...new Set(values.map(formatAttrValue))].join(', ') - } - return channel -} - -export function applyEdgeLabels() { - if (!document.getElementById('showEdgeLabels').checked) return - const mode = document.getElementById('edgeLabelContent').value || 'type' - cy.batch(() => { - cy.edges().forEach((e) => { - const text = edgeLabelText(e, mode) - if (e.data('elabel') !== text) e.data('elabel', text) - }) - }) -} - -// Keeps the "attribute" choices in the edge label list in step with the -// edge attributes of the loaded network. -export function refreshEdgeLabelOptions() { - const sel = document.getElementById('edgeLabelContent') - const current = sel.value - ;[...sel.querySelectorAll('option[data-attr]')].forEach((o) => o.remove()) - Object.keys(attrSchema.edge || {}).forEach((key) => { - const opt = new Option(`Text: attribute "${key}"`, 'attr:' + key) - opt.dataset.attr = '1' - sel.add(opt) - }) - sel.value = [...sel.options].some((o) => o.value === current) ? current : 'type' -} - -export function updateLabelStyle() { - const showNode = document.getElementById('showNodeLabels').checked - const showEdge = document.getElementById('showEdgeLabels').checked - const pos = document.getElementById('labelPosition').value - const nodePx = parseFloat(document.getElementById('nodeLabelSize').value) || 12 - const edgePx = parseFloat(document.getElementById('edgeLabelSize').value) || 9 - const minPx = parseFloat(document.getElementById('labelMinScreenSize').value) || 0 - document.getElementById('nodeLabelSizeValue').textContent = `${nodePx} px` - document.getElementById('edgeLabelSizeValue').textContent = `${edgePx} px` - document.getElementById('edgeLabelControls').hidden = !showEdge - - let valign = 'center', - halign = 'center', - marginY = 0, - marginX = 0 - if (pos === 'top') { - valign = 'top' - marginY = -4 - } else if (pos === 'bottom') { - valign = 'bottom' - marginY = 4 - } else if (pos === 'left') { - halign = 'left' - marginX = -6 - } else if (pos === 'right') { - halign = 'right' - marginX = 6 - } - - applyLabelSizes() - applyEdgeLabels() - setStyle('node', { - label: showNode ? 'data(id)' : '', - 'text-valign': valign, - 'text-halign': halign, - 'text-margin-y': marginY, - 'text-margin-x': marginX, - 'min-zoomed-font-size': minPx, - }) - setStyle('edge', { - label: showEdge ? 'data(elabel)' : '', - 'font-size': edgePx, - 'font-family': 'Inter, sans-serif', - color: currentTheme.text, - 'text-background-color': currentTheme.bg, - 'text-background-opacity': 0.85, - 'text-background-padding': 2, - 'text-background-shape': 'roundrectangle', - 'text-rotation': document.getElementById('edgeLabelOrientation').value, - 'min-zoomed-font-size': minPx, - }) - applyEdgeCurveStyle() - applyLabelColors() -} - -/* edge opacity */ -export function applyEdgeOpacity() { - const o = parseFloat(document.getElementById('edgeOpacity').value) || 0.85 - document.getElementById('edgeOpacityValue').textContent = o.toFixed(2) - setStyle('edge', { opacity: o }) - setStyle('edge.dimmed', { opacity: Math.min(0.04, o) }) -} - -/* spread: nodes repel (right) or attract (left) each other. - The slider sets the strength of repulsion (which acts between all nearby - nodes) against the pull of the links, from a quarter to four times the - current arrangement (the "base"). Each pair of nearby nodes, plus a few - distant anchors, gets a target distance from the base: pushed apart, - unlinked neighbours separate most and links resist, so linked groups - stay together and move apart; drawn together, links shorten most and - nodes stop before they overlap. Stress majorization moves the nodes - toward those distances, keeping the arrangement. At 1x the targets are - the base distances, and returning to 1x restores the base exactly. - Dragging nodes, undo and layouts start a new base. */ -let spreadApplied = 0 - -export let spreadBase = null -// { v0, ids, pos, out } -let spreadFrame = 0 - -function spreadFactor(v) { - return Math.pow(2, v) -} - -export function updateSpreadReadout() { - const v = parseFloat(document.getElementById('spreadSlider').value) || 0 - document.getElementById('spreadValue').textContent = spreadFactor(v).toFixed(2) + '×' -} - -// The positions the slider last produced, if the nodes are still there. -function spreadBaseValid(nodes) { - const b = spreadBase - if (!b || b.ids.length !== nodes.length) return false - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i] - if (n.id() !== b.ids[i]) return false - const p = n.position() - if (Math.abs(p.x - b.out[2 * i]) > 1e-6 || Math.abs(p.y - b.out[2 * i + 1]) > 1e-6) return false - } - return true -} - -// Nodes may come as close as they were in the base, but not overlap more. -function spreadSeparate(X, B, n, radii, passes) { - const gap = 4 - const maxR = radii.reduce((a, r) => Math.max(a, r), 0) - if (!(maxR > 0)) return - const size = 2 * maxR + gap - for (let pass = 0; pass < passes; pass++) { - const grid = new Map() - for (let i = 0; i < n; i++) { - const key = - (Math.floor(X[2 * i] / size) + 50000) * 100003 + Math.floor(X[2 * i + 1] / size) + 50000 - let c = grid.get(key) - if (!c) grid.set(key, (c = [])) - c.push(i) - } - let moved = false - grid.forEach((list, key) => { - for (let ox = -1; ox <= 1; ox++) - for (let oy = -1; oy <= 1; oy++) { - const other = grid.get(key + ox * 100003 + oy) - if (!other) continue - for (const i of list) - for (const j of other) { - if (i >= j) continue - let dx = X[2 * i] - X[2 * j], - dy = X[2 * i + 1] - X[2 * j + 1] - const d = Math.hypot(dx, dy) - const want = Math.min( - radii[i] + radii[j] + gap, - Math.hypot(B[2 * i] - B[2 * j], B[2 * i + 1] - B[2 * j + 1]) - ) - if (d >= want - 1e-6) continue - let len = d - if (d < 1e-9) { - dx = (i + j) % 2 ? 1 : -1 - dy = 0 - len = 1 - } - const push = (want - d) / 2 / len - X[2 * i] += dx * push - X[2 * i + 1] += dy * push - X[2 * j] -= dx * push - X[2 * j + 1] -= dy * push - moved = true - } - } - }) - if (!moved) break - } -} - -// Target distances from the base: repulsion acts on every pair of nearby -// nodes and attraction only along links, so pushing apart stretches unlinked -// pairs most (links resist) and drawing together shortens links most -// (unlinked pairs follow less). Crowded pairs change most, distant pairs -// least. Stress majorization then moves the nodes toward those distances. -function spreadRelax(base, n, edges, radii, rel) { - const B = base.pos - if (n < 2) return B.slice() - const dist = (i, j) => Math.hypot(B[2 * i] - B[2 * j], B[2 * i + 1] - B[2 * j + 1]) - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity - for (let i = 0; i < n; i++) { - minX = Math.min(minX, B[2 * i]) - maxX = Math.max(maxX, B[2 * i]) - minY = Math.min(minY, B[2 * i + 1]) - maxY = Math.max(maxY, B[2 * i + 1]) - } - const cell = Math.max(Math.sqrt(Math.max((maxX - minX) * (maxY - minY), 1) / n) * 2, 1e-6) - const cellOf = (i) => [ - Math.floor((B[2 * i] - minX) / cell), - Math.floor((B[2 * i + 1] - minY) / cell), - ] - const cells = new Map() - for (let i = 0; i < n; i++) { - const [gx, gy] = cellOf(i), - key = gx * 100003 + gy - let c = cells.get(key) - if (!c) cells.set(key, (c = [])) - c.push(i) - } - const NN = 12, - FAR = 12 - const pairs = new Map() - const addPair = (i, j, linked) => { - if (i === j) return - const a = Math.min(i, j), - b = Math.max(i, j), - key = a * n + b - const had = pairs.get(key) - if (had) { - if (linked) had[2] = true - return - } - pairs.set(key, [a, b, linked]) - } - for (let e = 0; e < edges.length; e += 2) addPair(edges[e], edges[e + 1], true) - const nnDist = [] - for (let i = 0; i < n; i++) { - const [gx, gy] = cellOf(i) - let cand = [] - for (let r = 1; r <= 4 && cand.length < NN; r++) { - cand = [] - for (let ox = -r; ox <= r; ox++) - for (let oy = -r; oy <= r; oy++) { - const c = cells.get((gx + ox) * 100003 + gy + oy) - if (c) for (const j of c) if (j !== i) cand.push(j) - } - } - cand.sort((a, b) => dist(i, a) - dist(i, b)) - cand.slice(0, NN).forEach((j) => addPair(i, j, false)) - if (cand.length && dist(i, cand[0]) > 1e-9) nnDist.push(dist(i, cand[0])) - let seed = (i * 2654435761) >>> 0 - for (let f = 0; f < FAR; f++) { - seed = (seed * 1664525 + 1013904223) >>> 0 - addPair(i, seed % n, false) - } - } - nnDist.sort((a, b) => a - b) - const k = nnDist.length ? nnDist[nnDist.length >> 1] * 2 : 50 - - const lr = Math.log(rel) - const P = [] - pairs.forEach(([i, j, linked]) => { - const d0 = dist(i, j) - if (d0 < 1e-9) return - const ratio = d0 / k - const near = ratio <= 1 ? 1 : ratio >= 12 ? 0.08 : 1 - (0.92 * Math.log(ratio)) / Math.log(12) - const follows = rel >= 1 ? (linked ? 0.6 : 1) : linked ? 1 : 0.6 - let t = d0 * Math.exp(lr * near * follows) - if (rel < 1) t = Math.max(t, Math.min(d0, radii[i] + radii[j] + 4)) - P.push(i, j, t) - }) - const X = B.slice() - const iters = n > 2500 ? 30 : n > 800 ? 45 : 70 - const nx = new Float64Array(2 * n), - ws = new Float64Array(n) - let cx = 0, - cyy = 0 - for (let i = 0; i < n; i++) { - cx += B[2 * i] - cyy += B[2 * i + 1] - } - cx /= n - cyy /= n - for (let it = 0; it < iters; it++) { - nx.fill(0) - ws.fill(0) - for (let p = 0; p < P.length; p += 3) { - const i = P[p], - j = P[p + 1], - t = P[p + 2] - let dx = X[2 * i] - X[2 * j], - dy = X[2 * i + 1] - X[2 * j + 1] - let d = Math.hypot(dx, dy) - if (d < 1e-9) { - dx = 1e-3 - dy = 0 - d = 1e-3 - } - const w = 1 / t - const ux = (dx / d) * t, - uy = (dy / d) * t - nx[2 * i] += w * (X[2 * j] + ux) - nx[2 * i + 1] += w * (X[2 * j + 1] + uy) - ws[i] += w - nx[2 * j] += w * (X[2 * i] - ux) - nx[2 * j + 1] += w * (X[2 * i + 1] - uy) - ws[j] += w - } - for (let i = 0; i < n; i++) { - if (ws[i] > 0) { - X[2 * i] = nx[2 * i] / ws[i] - X[2 * i + 1] = nx[2 * i + 1] / ws[i] - } - } - spreadSeparate(X, B, n, radii, 6) - } - spreadSeparate(X, B, n, radii, 150) - let mx = 0, - my = 0 - for (let i = 0; i < n; i++) { - mx += X[2 * i] - my += X[2 * i + 1] - } - mx = mx / n - cx - my = my / n - cyy - for (let i = 0; i < n; i++) { - X[2 * i] -= mx - X[2 * i + 1] -= my - } - return X -} - -function applySpread() { - const v = parseFloat(document.getElementById('spreadSlider').value) || 0 - updateSpreadReadout() - const nodes = cy.nodes() - if (!nodes.length) { - spreadApplied = v - return - } - if (!spreadBaseValid(nodes)) { - const ids = nodes.map((nd) => nd.id()) - const pos = new Float64Array(2 * nodes.length) - nodes.forEach((nd, i) => { - const p = nd.position() - pos[2 * i] = p.x - pos[2 * i + 1] = p.y - }) - spreadBase = { v0: spreadApplied, ids, pos, out: pos } - } - spreadApplied = v - const b = spreadBase - const rel = spreadFactor(v - b.v0) - let out - if (Math.abs(v - b.v0) < 1e-9) out = b.pos - else { - const index = new Map(b.ids.map((id, i) => [id, i])) - const seen = new Set(), - edges = [] - cy.edges().forEach((e) => { - const i = index.get(e.source().id()), - j = index.get(e.target().id()) - if (i === undefined || j === undefined || i === j) return - const key = i < j ? i * nodes.length + j : j * nodes.length + i - if (seen.has(key)) return - seen.add(key) - edges.push(i, j) - }) - const radii = nodes.map((nd) => (nd.width() || 0) / 2) - out = spreadRelax(b, nodes.length, edges, radii, rel) - } - b.out = out - cy.batch(() => { - nodes.forEach((nd, i) => nd.position({ x: out[2 * i], y: out[2 * i + 1] })) - }) - drawGroupHulls() -} - -// Large networks take a moment per step, so they follow the slider when it -// is released rather than while it is dragged. -const SPREAD_LIVE_MAX = 1500 - -function queueSpread() { - updateSpreadReadout() - if (spreadFrame || cy.nodes().length > SPREAD_LIVE_MAX) return - spreadFrame = requestAnimationFrame(() => { - spreadFrame = 0 - applySpread() - }) -} - -function resetSpread() { - spreadApplied = 0 - spreadBase = null - document.getElementById('spreadSlider').value = 0 - updateSpreadReadout() -} - -/* edge curve style (straight / curved / bundled) */ -export const BUNDLE_MAX_EDGES = 10000 - -const BUNDLED_STYLE_PROPS = - 'curve-style control-point-distances control-point-weights edge-distances' - -let bundleSeq = 0 - -let bundleTimer = null - -let bundleSignature = '' - -function bundlingOn() { - return document.getElementById('edgeCurveStyle').value === 'bundled' -} - -function clearBundles() { - bundleSignature = '' - lastBundle = null - cy.batch(() => - cy.edges('.bundled').forEach((e) => { - e.removeStyle(BUNDLED_STYLE_PROPS) - e.removeClass('bundled') - }) - ) -} - -export function applyEdgeCurveStyle() { - const curveStyle = document.getElementById('edgeCurveStyle').value - const curvature = parseInt(document.getElementById('edgeCurvature').value, 10) || 40 - document.getElementById('curvatureValue').textContent = curvature - document.getElementById('curvatureRow').style.display = curveStyle === 'bezier' ? 'block' : 'none' - document.getElementById('bundleRow').hidden = curveStyle !== 'bundled' - document.getElementById('bundleStrengthValue').textContent = - document.getElementById('bundleStrength').value + '%' - - // Cytoscape can't draw labels on haystack edges; with edge labels on, - // straight edges use the (equally straight) 'straight' style instead. - // Bundled edges start straight and are bent individually once computed. - const labelsOn = document.getElementById('showEdgeLabels').checked - let effective = curveStyle - if (curveStyle === 'bundled') effective = 'straight' - else if ( - curveStyle === 'haystack' && - (labelsOn || (typeof arrowsShown === 'function' && arrowsShown())) - ) - effective = 'straight' - setStyle('edge', { - 'curve-style': effective, - 'control-point-step-size': curvature, - 'haystack-radius': 0.4, - }) - - if (curveStyle === 'bundled') scheduleBundling(true) - else { - bundleSeq++ - clearBundles() - setStatus('bundleStatus', []) - } -} - -// Recomputes bundles shortly after the drawing changes. Repeated triggers -// within the delay collapse into one run. -export function scheduleBundling(force) { - if (!bundlingOn()) return - if (force) bundleSignature = '' - clearTimeout(bundleTimer) - bundleTimer = setTimeout(runBundling, 250) -} - -// The last computed bundles, kept so they can be re-applied cheaply when -// node sizes change (which changes which bend points fall inside nodes). -var lastBundle = null - -// Bends each edge through its bundled points. Points that fall inside the -// source or target node are left out: Cytoscape can't find where such an -// edge leaves its node and silently skips drawing it. -export function applyBundleResult() { - if (!lastBundle || !bundlingOn()) return - const { list, segs, result } = lastBundle - const keep = new Set(list.map((e) => e.id())) - cy.batch(() => { - cy.edges('.bundled').forEach((e) => { - if (!keep.has(e.id())) { - e.removeStyle(BUNDLED_STYLE_PROPS) - e.removeClass('bundled') - } - }) - list.forEach((e, k) => { - const pts = result[k] - if (!pts || e.removed()) return - const s = segs[k] - const vx = s.tx - s.sx, - vy = s.ty - s.sy - const L2 = vx * vx + vy * vy - if (L2 < 1e-9) return - const L = Math.sqrt(L2) - const srcR = e.source().outerWidth() / 2 + 3 - const tgtR = e.target().outerWidth() / 2 + 3 - const weights = [], - distances = [] - for (let i = 0; i < pts.length; i += 2) { - if (Math.hypot(pts[i] - s.sx, pts[i + 1] - s.sy) <= srcR) continue - if (Math.hypot(pts[i] - s.tx, pts[i + 1] - s.ty) <= tgtR) continue - const px = pts[i] - s.sx, - py = pts[i + 1] - s.sy - // rounded: Cytoscape's style parser rejects exponent notation (1e-15) - weights.push( - Math.round(Math.min(0.999, Math.max(0.001, (px * vx + py * vy) / L2)) * 1000) / 1000 - ) - // Cytoscape measures positive distances to the left of source->target - distances.push(Math.round(((py * vx - px * vy) / L) * 100) / 100) - } - if (!weights.length || distances.every((d) => d === 0)) { - if (e.hasClass('bundled')) { - e.removeStyle(BUNDLED_STYLE_PROPS) - e.removeClass('bundled') - } - return - } - e.style({ - 'curve-style': 'unbundled-bezier', - 'edge-distances': 'node-position', - 'control-point-weights': weights, - 'control-point-distances': distances, - }) - e.addClass('bundled') - }) - }) -} - -async function runBundling() { - if (!bundlingOn()) return - const edges = cy.edges(':visible').filter((e) => e.data('source') !== e.data('target')) - const strength = (parseFloat(document.getElementById('bundleStrength').value) || 50) / 100 - const signature = - strength + - '|' + - edges - .map((e) => { - const a = e.source().position(), - b = e.target().position() - return ( - e.id() + - ':' + - Math.round(a.x) + - ',' + - Math.round(a.y) + - ',' + - Math.round(b.x) + - ',' + - Math.round(b.y) - ) - }) - .join(';') - if (signature === bundleSignature) return - if (edges.length > BUNDLE_MAX_EDGES) { - clearBundles() - setStatus('bundleStatus', [ - { - level: 'warn', - text: `Bundling handles up to ${BUNDLE_MAX_EDGES.toLocaleString()} visible edges; this view shows ${edges.length.toLocaleString()}. Untick some channels or groups, or merge parallel edges.`, - }, - ]) - return - } - const run = ++bundleSeq - setStatus( - 'bundleStatus', - edges.length > 300 - ? [{ level: 'busy', text: `Bundling ${edges.length.toLocaleString()} edges…`, progress: 0 }] - : [] - ) - const list = edges.toArray() - const segs = list.map((e) => { - const a = e.source().position(), - b = e.target().position() - return { sx: a.x, sy: a.y, tx: b.x, ty: b.y } - }) - const result = await bundleAsync( - segs, - { - threshold: 0.85 - 0.5 * strength, - iterations: Math.round(30 + 60 * strength), - }, - (f) => { - if (run === bundleSeq && edges.length > 300) - setStatus('bundleStatus', [ - { level: 'busy', text: `Bundling ${edges.length.toLocaleString()} edges…`, progress: f }, - ]) - } - ) - if (run !== bundleSeq || !result || !bundlingOn()) return - bundleSignature = signature - lastBundle = { list, segs, result } - applyBundleResult() - setStatus('bundleStatus', []) -} - -/* edge thickness */ -export function applyEdgeWidth() { - const mode = document.getElementById('edgeWidthMode').value - document.getElementById('edgeWidthFixedRow').style.display = mode === 'fixed' ? 'block' : 'none' - document.getElementById('edgeWidthWeightRow').style.display = mode === 'weight' ? 'block' : 'none' - - if (mode === 'fixed') { - const w = parseFloat(document.getElementById('edgeWidthFixed').value) || 2 - document.getElementById('edgeWidthFixedValue').textContent = w - setStyle('edge', { width: w }) - setStyle('edge.highlighted', { width: w * 1.6 }) - } else { - const minW = parseFloat(document.getElementById('edgeWidthMin').value) || 1 - const maxW = parseFloat(document.getElementById('edgeWidthMax').value) || 8 - const knownWeights = cy - .edges() - .map((e) => e.data('weight')) - .filter((w) => typeof w === 'number' && !isNaN(w)) - const lo = knownWeights.length ? Math.min(...knownWeights) : 0 - const hi = knownWeights.length ? Math.max(...knownWeights) : 1 - cy.edges().forEach((e) => { - const w = e.data('weight') - const v = typeof w === 'number' && !isNaN(w) ? w : (lo + hi) / 2 - const width = hi === lo ? (minW + maxW) / 2 : minW + ((v - lo) / (hi - lo)) * (maxW - minW) - e.data('edgeWidth', width) - }) - setStyle('edge', { width: 'data(edgeWidth)' }) - setStyle('edge.highlighted', { width: 'data(edgeWidth)' }) - } - applyEdgeMerge() -} - -/* ============================================================ - VIEWPORT: unbounded zoom, but the network never leaves the frame - Zoom is effectively unlimited (see minZoom/maxZoom). After every pan - or zoom the view is nudged so that: - - a network smaller than the canvas stays entirely inside it, and - - a network larger than the canvas always covers it, so there is - no panning off into empty space. - The model-space bounding box of the visible nodes is cached and only - recomputed when nodes move, appear or disappear, so the check itself - is cheap enough to run on every viewport change. - ============================================================ */ -const VIEW_MARGIN = 40 - -var viewBoxCache = null - -var clampingViewport = false - -export function invalidateViewBox() { - viewBoxCache = null -} - -function visibleModelBox() { - if (!viewBoxCache) { - const nodes = cy.nodes(':visible') - viewBoxCache = nodes.length - ? nodes.boundingBox({ includeLabels: false, includeOverlays: false }) - : 'empty' - } - return viewBoxCache === 'empty' ? null : viewBoxCache -} - -function axisCorrection(lo, hi, size) { - const m = Math.min(VIEW_MARGIN, size / 4) - if (hi - lo <= size - 2 * m) { - if (lo < m) return m - lo - if (hi > size - m) return size - m - hi - return 0 - } - if (lo > m) return m - lo - if (hi < size - m) return size - m - hi - return 0 -} - -function clampViewport() { - if (clampingViewport || cy.animated()) return - const W = cy.width(), - H = cy.height() - if (!W || !H) return - const bb = visibleModelBox() - if (!bb) return - const z = cy.zoom(), - pan = cy.pan() - const dx = axisCorrection(bb.x1 * z + pan.x, bb.x2 * z + pan.x, W) - const dy = axisCorrection(bb.y1 * z + pan.y, bb.y2 * z + pan.y, H) - if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { - clampingViewport = true - cy.panBy({ x: dx, y: dy }) - clampingViewport = false - } -} - -export function formatZoom(z) { - const pct = z * 100 - if (pct >= 1e5 || pct < 0.01) return pct.toExponential(0).replace('e+', 'e') + '%' - if (pct >= 100) return Math.round(pct).toLocaleString() + '%' - if (pct >= 1) return pct.toFixed(pct < 10 ? 1 : 0) + '%' - return pct.toPrecision(2) + '%' -} - -export function updateZoomReadout() { - document.getElementById('zoomLevel').textContent = formatZoom(cy.zoom()) -} - -function zoomAroundCenter(factor) { - const level = Math.min(cy.maxZoom(), Math.max(cy.minZoom(), cy.zoom() * factor)) - cy.zoom({ level, renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 } }) -} - -export function fitView(eles, padding = 40) { - if (typeof net3d !== 'undefined' && net3d.active) { - fit3d(eles) - return - } - const target = eles && eles.length ? eles : cy.elements(':visible') - if (!target.length) return - cy.animate( - { fit: { eles: target, padding } }, - { duration: 400, easing: 'ease-out', complete: clampViewport } - ) -} - -/* ============================================================ - NODE SEARCH - Matches node names (and, optionally, attribute values) by substring, - exact name, prefix, suffix or regular expression. A regular expression - may be written plainly (^Rp[LS]\d+$) or with slashes and flags - (/^rp[ls]/i). Enter zooms to the matches; Escape clears. - ============================================================ */ -var searchMatches = null - -var searchTimer = null - -function attrValueStrings(attrs) { - const out = [] - Object.values(attrs || {}).forEach((v) => { - if (Array.isArray(v)) v.forEach((x) => out.push(formatAttrValue(x))) - else if (attrKind(v) !== 'empty') out.push(formatAttrValue(v)) - }) - return out -} - -// Returns { test(str) -> bool } or throws with a readable message. -function buildSearchMatcher(query, mode, matchCase) { - if (mode === 'regex') { - let pattern = query, - flags = '' - const slashed = query.match(/^\/(.*)\/([a-z]*)$/s) - if (slashed) { - pattern = slashed[1] - flags = slashed[2] - } - flags = flags.replace(/[gy]/g, '') // stateful flags would skip matches - if (!matchCase && !flags.includes('i')) flags += 'i' - let re - try { - re = new RegExp(pattern, flags) - } catch (err) { - // browsers phrase this as "Invalid regular expression: /pattern/flags: reason" - const reason = err.message.split(': ').pop() - throw new Error( - `Not a valid regular expression (${reason.charAt(0).toLowerCase() + reason.slice(1)}). Check brackets and escape special characters with \\.` - ) - } - return { test: (str) => re.test(str) } - } - const q = matchCase ? query : query.toLowerCase() - const norm = (str) => (matchCase ? str : str.toLowerCase()) - if (mode === 'exact') return { test: (str) => norm(str) === q } - if (mode === 'prefix') return { test: (str) => norm(str).startsWith(q) } - if (mode === 'suffix') return { test: (str) => norm(str).endsWith(q) } - return { test: (str) => norm(str).includes(q) } -} - -function setSearchStatus(text, isError) { - const el = document.getElementById('searchStatus') - el.textContent = text - el.classList.toggle('error', !!isError) - const badge = document.getElementById('searchBadge') - if (badge) badge.textContent = isError ? '!' : text - document.getElementById('search').setAttribute('aria-invalid', isError ? 'true' : 'false') -} - -function clearSearchHighlight() { - searchMatches = null - cy.elements().removeClass('dimmed highlighted') -} - -function runSearch() { - const input = document.getElementById('search') - const mode = document.getElementById('searchMode').value - // keep surrounding spaces in regex and exact modes; they may be intended - const query = mode === 'regex' || mode === 'exact' ? input.value : input.value.trim() - if (!query) { - clearSearchHighlight() - setSearchStatus('', false) - return - } - let matcher - try { - matcher = buildSearchMatcher(query, mode, document.getElementById('searchCase').checked) - } catch (err) { - clearSearchHighlight() - setSearchStatus(err.message, true) - return - } - const withAttrs = document.getElementById('searchAttrs').checked - const candidates = cy.nodes().filter((n) => !n.hasClass('hidden-group')) - const matched = candidates.filter( - (n) => - matcher.test(n.id()) || - (withAttrs && attrValueStrings(n.data('attrs')).some((v) => matcher.test(v))) - ) - searchMatches = matched - cy.batch(() => { - cy.elements().removeClass('highlighted').addClass('dimmed') - matched.removeClass('dimmed').addClass('highlighted') - matched.connectedEdges().removeClass('dimmed') - }) - setSearchStatus( - matched.length - ? `${matched.length.toLocaleString()} of ${candidates.length.toLocaleString()}` - : 'No matches', - false - ) -} - -function scheduleSearch() { - clearTimeout(searchTimer) - searchTimer = setTimeout(runSearch, cy.nodes().length > 2000 ? 200 : 60) -} - -/* node click -> info panel */ -export function hideInfo() { - document.getElementById('infopanel').classList.remove('show') -} - -function setInfoDesc(text) { - const el = document.getElementById('infoDesc') - el.textContent = text || '' - el.style.display = text ? 'block' : 'none' -} - -/* group inspector: reuses the node info panel for a whole group */ -export function showGroupInfo(g) { - hideEdgePopup() - const members = cy.nodes().filter((n) => (n.data('groups') || []).includes(g)) - const memberIds = new Set(members.map((n) => n.id())) - - let internalEdges = 0, - boundaryEdges = 0 - const internalPairs = new Set() - const internal = cy.collection() - cy.edges().forEach((e) => { - const s = e.data('source'), - t = e.data('target') - const inS = memberIds.has(s), - inT = memberIds.has(t) - if (inS && inT) { - internalEdges++ - internal.merge(e) - if (s !== t) internalPairs.add(s < t ? s + '|' + t : t + '|' + s) - } else if (inS || inT) { - boundaryEdges++ - } - }) - const k = members.length - const density = k > 1 ? internalPairs.size / ((k * (k - 1)) / 2) : 0 - - const overlaps = {} - members.forEach((n) => - (n.data('groups') || []).forEach((o) => { - if (o !== g) overlaps[o] = (overlaps[o] || 0) + 1 - }) - ) - const sharedCount = members.filter((n) => (n.data('groups') || []).length > 1).length - - cy.elements().removeClass('highlighted').addClass('dimmed') - members.removeClass('dimmed').addClass('highlighted') - internal.removeClass('dimmed') - - document.getElementById('infoDots').innerHTML = - `` - document.getElementById('infoName').textContent = groupLabel(g) - document.getElementById('infoExplore').hidden = true - document.getElementById('infoSub').textContent = - groupLabel(g) === g ? `group · ${k} members` : `group "${g}" · ${k} members` - setInfoDesc(groupDescription(g)) - - const stat = (label, value) => - `
    ${label}${value}
    ` - document.getElementById('infoMetrics').innerHTML = - stat('Shared', sharedCount) + - stat('Density', density.toFixed(3)) + - stat('Inside', internalEdges) + - stat('Outgoing', boundaryEdges) - - const custom = customGroupAttrs(g) - const currentShape = S.groupShapes[g] || 'ellipse' - document.getElementById('infoAttrs').innerHTML = - '
    Node shape for this group
    ' + - NODE_SHAPES.map( - ([key, name]) => - `` - ).join('') + - '
    ' + - (Object.keys(custom).length - ? '
    Group attributes
    ' + attrRowsHtml(custom, 'kvrow') - : '') - document.querySelectorAll('#infoAttrs .shape-opt').forEach((btn) => { - btn.addEventListener('click', () => { - setGroupShape(g, btn.dataset.shape) - document - .querySelectorAll('#infoAttrs .shape-opt') - .forEach((b) => b.setAttribute('aria-selected', b === btn ? 'true' : 'false')) - }) - }) - - const overlapEntries = Object.entries(overlaps).sort((a, b) => - byName(groupLabel(a[0]), groupLabel(b[0])) - ) - const connEl = document.getElementById('infoConns') - connEl.innerHTML = overlapEntries.length - ? '
    Shares members with
    ' + - overlapEntries - .map( - ([o, c]) => ` -
    - ${escapeHtml(groupLabel(o))}${c} ${c === 1 ? 'node' : 'nodes'} -
    ` - ) - .join('') - : '
    No members shared with other groups
    ' - connEl.querySelectorAll('.conn.jump').forEach((row) => { - const go = () => showGroupInfo(row.dataset.group) - row.addEventListener('click', go) - row.addEventListener('keydown', (ev) => { - if (ev.key === 'Enter' || ev.key === ' ') { - ev.preventDefault() - go() - } - }) - }) - - document.getElementById('infopanel').classList.add('show') -} - -/* edge click -> popup */ -export function hideEdgePopup() { - document.getElementById('edgePopup').classList.remove('show') -} - -/* ============================================================ - GROUP SHAPES - Each group can have a node shape. A node takes its group's shape only - when exactly one of its groups is currently ticked; nodes showing two - or more active groups stay circular so their pie slices remain - readable. Unticking groups therefore changes which shape a node shows. - ============================================================ */ -function polyPoints(n, r, rot = -Math.PI / 2, cx = 8, cy = 8) { - return Array.from({ length: n }, (_, i) => { - const a = rot + (2 * Math.PI * i) / n - return `${(cx + r * Math.cos(a)).toFixed(2)},${(cy + r * Math.sin(a)).toFixed(2)}` - }).join(' ') -} - -function starPoints() { - return Array.from({ length: 10 }, (_, i) => { - const a = -Math.PI / 2 + (Math.PI * i) / 5 - const r = i % 2 ? 3 : 7 - return `${(8 + r * Math.cos(a)).toFixed(2)},${(8.6 + r * Math.sin(a)).toFixed(2)}` - }).join(' ') -} - -// [cytoscape shape, name, svg markup inside a 16x16 box] -var NODE_SHAPES = [ - ['ellipse', 'Circle', ''], - ['rectangle', 'Square', ''], - ['round-rectangle', 'Rounded square', ''], - ['diamond', 'Diamond', ''], - ['triangle', 'Triangle', ''], - ['vee', 'Vee', ''], - ['pentagon', 'Pentagon', ``], - ['hexagon', 'Hexagon', ``], - ['octagon', 'Octagon', ``], - ['star', 'Star', ``], - ['rhomboid', 'Rhomboid', ''], - ['tag', 'Tag', ''], - ['concave-hexagon', 'Concave hexagon', ''], - ['barrel', 'Barrel', ''], -] - -export var SHAPE_BY_KEY = Object.fromEntries(NODE_SHAPES.map((s) => [s[0], s])) - -export function shapeSvg(key) { - const s = SHAPE_BY_KEY[key] || SHAPE_BY_KEY.ellipse - return `` -} - -// The shape a node should show given its currently active groups. -export function nodeShapeFor(effectiveGroups) { - return effectiveGroups.length === 1 ? S.groupShapes[effectiveGroups[0]] || 'ellipse' : 'ellipse' -} - -function setGroupShape(g, shape) { - if (!shape || shape === 'ellipse') delete S.groupShapes[g] - else S.groupShapes[g] = shape - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - buildGroupLegend() -} - -function assignDistinctShapes() { - const choices = NODE_SHAPES.map((s) => s[0]).filter((k) => k !== 'ellipse') - const groups = typeof filteredGroups === 'function' ? filteredGroups() : getUsedGroups() - groups.forEach((g, i) => { - S.groupShapes[g] = choices[i % choices.length] - }) - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - buildGroupLegend() -} - -function resetShapes() { - const groups = typeof filteredGroups === 'function' ? filteredGroups() : getUsedGroups() - groups.forEach((g) => { - delete S.groupShapes[g] - }) - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - buildGroupLegend() -} - -/* shape chooser popover, shared by all group rows */ -function positionShapePopover() { - const pop = document.getElementById('shapePopover') - if (pop.hidden || !shapePopoverAnchor) return - const r = shapePopoverAnchor.getBoundingClientRect() - const side = document.getElementById('sidebar').getBoundingClientRect() - if (!document.body.contains(shapePopoverAnchor) || r.bottom < side.top || r.top > side.bottom) { - closeShapePopover(false) - return - } - const w = pop.offsetWidth, - h = pop.offsetHeight - pop.style.left = Math.min(window.innerWidth - w - 8, Math.max(8, r.left)) + 'px' - pop.style.top = - (r.bottom + 4 + h > window.innerHeight ? Math.max(8, r.top - h - 4) : r.bottom + 4) + 'px' -} - -export let shapePopoverGroup = null - -let shapePopoverAnchor = null - -export function closeShapePopover(returnFocus) { - const pop = document.getElementById('shapePopover') - if (pop.hidden) return - pop.hidden = true - if (returnFocus && shapePopoverAnchor && document.body.contains(shapePopoverAnchor)) - shapePopoverAnchor.focus() - shapePopoverGroup = null - shapePopoverAnchor = null -} - -export function openShapePopover(g, anchor) { - const pop = document.getElementById('shapePopover') - shapePopoverGroup = g - shapePopoverAnchor = anchor - const current = S.groupShapes[g] || 'ellipse' - pop.innerHTML = - `
    Shape for ${escapeHtml(groupLabel(g))}
    ` + - NODE_SHAPES.map( - ([key, name]) => - `` - ).join('') + - '
    ' - pop.hidden = false - positionShapePopover() - const opts = [...pop.querySelectorAll('.shape-opt')] - opts.forEach((btn, i) => { - btn.addEventListener('click', () => { - setGroupShape(g, btn.dataset.shape) - closeShapePopover(false) - }) - btn.addEventListener('keydown', (e) => { - const cols = 7 - let j = i - if (e.key === 'ArrowRight') j = i + 1 - else if (e.key === 'ArrowLeft') j = i - 1 - else if (e.key === 'ArrowDown') j = i + cols - else if (e.key === 'ArrowUp') j = i - cols - else if (e.key === 'Escape') { - e.preventDefault() - closeShapePopover(true) - return - } else return - e.preventDefault() - opts[(j + opts.length) % opts.length].focus() - }) - }) - ;(pop.querySelector('[aria-selected="true"]') || opts[0]).focus() -} - -/* ============================================================ - VIEWS - A view is one network visualization with everything that belongs to - it: the data on the canvas, which library files it was built from, - every Display setting, node positions, zoom and pan, which groups and - channels are ticked, and group and channel colors. The theme and the - search options are app-wide and are not part of a view. - Only the active view lives on the canvas; switching saves it into its - record and rebuilds the other one from its record. - ============================================================ */ -const APP_WIDE_SETTINGS = new Set(['themeSelect', 'searchMode', 'searchCase', 'searchAttrs']) - -export const views = [] - -export function viewSettings() { - const cfg = getCurrentConfig() - APP_WIDE_SETTINGS.forEach((k) => delete cfg[k]) - return cfg -} - -// Writes settings into the controls without side effects; callers refresh. -export function setControls(cfg) { - CONFIG_FIELDS.forEach(({ id, prop }) => { - if (!cfg || !(id in cfg) || APP_WIDE_SETTINGS.has(id)) return - const el = document.getElementById(id) - if (!el) return - if (prop === 'checked') el.checked = !!cfg[id] - else el.value = cfg[id] - }) -} - -// The canvas contents in loadData's JSON shape (the same shape Save JSON writes). -export function snapshotData() { - if (!cy.nodes().length) return null - const nodeColors = {} - Object.entries(S.nodeColorMap).forEach(([g, c]) => { - if (g !== UNGROUPED) nodeColors[g] = c - }) - const edgeColors = {} - cy.edges().forEach((e) => { - edgeColors[e.data('type')] = e.data('color') - }) - return { - nodes: cy.nodes().map((n) => ({ - id: n.id(), - groups: (n.data('groups') || []).filter((g) => g !== UNGROUPED), - size: n.data('baseSize'), - ...(n.data('nodeColor') ? { color: n.data('nodeColor') } : {}), - ...(n.data('values') ? { values: n.data('values') } : {}), - ...(n.data('attrs') || {}), - })), - edges: cy.edges().map((e) => { - const out = { - id: e.id(), - source: e.data('source'), - target: e.data('target'), - type: e.data('type'), - } - if (typeof e.data('weight') === 'number') out.weight = e.data('weight') - if (e.data('directed')) out.directed = true - return { ...out, ...(e.data('attrs') || {}) } - }), - nodeColors, - edgeColors, - groupOrder: getUsedGroups().filter((g) => g !== UNGROUPED), - ...(Object.keys(S.groupShapes).length ? { groupShapes: { ...S.groupShapes } } : {}), - ...(Object.keys(S.groupAttrs).length ? { groupAttrs: { ...S.groupAttrs } } : {}), - ...(legendExtra ? { legendExtra } : {}), - } -} - -export function selectionFromKey(key) { - return { - networks: key && key.nets ? key.nets.split('|').filter(Boolean) : [], - annotation: key ? key.annotation || '' : '', - colors: key ? key.colors || '' : '', - } -} - -export function activeView() { - return views.find((v) => v.id === S.activeViewId) || null -} - -export function captureActiveView() { - const v = activeView() - if (!v) return - flushHistory() - cancelFrJobs() - S.layoutRunSeq++ - const positions = {} - cy.nodes().forEach((n) => { - const p = n.position() - positions[n.id()] = { x: p.x, y: p.y } - }) - const typeColors = {} - getUsedTypes().forEach((t) => { - if (EDGE_TYPES[t]) typeColors[t] = EDGE_TYPES[t].color - }) - v.data = snapshotData() - v.selection = { - networks: [...libSelection.networks], - annotation: libSelection.annotation, - colors: libSelection.colors, - } - v.state = { - config: viewSettings(), - positions, - zoom: cy.zoom(), - pan: { ...cy.pan() }, - activeGroups: [...S.activeGroups], - activeTypes: [...S.activeTypes], - groupColors: { ...S.nodeColorMap }, - typeColors, - libView: S.currentLibView ? { ...S.currentLibView } : null, - spread: parseFloat(document.getElementById('spreadSlider').value) || 0, - groupFilter: document.getElementById('groupFilter').value, - status: document.getElementById('normaStatus').innerHTML, - view3d: capture3d(), - } -} - -export function clearCanvas() { - bumpDataVersion() - cancelFrJobs() - S.layoutRunSeq++ - cy.elements().remove() - document.getElementById('edgeLegend').innerHTML = '' - document.getElementById('nodeGroupLegend').innerHTML = '' - Object.keys(EDGE_TYPES).forEach((k) => { - if (ORIGINAL_EDGE_TYPES[k]) EDGE_TYPES[k] = { ...ORIGINAL_EDGE_TYPES[k] } - else delete EDGE_TYPES[k] - }) - S.autoEdgeIdx = 0 - S.nodeColorMap = {} - S.groupShapes = {} - S.groupAttrs = {} - S.activeGroups = new Set() - invalidateFullMetrics() - buildAttrSchema() - renderAttrSchema() - buildGroupLegend() - updateStats() - hideInfo() - hideEdgePopup() - S.currentLibView = null - drawGroupHulls() -} - -// With reuseData, the network already on the canvas is kept and only the -// view's look and positions are put back (used by undo/redo). -export function restoreView(v, { reuseData = false } = {}) { - const st = v.state || {} - setControls(st.config || S.DEFAULT_VIEW_CONFIG) - document.getElementById('groupFilter').value = st.groupFilter || '' - if (reuseData) { - cancelFrJobs() - S.layoutRunSeq++ - setLayoutBusy(false) - } else if (v.data) { - loadData(v.data, { positions: st.positions || null }) - } else { - clearCanvas() - } - // some choices (e.g. an edge attribute as label text) only exist once - // the view's data is loaded, so apply the settings again - if (v.data) setControls(st.config || S.DEFAULT_VIEW_CONFIG) - refreshAllDerivedUI() - if (v.data) { - if (st.groupColors) { - S.nodeColorMap = {} - Object.entries(st.groupColors).forEach(([g, c]) => { - const sc = sanitizeColor(c) - if (sc) S.nodeColorMap[g] = sc - }) - } - if (st.typeColors) { - Object.entries(st.typeColors).forEach(([t, c]) => { - const sc = sanitizeColor(c) - if (!sc) return - if (EDGE_TYPES[t]) EDGE_TYPES[t].color = sc - else EDGE_TYPES[t] = { color: sc, label: t } - }) - cy.edges().forEach((e) => { - const m = EDGE_TYPES[e.data('type')] - if (m) e.data('color', m.color) - }) - } - const used = new Set(getUsedGroups()) - if (st.activeGroups) S.activeGroups = new Set(st.activeGroups.filter((g) => used.has(g))) - const usedTypes = getUsedTypes() - if (st.activeTypes) S.activeTypes = new Set(st.activeTypes.filter((t) => usedTypes.includes(t))) - buildGroupLegend() - buildLegend(usedTypes) - applyGroupVisibility() - applyTypeVisibility() - if (reuseData && st.positions) { - cy.batch(() => - cy.nodes().forEach((n) => { - const p = st.positions[n.id()] - if (p) n.position(p) - }) - ) - invalidateViewBox() - } - if (!reuseData && st.positions && typeof st.zoom === 'number') { - cy.viewport({ zoom: st.zoom, pan: st.pan }) - } - } - restore3d(st.view3d) - spreadApplied = st.spread || 0 - spreadBase = st.spreadBase ? { ...st.spreadBase } : null - document.getElementById('spreadSlider').value = spreadApplied - updateSpreadReadout() - - const sel = v.selection || { networks: [], annotation: '', colors: '' } - libSelection.networks = new Set(sel.networks.filter((id) => libEntry('network', id))) - libSelection.annotation = libEntry('annotation', sel.annotation) ? sel.annotation : '' - libSelection.colors = libEntry('colors', sel.colors) ? sel.colors : '' - S.currentLibView = st.libView || null - renderLibraryLists() - if (!reuseData) document.getElementById('normaStatus').innerHTML = st.status || '' - updateStats() - invalidateViewBox() - drawGroupHulls() - cy.elements().removeClass('dimmed highlighted') - if (document.getElementById('search').value) runSearch() - if (v.needsRefresh && !reuseData) refreshAfterDelete(v) -} - -export function uniqueViewName(base) { - const taken = new Set(views.map((v) => v.name)) - if (!taken.has(base)) return base - let n = 2 - while (taken.has(`${base} ${n}`)) n++ - return `${base} ${n}` -} - -export function renderViewBar() { - const sel = document.getElementById('viewSelect') - sel.innerHTML = '' - views.forEach((v) => sel.add(new Option(v.name, v.id))) - sel.value = S.activeViewId - document.getElementById('viewCount').textContent = - views.length > 1 ? `${views.length} views open` : '' - if (typeof updateContextInfo === 'function') updateContextInfo() -} - -// Creates a view and makes it active. With `copy`, it starts as a copy of -// the current view; otherwise it starts empty with default settings. -export function createView(name, { copy = false } = {}) { - captureActiveView() - const from = activeView() - const v = { - id: 'view' + ++S.viewSeq, - name: uniqueViewName(name || 'Untitled view'), - autoName: !name || name === 'Untitled view', - data: copy && from ? from.data : null, - selection: - copy && from - ? { ...from.selection, networks: [...from.selection.networks] } - : { networks: [], annotation: '', colors: '' }, - state: - copy && from - ? JSON.parse(JSON.stringify(from.state)) - : { config: { ...S.DEFAULT_VIEW_CONFIG } }, - } - views.push(v) - S.activeViewId = v.id - S.historySuspended++ - try { - restoreView(v) - } finally { - S.historySuspended-- - } - setHistoryBaseline() - renderViewBar() - return v -} - -export function switchView(id) { - if (id === S.activeViewId || !views.some((v) => v.id === id)) return - captureActiveView() - S.activeViewId = id - S.historySuspended++ - try { - restoreView(activeView()) - } finally { - S.historySuspended-- - } - S.dataCache = { version: dataVersion, data: activeView().data } - setHistoryBaseline() - renderViewBar() - updateUndoButtons() -} - -export function closeActiveView() { - const idx = views.findIndex((v) => v.id === S.activeViewId) - if (idx < 0) return - views.splice(idx, 1) - if (!views.length) { - S.activeViewId = null - createView('Untitled view') - return - } - S.activeViewId = views[Math.max(0, idx - 1)].id - S.historyGestureOpen = false - clearTimeout(historyTimer) - S.historySuspended++ - try { - restoreView(activeView()) - } finally { - S.historySuspended-- - } - S.dataCache = { version: dataVersion, data: activeView().data } - setHistoryBaseline() - renderViewBar() - updateUndoButtons() -} - -// Runs `load` in a view of its own. An untouched empty view is reused -// rather than leaving it behind. -export function openInNewView(name, load) { - const cur = activeView() - if (cur && !cy.nodes().length && cur.autoName !== false && !cur.data) { - cur.name = uniqueViewName(name) - cur.autoName = false - setControls(S.DEFAULT_VIEW_CONFIG) - refreshAllDerivedUI() - } else { - createView(name) - activeView().autoName = false - } - S.historySuspended++ - try { - load() - } finally { - S.historySuspended-- - } - setHistoryBaseline() - renderViewBar() - updateUndoButtons() -} - -export function nameForSelection() { - const nets = selectedNetworks() - const ann = libEntry('annotation', libSelection.annotation) - const base = - nets.length === 1 ? nets[0].name : nets.length ? `${nets.length} networks` : 'Untitled view' - return ann ? `${base}, ${ann.name}` : base -} - -// page wiring, run by main.ts in the original order -export function init() { - document.getElementById('btnProfile').addEventListener('click', runProfiler) - - document.getElementById('btnProfileTsv').addEventListener('click', profilerTsv) - - // statistic definitions in the Help tab come from the same table - document.getElementById('helpStatsRows').innerHTML = PROFILE_STATS.map( - (st) => `${escapeHtml(st.label)}${escapeHtml(st.desc)}` - ).join('') - - ;[ - 'showNodeLabels', - 'showEdgeLabels', - 'labelPosition', - 'labelScaleWithNode', - 'edgeLabelContent', - 'edgeLabelOrientation', - 'labelMinScreenSize', - ].forEach((id) => { - document.getElementById(id).addEventListener('change', updateLabelStyle) - }) - - ;['nodeLabelSize', 'edgeLabelSize'].forEach((id) => { - document.getElementById(id).addEventListener('input', updateLabelStyle) - }) - - document.getElementById('edgeOpacity').addEventListener('input', applyEdgeOpacity) - - document.getElementById('spreadSlider').addEventListener('input', queueSpread) - - document.getElementById('spreadSlider').addEventListener('change', () => { - const v = parseFloat(document.getElementById('spreadSlider').value) || 0 - if (spreadFrame) { - cancelAnimationFrame(spreadFrame) - spreadFrame = 0 - } - if (v !== spreadApplied || cy.nodes().length > SPREAD_LIVE_MAX) applySpread() - scheduleBundling() - }) - - document.getElementById('spreadSlider').addEventListener('dblclick', () => { - document.getElementById('spreadSlider').value = 0 - applySpread() - }) - - cy.on('layoutstart', resetSpread) - - document.getElementById('edgeCurveStyle').addEventListener('change', applyEdgeCurveStyle) - - document.getElementById('edgeCurvature').addEventListener('input', applyEdgeCurveStyle) - - document.getElementById('bundleStrength').addEventListener('input', () => { - document.getElementById('bundleStrengthValue').textContent = - document.getElementById('bundleStrength').value + '%' - }) - - document.getElementById('bundleStrength').addEventListener('change', () => scheduleBundling(true)) - - cy.on('layoutstop dragfree', () => scheduleBundling()) - - document.getElementById('edgeMergeMode').addEventListener('change', applyEdgeMerge) - - document.getElementById('edgeWidthMode').addEventListener('change', applyEdgeWidth) - - document.getElementById('edgeWidthFixed').addEventListener('input', applyEdgeWidth) - - document.getElementById('edgeWidthMin').addEventListener('change', applyEdgeWidth) - - document.getElementById('edgeWidthMax').addEventListener('change', applyEdgeWidth) - - /* node fill mode */ - document.getElementById('nodeFillSelect').addEventListener('change', () => { - if (nodeFillMode() === 'values') applyValueColors() - cy.batch(() => cy.nodes().forEach((n) => refreshNodeVisual(n))) - scheduleLegend() - updateContextInfo() - }) - - /* color palettes */ - document - .getElementById('nodePaletteSelect') - .addEventListener('change', (e) => applyNodePalette(e.target.value)) - - document - .getElementById('edgePaletteSelect') - .addEventListener('change', (e) => applyEdgePalette(e.target.value)) - - /* node size by centrality */ - ;['sizeMetric', 'sizeChannelOnly'].forEach((id) => { - document.getElementById(id).addEventListener('change', applyNodeSizing) - }) - - document.getElementById('sizeMin').addEventListener('change', applyNodeSizing) - - document.getElementById('sizeMax').addEventListener('change', applyNodeSizing) - - document.getElementById('nodeScale').addEventListener('input', applyNodeSizing) - - cy.on('viewport', () => { - clampViewport() - updateZoomReadout() - }) - - cy.on('position add remove', invalidateViewBox) - - cy.on('style data', 'node', invalidateViewBox) - - cy.on('dragfree layoutstop', () => { - invalidateViewBox() - clampViewport() - }) - - window.addEventListener('resize', () => { - invalidateViewBox() - clampViewport() - }) - - document.getElementById('zoomIn').addEventListener('click', () => zoomAroundCenter(1.25)) - - document.getElementById('zoomOut').addEventListener('click', () => zoomAroundCenter(0.8)) - - document.getElementById('zoomFit').addEventListener('click', () => fitView()) - - document.getElementById('search').addEventListener('input', scheduleSearch) - - ;['searchMode', 'searchCase', 'searchAttrs'].forEach((id) => { - document.getElementById(id).addEventListener('change', runSearch) - }) - - document.getElementById('search').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault() - clearTimeout(searchTimer) - runSearch() - if (searchMatches && searchMatches.length) fitView(searchMatches, 80) - } else if (e.key === 'Escape') { - e.target.value = '' - runSearch() - } - }) - - document.getElementById('searchMode').addEventListener('change', (e) => { - document.getElementById('search').placeholder = - { - contains: 'Find nodes', - exact: 'Exact node name', - prefix: 'Name starts with…', - suffix: 'Name ends with…', - regex: 'e.g. ^Rp[LS]\\d+ or /kinase$/i', - }[e.target.value] || 'Find nodes' - }) - - cy.on('tap', 'node', (evt) => { - const node = evt.target - hideEdgePopup() - cy.elements().removeClass('highlighted') - cy.elements().addClass('dimmed') - node.removeClass('dimmed').addClass('highlighted') - const neigh = node.closedNeighborhood() - neigh.removeClass('dimmed') - node.connectedEdges().addClass('highlighted') - - const panel = document.getElementById('infopanel') - const groupsArr = node.data('groups') || [UNGROUPED] - document.getElementById('infoDots').innerHTML = groupsArr - .map((g) => ``) - .join('') - document.getElementById('infoName').textContent = node.data('id') - const groupLinks = groupsArr - .map( - (g) => - `` - ) - .join(' + ') - const subEl = document.getElementById('infoSub') - subEl.innerHTML = `${groupLinks} · ${node.connectedEdges().length} edges · ${node.neighborhood('node').length} neighbors` - subEl - .querySelectorAll('.glink') - .forEach((b) => b.addEventListener('click', () => showGroupInfo(b.dataset.group))) - setInfoDesc('') - - const nid = node.id() - const m = ensureFullMetrics() - const nodeDirected = node.connectedEdges().some((e) => edgeIsDirected(e)) - document.getElementById('infoMetrics').innerHTML = ` -
    Degree${m.degree[nid] || 0}
    - ${nodeDirected ? `
    In / out${m.indegree[nid] || 0} / ${m.outdegree[nid] || 0}
    ` : ''} -
    Betweenness${(m.betweenness[nid] || 0).toFixed(3)}
    -
    Closeness${(m.closeness[nid] || 0).toFixed(3)}
    -
    Clustering${(m.clustering[nid] || 0).toFixed(3)}
    - ` - - const attrs = { - ...(node.data('nodeColor') ? { color: node.data('nodeColor') } : {}), - ...(node.data('attrs') || {}), - } - document.getElementById('infoAttrs').innerHTML = Object.keys(attrs).length - ? '
    Node attributes
    ' + attrRowsHtml(attrs, 'kvrow') - : '' - - const connsByNeighbor = {} - node.connectedEdges().forEach((e) => { - const outgoing = e.source().id() === node.id() - const other = outgoing ? e.target().id() : e.source().id() - connsByNeighbor[other] = connsByNeighbor[other] || [] - const meta = EDGE_TYPES[e.data('type')] || { label: e.data('type') } - const arrow = edgeIsDirected(e) ? (outgoing ? '→ ' : '← ') : '' - connsByNeighbor[other].push(arrow + meta.label) - }) - const connEl = document.getElementById('infoConns') - connEl.innerHTML = - (nodeDirected - ? '
    → outgoing · ← incoming
    ' - : '') + - Object.entries(connsByNeighbor) - .sort((a, b) => byName(a[0], b[0])) - .map( - ([other, types]) => ` -
    ${escapeHtml(other)}${escapeHtml(types.join(', '))}
    - ` - ) - .join('') - - S.infoNodeId = node.id() - document.getElementById('infoExplore').hidden = false - panel.classList.add('show') - }) - - cy.on('tap', 'edge', (evt) => { - const edge = evt.target - hideInfo() - cy.elements().removeClass('highlighted') - cy.elements().addClass('dimmed') - edge.removeClass('dimmed').addClass('highlighted') - edge.connectedNodes().removeClass('dimmed') - - const meta = EDGE_TYPES[edge.data('type')] || { - color: edge.data('color'), - label: edge.data('type'), - } - document.getElementById('epDot').style.background = meta.color - document.getElementById('epType').textContent = meta.label - document.getElementById('epSource').textContent = edge.data('source') - document.getElementById('epTarget').textContent = edge.data('target') - document.getElementById('epId').textContent = edge.data('id') - document.getElementById('epDirection').textContent = edgeIsDirected(edge) - ? `${edge.data('source')} → ${edge.data('target')}` - : 'undirected' - - const weight = edge.data('weight') - const attrs = edge.data('attrs') || {} - const rows = [] - if (typeof weight === 'number') - rows.push(`
    weight${weight}
    `) - rows.push(attrRowsHtml(attrs, 'eprow')) - const merged = edge.data('mergedWith') - if (merged && merged.length > 1) { - rows.push( - `
    Merged channels${merged.length}
    ` - ) - merged.forEach((id) => { - const other = cy.getElementById(id) - if (!other.length) return - const m = EDGE_TYPES[other.data('type')] || { label: other.data('type') } - const w = other.data('weight') - rows.push( - `
    ${escapeHtml(m.label)}${typeof w === 'number' ? escapeHtml(formatAttrValue(w)) : ''}
    ` - ) - }) - } - document.getElementById('epExtra').innerHTML = rows.join('') - - const canvasRect = document.getElementById('canvas').getBoundingClientRect() - const pos = evt.renderedPosition || { x: canvasRect.width / 2, y: canvasRect.height / 2 } - const popup = document.getElementById('edgePopup') - const left = Math.min(pos.x + 14, canvasRect.width - 266) - const top = Math.min(pos.y + 14, canvasRect.height - 180) - popup.style.left = Math.max(10, left) + 'px' - popup.style.top = Math.max(10, top) + 'px' - popup.classList.add('show') - }) - - document.getElementById('edgePopupClose').addEventListener('click', () => { - cy.elements().removeClass('dimmed highlighted') - hideEdgePopup() - }) - - cy.on('tap', (evt) => { - if (evt.target === cy) { - cy.elements().removeClass('dimmed highlighted') - hideInfo() - hideEdgePopup() - } - }) - - /* double-click to recenter */ - cy.on('dbltap', (evt) => { - if (evt.target === cy) { - fitView() - } - }) - - cy.on('dbltap', 'node', (evt) => { - cy.animate( - { center: { eles: evt.target } }, - { duration: 300, easing: 'ease-out', complete: clampViewport } - ) - }) - - document.getElementById('infoClose').addEventListener('click', () => { - cy.elements().removeClass('dimmed highlighted') - hideInfo() - }) - - /* theme */ - document - .getElementById('themeSelect') - .addEventListener('change', (e) => applyTheme(e.target.value)) - - /* group highlighting (hulls / fog clouds) */ - cy.on('render', drawGroupHulls) - - // The WebGL renderer does not emit 'render', so the shading follows the - // changes that move or restyle nodes instead (once per frame). - if (WEBGL_ACTIVE) { - let hullQueued = false - const queueHulls = () => { - if (hullQueued) return - hullQueued = true - requestAnimationFrame(() => { - hullQueued = false - drawGroupHulls() - }) - } - cy.on('viewport position add remove data style class resize layoutstop', queueHulls) - } - - document.getElementById('showGroupHulls').addEventListener('change', (e) => { - document.getElementById('hullControls').style.display = e.target.checked ? 'block' : 'none' - drawGroupHulls() - }) - - document.getElementById('hullStyle').addEventListener('change', drawGroupHulls) - - document.getElementById('hullOpacity').addEventListener('input', (e) => { - document.getElementById('hullOpacityValue').textContent = e.target.value - drawGroupHulls() - }) - - document.addEventListener('mousedown', (e) => { - const pop = document.getElementById('shapePopover') - if (!pop.hidden && !pop.contains(e.target) && !e.target.closest('.gshape')) - closeShapePopover(false) - }) - - document.getElementById('sidebar').addEventListener('scroll', positionShapePopover) - - window.addEventListener('resize', positionShapePopover) - - document.getElementById('btnShapesAssign').addEventListener('click', assignDistinctShapes) - - document.getElementById('btnShapesReset').addEventListener('click', resetShapes) -} diff --git a/frontend/src/recording.ts b/frontend/src/recording.ts deleted file mode 100644 index 79dbb8c..0000000 --- a/frontend/src/recording.ts +++ /dev/null @@ -1,291 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { UNGROUPED, escapeHtml, getUsedGroups, nodeFillMode } from './network_state' -import { activeView, fitView } from './profiler' -import { currentTab } from './wiring' -import { cy } from './cy' -import { - historyTimer, - noteChange, - redo, - scheduleSettle, - setHistoryBaseline, - undo, -} from './demo_downloads' -import { libEntry, libSelection, normaLibrary, renderLibraryLists } from './layouts/controls' -import { refreshLibraryView } from './library' -import { sortedByName } from './hulls' -import { updateStats } from './metrics' -import { updateStringUI } from './string/ui_state' - -/* ---------- what gets recorded ---------- */ -const panelDisplayEl = document.getElementById('panelDisplay') - -function typingInField(target) { - if (!target) return false - if (target.isContentEditable) return true - if (target.tagName === 'TEXTAREA') return true - if (target.tagName === 'INPUT') { - const t = (target.type || '').toLowerCase() - return !['checkbox', 'radio', 'range', 'button', 'color', 'file'].includes(t) - } - return false -} - -/* ============================================================ - MULTIPLE SELECTION - Shift/Ctrl/Cmd-click adds or removes nodes, Shift-drag on the - background draws a selection box, Ctrl/Cmd+A selects every visible - node, Escape clears. Selected nodes get a thick border and a halo; - dragging one moves them all. - ============================================================ */ -let selectionUpdateQueued = false - -function updateSelectionBar() { - selectionUpdateQueued = false - const nodes = cy.nodes(':selected').length - const edges = cy.edges(':selected').length - const bar = document.getElementById('selectionBar') - bar.hidden = nodes + edges < 2 && !(nodes === 1 && edges > 0) - if (bar.hidden) return - const parts = [] - if (nodes) parts.push(`${nodes.toLocaleString()} node${nodes === 1 ? '' : 's'}`) - if (edges) parts.push(`${edges.toLocaleString()} edge${edges === 1 ? '' : 's'}`) - document.getElementById('selectionText').textContent = `${parts.join(' and ')} selected` - document.getElementById('btnSelPaths').hidden = nodes !== 2 - document.getElementById('btnSelNeighbours').hidden = !nodes -} - -/* ============================================================ - CONTEXT LINE: what the current view shows - Network, grouping and node colors of the active view, shown next to - the page tabs so they stay visible on every page. - ============================================================ */ -function contextChip(tint, label, value, title) { - return `${escapeHtml(label)}${escapeHtml(value)}` -} - -// Grouping choices for the current view: every annotation in the file -// library when the view was built from files, otherwise the groups that -// came with the data. -let groupingSelectKey = '' - -export function renderGroupingSelect() { - const sel = document.getElementById('viewGrouping') - if (!sel) return - const lib = S.currentLibView - const groupCount = getUsedGroups().filter((g) => g !== UNGROUPED).length - let options, value, disabled, title - if (lib) { - options = [['', 'None']].concat( - sortedByName(normaLibrary.annotation, (e) => e.name).map((e) => [ - e.id, - `${e.name} (${e.parsed.summary})`, - ]) - ) - value = lib.annotation || '' - disabled = false - title = 'Group the nodes of this view by another annotation. Node positions are kept.' - } else if (cy.nodes().length) { - options = [ - ['data', groupCount ? `From the data (${groupCount} groups)` : 'No groups in the data'], - ] - value = 'data' - disabled = true - title = - 'This view was opened from a built-in demo or a JSON file, so its groups come with the data. Views opened from network files can switch annotations here.' - } else { - options = [['', 'No network shown']] - value = '' - disabled = true - title = 'Open an example or files first.' - } - const key = JSON.stringify([options, value, disabled]) - if (key === groupingSelectKey) { - updateGroupingDelete() - return - } - groupingSelectKey = key - sel.innerHTML = '' - options.forEach(([v, t]) => sel.add(new Option(t, v))) - sel.value = value - sel.disabled = disabled - sel.title = title - updateGroupingDelete() -} - -function updateGroupingDelete() { - const btn = document.getElementById('btnGroupingDelete') - if (!btn) return - const id = S.currentLibView && S.currentLibView.annotation - const e = id && libEntry('annotation', id) - btn.disabled = !e - btn.title = e ? `Completely remove the grouping "${e.name}"` : 'No grouping file to remove' -} - -export function updateContextInfo() { - renderGroupingSelect() - if (typeof updateStringUI === 'function') updateStringUI() - const el = document.getElementById('contextInfo') - if (!el) return - const v = typeof activeView === 'function' ? activeView() : null - if (!cy.nodes().length) { - el.innerHTML = - 'Empty view. Open an example or files in the Upload Data tab, or fetch a network under Database importers.' - return - } - const lib = S.currentLibView - const groups = getUsedGroups().filter((g) => g !== UNGROUPED) - let network, grouping, colors - if (lib) { - const nets = lib.nets - .split('|') - .map((id) => libEntry('network', id)) - .filter(Boolean) - .map((e) => e.name) - network = nets.length ? nets.join(' + ') : 'network files' - const ann = lib.annotation ? libEntry('annotation', lib.annotation) : null - grouping = ann ? ann.name : 'none' - const col = lib.colors ? libEntry('colors', lib.colors) : null - const fill = nodeFillMode() - colors = - fill === 'groups' - ? groups.length - ? 'group colors' - : 'default' - : fill === 'values' - ? `${col ? col.name : 'values'}: ${document.getElementById('valueColumn').value}` - : col - ? col.name - : 'from the data' - } else { - network = v ? v.name : 'current data' - grouping = groups.length ? 'from the data' : 'none' - const hasOwn = cy.nodes().some((n) => n.data('nodeColor')) - const fill = nodeFillMode() - colors = - fill === 'values' - ? `values: ${document.getElementById('valueColumn').value}` - : fill === 'data' && hasOwn - ? 'from the data' - : groups.length - ? 'group colors' - : 'default' - } - const groupText = groups.length - ? `${grouping} (${groups.length.toLocaleString()} groups)` - : grouping - el.innerHTML = - contextChip('data', 'Network', network) + - contextChip('groups', 'Grouping', groupText) + - contextChip('colors', 'Colors', colors) -} - -// page wiring, run by main.ts in the original order -export function init() { - ;['input', 'change'].forEach((type) => { - panelDisplayEl.addEventListener( - type, - (e) => { - if (e.target.id === 'groupFilter') return - noteChange() - }, - true - ) - }) - - panelDisplayEl.addEventListener( - 'click', - (e) => { - const btn = e.target.closest('button') - if ( - !btn || - btn.disabled || - btn.classList.contains('helplink') || - btn.classList.contains('ginfo') || - btn.id === 'btnFit' - ) - return - if (btn.getAttribute('role') === 'radio') return // switching layout mode alone moves nothing - noteChange() - }, - true - ) - - ;['btnRefreshView', 'btnClear'].forEach((id) => { - document.getElementById(id).addEventListener('click', noteChange, true) - }) - - document.getElementById('configFileInput').addEventListener('change', noteChange, true) - - cy.on('dragfree', 'node', noteChange) - - cy.on('layoutstop', () => { - S.cyLayoutsRunning = Math.max(0, S.cyLayoutsRunning - 1) - if (S.historySuspended) return - // a layout the user started belongs to the open entry; one that ran by - // itself (e.g. when data loads) just moves the starting point - if (historyTimer || S.historyGestureOpen) scheduleSettle() - else setHistoryBaseline() - }) - - document.getElementById('btnUndo').addEventListener('click', undo) - - document.getElementById('btnRedo').addEventListener('click', redo) - - document.addEventListener('keydown', (e) => { - if (typingInField(e.target)) return - const mod = e.ctrlKey || e.metaKey - const key = e.key.toLowerCase() - if (mod && key === 'z') { - e.preventDefault() - if (e.shiftKey) redo() - else undo() - } else if (mod && key === 'y') { - e.preventDefault() - redo() - } else if ( - (currentTab === 'network' || currentTab === 'network3d') && - mod && - key === 'a' && - !['SELECT'].includes(e.target.tagName) - ) { - e.preventDefault() - cy.nodes(':visible').select() - } else if ( - (currentTab === 'network' || currentTab === 'network3d') && - key === 'escape' && - cy.$(':selected').length - ) { - cy.$(':selected').unselect() - } - }) - - cy.on('select unselect remove', () => { - if (selectionUpdateQueued) return - selectionUpdateQueued = true - requestAnimationFrame(updateSelectionBar) - }) - - document - .getElementById('btnSelClear') - .addEventListener('click', () => cy.$(':selected').unselect()) - - document.getElementById('btnSelFit').addEventListener('click', () => { - const sel = cy.$(':selected') - if (sel.length) - fitView(sel.union(sel.connectedNodes ? sel.connectedNodes() : cy.collection()), 60) - }) - - document.getElementById('viewGrouping').addEventListener('change', (e) => { - if (!S.currentLibView) return - libSelection.networks = new Set(S.currentLibView.nets.split('|').filter(Boolean)) - libSelection.annotation = e.target.value - if (S.currentLibView.colors !== undefined) libSelection.colors = S.currentLibView.colors - renderLibraryLists() - refreshLibraryView() - groupingSelectKey = '' - updateStats() - }) -} diff --git a/frontend/src/sample_data.ts b/frontend/src/sample_data.ts deleted file mode 100644 index c9736e5..0000000 --- a/frontend/src/sample_data.ts +++ /dev/null @@ -1,546 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -/* ---------- sample data, modeled on the STRING trp-operon screenshot ---------- */ -function buildSampleData() { - // seeded, so the demo (and its download in Help) is the same every time - const rand = mulberry32(3) - const hub = ['trpA', 'trpB', 'trpC', 'trpD', 'trpE', 'pabB', 'tyrA'] - const secondGroup = { - trpA: 'indole-pathway', - pabB: 'folate-pathway', - tyrA: 'tyrosine-pathway', - } - // node attributes: arbitrary metadata about an individual node, shown in - // its info popup (distinct from group attributes below, which describe - // the *group* rather than any one member). - const geneFunction = { - trpA: 'Tryptophan synthase, alpha subunit', - trpB: 'Tryptophan synthase, beta subunit', - trpC: 'Indole-3-glycerol-phosphate synthase', - trpD: 'Anthranilate phosphoribosyltransferase', - trpE: 'Anthranilate synthase component I', - pabB: 'Aminodeoxychorismate synthase component I', - tyrA: 'Chorismate mutase / prephenate dehydrogenase', - lgt: 'Prolipoprotein diacylglyceryl transferase', - putP: 'Sodium/proline symporter', - tnaA: 'Tryptophanase', - glyA: 'Serine hydroxymethyltransferase', - } - const ecNumber = { - trpA: '4.2.1.20', - trpB: '4.2.1.20', - trpC: '4.1.1.48', - trpD: '2.4.2.18', - trpE: '4.1.3.27', - tnaA: '4.1.99.1', - glyA: '2.1.2.1', - } - const nodes = [ - ...hub.map((id) => ({ - id, - groups: secondGroup[id] ? ['core', secondGroup[id]] : ['core'], - size: id === 'trpA' ? 54 : 46, - function: geneFunction[id], - ...(ecNumber[id] ? { ecNumber: ecNumber[id] } : {}), - })), - { id: 'lgt', groups: ['peripheral'], size: 40, function: geneFunction.lgt }, - { id: 'putP', groups: ['peripheral', 'transport'], size: 40, function: geneFunction.putP }, - { - id: 'tnaA', - groups: ['peripheral', 'tryptophan-catabolism'], - size: 40, - function: geneFunction.tnaA, - ecNumber: ecNumber.tnaA, - }, - { - id: 'glyA', - groups: ['peripheral'], - size: 40, - function: geneFunction.glyA, - ecNumber: ecNumber.glyA, - }, - ] - - // group attributes: metadata about each group as a whole -- a nicer - // display label and a short description -- shown in the Node groups - // legend (as the row's label and hover tooltip) and in a node's info - // popup subtitle. - const groupAttrs = { - core: { label: 'Core operon', description: 'Tryptophan biosynthesis operon genes (trpA-E)' }, - 'indole-pathway': { - label: 'Indole pathway', - description: 'Shared branch point with indole/tryptophan interconversion', - }, - 'folate-pathway': { - label: 'Folate pathway', - description: 'para-aminobenzoate / folate biosynthesis branch', - }, - 'tyrosine-pathway': { - label: 'Tyrosine pathway', - description: 'Aromatic amino acid biosynthesis branch', - }, - peripheral: { - label: 'Peripheral genes', - description: 'Functionally related genes outside the core operon', - }, - transport: { label: 'Transport', description: 'Membrane transport function' }, - 'tryptophan-catabolism': { - label: 'Trp catabolism', - description: 'Tryptophan breakdown pathway', - }, - } - - const edges = [] - let eid = 0 - const pairTypes = [ - 'neighborhood', - 'cooccurrence', - 'coexpression', - 'experiments', - 'textmining', - 'database', - ] - - // dense, richly multi-edged core cluster - for (let i = 0; i < hub.length; i++) { - for (let j = i + 1; j < hub.length; j++) { - const a = hub[i], - b = hub[j] - const n = 3 + Math.floor(rand() * 3) // 3-5 parallel edges - const shuffled = [...pairTypes].sort(() => rand() - 0.5).slice(0, n) - shuffled.forEach((t) => { - // edge attributes: arbitrary metadata about an individual edge - // (evidenceCount here), shown in its popup alongside weight/type. - edges.push({ - id: 'e' + eid++, - source: a, - target: b, - type: t, - weight: +(0.4 + rand() * 0.59).toFixed(2), - evidenceCount: 1 + Math.floor(rand() * 14), - }) - }) - } - } - - // peripheral, sparser connections - edges.push({ id: 'e' + eid++, source: 'lgt', target: 'trpA', type: 'coexpression' }) - edges.push({ id: 'e' + eid++, source: 'lgt', target: 'trpA', type: 'textmining' }) - - edges.push({ id: 'e' + eid++, source: 'putP', target: 'trpA', type: 'database' }) - edges.push({ id: 'e' + eid++, source: 'putP', target: 'pabB', type: 'database' }) - edges.push({ id: 'e' + eid++, source: 'putP', target: 'tyrA', type: 'database' }) - - edges.push({ id: 'e' + eid++, source: 'tnaA', target: 'trpB', type: 'database' }) - edges.push({ id: 'e' + eid++, source: 'tnaA', target: 'trpB', type: 'cooccurrence' }) - edges.push({ id: 'e' + eid++, source: 'tnaA', target: 'trpA', type: 'database' }) - - edges.push({ id: 'e' + eid++, source: 'glyA', target: 'trpB', type: 'database' }) - edges.push({ id: 'e' + eid++, source: 'glyA', target: 'trpB', type: 'cooccurrence' }) - - return { nodes, edges, groupAttrs } -} - -// Procedurally generates a plausible multi-group, multi-channel network of a -// given size, for demonstrating the tool across a range of scales (a handful -// of nodes up to several thousand). Group count grows with network size; -// edges are a random spanning backbone (guarantees connectivity) plus extra -// random links for density, each carrying a random channel and weight. -export function generateRandomNetwork(n, opts = {}) { - // seeded, so each demo (and its download in Help) is the same every time - const rand = mulberry32(opts.seed ?? n * 7919 + 17) - const INTERACTIONS = ['activates', 'inhibits', 'binds', 'phosphorylates', 'regulates'] - const groupCount = opts.groupCount || Math.max(2, Math.min(16, Math.round(Math.sqrt(n)))) - const groupNames = Array.from({ length: groupCount }, (_, i) => 'group-' + (i + 1)) - const edgeTypePool = [ - 'neighborhood', - 'cooccurrence', - 'coexpression', - 'experiments', - 'database', - 'textmining', - ] - - const nodes = [] - for (let i = 0; i < n; i++) { - const primary = groupNames[i % groupNames.length] - const groups = [primary] - if (rand() < 0.12) { - const second = groupNames[Math.floor(rand() * groupNames.length)] - if (second !== primary) groups.push(second) - } - nodes.push({ id: 'n' + i, groups, size: 30 + rand() * 20 }) - } - - const edges = [] - let eid = 0 - const makeEdge = (i, j) => ({ - id: 'ge' + eid++, - source: 'n' + i, - target: 'n' + j, - type: edgeTypePool[Math.floor(rand() * edgeTypePool.length)], - weight: +(0.3 + rand() * 0.69).toFixed(2), - ...(opts.edgeLabels - ? { interaction: INTERACTIONS[Math.floor(rand() * INTERACTIONS.length)] } - : {}), - }) - - // random spanning backbone -- every node (after the first) attaches to - // some earlier node, so the whole network stays connected - for (let i = 1; i < n; i++) { - edges.push(makeEdge(i, Math.floor(rand() * i))) - } - // extra random edges for density - const avgExtra = opts.avgExtraEdges ?? (n <= 100 ? 2 : n <= 1000 ? 1.2 : 0.6) - const extraCount = Math.round(n * avgExtra) - for (let k = 0; k < extraCount; k++) { - const a = Math.floor(rand() * n) - const b = Math.floor(rand() * n) - if (a === b) continue - edges.push(makeEdge(a, b)) - } - - const data = { nodes, edges } - if (opts.edgeLabels) { - // open with each edge labelled by its interaction, on curved edges so - // parallel labels don't sit on top of each other - data.config = { - showEdgeLabels: true, - edgeLabelContent: 'attr:interaction', - edgeLabelSize: '10', - edgeCurveStyle: 'bezier', - } - } - return data -} - -// A directed multi-edge network: each connected pair gets one to three -// channels (regulation, phosphorylation, binding, ...), each drawn as an -// arrow from source to target; some pairs also have edges in the reverse -// direction, so reciprocal and parallel arrows can be explored. -export function generateDirectedMultiNetwork(n, opts = {}) { - const rand = mulberry32(opts.seed ?? 4242) - const channels = ['activation', 'inhibition', 'phosphorylation', 'binding', 'expression'] - const groupNames = ['receptors', 'kinases', 'transcription factors', 'targets'] - const groupAttrs = { - receptors: { description: 'Upstream sensors that start the signal' }, - kinases: { description: 'Relay the signal by phosphorylation' }, - 'transcription factors': { description: 'Turn target genes on or off' }, - targets: { description: 'Downstream genes and effectors' }, - } - const nodes = [] - const layerOf = [] - for (let i = 0; i < n; i++) { - // nodes are spread over four layers, roughly a signalling cascade - const layer = Math.min(3, Math.floor((i / n) * 4)) - layerOf.push(layer) - const groups = [groupNames[layer]] - if (rand() < 0.1 && layer < 3) groups.push(groupNames[layer + 1]) - nodes.push({ id: ['R', 'K', 'TF', 'G'][layer] + (i + 1), groups, size: 34 + rand() * 16 }) - } - const edges = [] - let eid = 0 - const addPair = (a, b) => { - const count = 1 + Math.floor(rand() * 3) // 1-3 channels - const picked = [...channels].sort(() => rand() - 0.5).slice(0, count) - picked.forEach((type) => - edges.push({ - id: 'de' + eid++, - source: nodes[a].id, - target: nodes[b].id, - type, - weight: +(0.3 + rand() * 0.7).toFixed(2), - directed: true, - }) - ) - if (rand() < 0.25) { - // feedback in the other direction - edges.push({ - id: 'de' + eid++, - source: nodes[b].id, - target: nodes[a].id, - type: rand() < 0.5 ? 'inhibition' : 'binding', - weight: +(0.3 + rand() * 0.7).toFixed(2), - directed: true, - }) - } - } - const used = new Set() - const pairOnce = (a, b) => { - const key = a < b ? a + ',' + b : b + ',' + a - if (a === b || used.has(key)) return - used.add(key) - addPair(a, b) - } - // every node below the top layer is reached from an earlier layer - for (let i = 0; i < n; i++) { - if (layerOf[i] === 0) continue - const upstream = [] - for (let j = 0; j < n; j++) if (layerOf[j] === layerOf[i] - 1) upstream.push(j) - pairOnce(upstream[Math.floor(rand() * upstream.length)], i) - } - // extra links within and across neighbouring layers - for (let k = 0; k < n * 0.9; k++) { - const a = Math.floor(rand() * n) - const candidates = [] - for (let j = 0; j < n; j++) - if (layerOf[j] === layerOf[a] || layerOf[j] === layerOf[a] + 1) candidates.push(j) - pairOnce(a, candidates[Math.floor(rand() * candidates.length)]) - } - return { - nodes, - edges, - groupAttrs, - groupOrder: groupNames, - directed: true, - config: { - edgeDirection: 'data', - edgeCurveStyle: 'bezier', - edgeCurvature: '28', - arrowShape: 'triangle', - arrowScale: '1', - layoutSelect: 'breadthfirst', - }, - } -} - -// Small seeded PRNG so the module examples come out identical on every load. -export function mulberry32(seed) { - return function () { - seed |= 0 - seed = (seed + 0x6d2b79f5) | 0 - let t = Math.imul(seed ^ (seed >>> 15), 1 | seed) - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t - return ((t ^ (t >>> 14)) >>> 0) / 4294967296 - } -} - -// Four synthetic functional modules (not real genes). Each module biases -// what its members look like (localization, domains) and which evidence -// channels its internal links tend to come from. -const MODULE_DEFS = [ - { - key: 'signaling', - prefix: 'SIG', - label: 'Signaling', - category: 'regulatory', - description: 'Receptors and kinases that relay outside cues into the cell', - localization: ['membrane', 'cytoplasm', 'cytoplasm'], - domains: ['kinase', 'SH2', 'PH', 'GTPase'], - channels: ['experiments', 'experiments', 'coexpression', 'textmining'], - }, - { - key: 'metabolism', - prefix: 'MET', - label: 'Central metabolism', - category: 'enzymatic', - description: 'Enzymes that turn nutrients into energy and building blocks', - localization: ['cytoplasm', 'mitochondrion', 'mitochondrion'], - domains: ['Rossmann fold', 'TIM barrel', 'aldolase', 'NAD-binding'], - channels: ['database', 'neighborhood', 'cooccurrence', 'coexpression'], - }, - { - key: 'transport', - prefix: 'TRN', - label: 'Membrane transport', - category: 'transport', - description: 'Carriers and channels that move solutes across membranes', - localization: ['membrane', 'membrane', 'vesicle'], - domains: ['MFS', 'ABC', 'ion channel', 'porin'], - channels: ['cooccurrence', 'database', 'neighborhood', 'experiments'], - }, - { - key: 'transcription', - prefix: 'TSC', - label: 'Transcription control', - category: 'regulatory', - description: 'Factors that switch target genes on and off', - localization: ['nucleus', 'nucleus', 'cytoplasm'], - domains: ['zinc finger', 'HTH', 'bZIP', 'bromodomain'], - channels: ['textmining', 'coexpression', 'experiments', 'database'], - }, -] - -const CROSS_MODULE_CHANNELS = ['textmining', 'database', 'cooccurrence'] - -// Builds a 4-module network that exercises all three attribute levels: -// node attrs: localization (text), expression (number), essential (yes/no), -// length (integer), domains (list) -// edge attrs: evidenceCount (integer), firstReported (year) -// group attrs: label/description (built-in) plus category, enrichmentFDR, -// curated, keyMembers (custom) -// sizes -- members per module (before overlaps), sums to the node count -// overlaps -- one entry per shared node: [homeModule, ...extraModules] -// pIn/pOut -- link probability for a node pair inside / across modules -export function buildModuleNetwork({ sizes, overlaps, pIn, pOut, seed }) { - const rand = mulberry32(seed) - const pick = (arr) => arr[Math.floor(rand() * arr.length)] - const shuffle = (arr) => { - const a = arr.slice() - for (let i = a.length - 1; i > 0; i--) { - const j = Math.floor(rand() * (i + 1)) - ;[a[i], a[j]] = [a[j], a[i]] - } - return a - } - const pickDistinct = (pool, n) => { - const out = [] - const distinct = new Set(pool).size - while (out.length < Math.min(n, distinct)) { - const v = pick(pool) - if (!out.includes(v)) out.push(v) - } - return out - } - - // 1. nodes, each with a single home module - const nodes = [] - const members = MODULE_DEFS.map(() => []) - MODULE_DEFS.forEach((m, gi) => { - for (let i = 0; i < sizes[gi]; i++) { - const node = { - id: `${m.prefix}-${String(i + 1).padStart(2, '0')}`, - groups: [m.key], - size: 30 + Math.round(rand() * 16), - localization: pick(m.localization), - expression: +((rand() * 2 - 1) * 3).toFixed(2), - essential: rand() < 0.2, - length: 120 + Math.round(rand() * 1280), - domains: pickDistinct(m.domains, rand() < 0.35 ? 2 : 1), - } - nodes.push(node) - members[gi].push(node) - } - }) - - // 2. small overlaps: a few nodes join one or two extra modules and pick - // up a domain typical of each module they join - overlaps.forEach(([home, ...extra]) => { - const candidates = members[home].filter((n) => n.groups.length === 1) - if (!candidates.length) return - const node = pick(candidates) - extra.forEach((gj) => { - const m = MODULE_DEFS[gj] - node.groups.push(m.key) - members[gj].push(node) - const d = pick(m.domains) - if (!node.domains.includes(d)) node.domains.push(d) - }) - }) - - // 3. edges: 1-3 parallel channels per linked pair - const edges = [] - const seenPairs = new Set() - const degree = {} - let eid = 0 - const link = (a, b, channelPool) => { - if (a === b) return - const key = a.id < b.id ? a.id + '|' + b.id : b.id + '|' + a.id - if (seenPairs.has(key)) return - seenPairs.add(key) - const channelCount = 1 + (rand() < 0.45 ? 1 : 0) + (rand() < 0.15 ? 1 : 0) - pickDistinct(channelPool, channelCount).forEach((type) => { - edges.push({ - id: 'm' + eid++, - source: a.id, - target: b.id, - type, - weight: +(0.4 + rand() * 0.59).toFixed(2), - evidenceCount: 1 + Math.floor(rand() * 12), - firstReported: 1995 + Math.floor(rand() * 30), - }) - degree[a.id] = (degree[a.id] || 0) + 1 - degree[b.id] = (degree[b.id] || 0) + 1 - }) - } - - // inside each module: a random tree keeps the module connected, then - // extra links at density pIn - members.forEach((list, gi) => { - const pool = MODULE_DEFS[gi].channels - const order = shuffle(list) - for (let i = 1; i < order.length; i++) link(order[i], order[Math.floor(rand() * i)], pool) - for (let i = 0; i < list.length; i++) { - for (let j = i + 1; j < list.length; j++) { - if (rand() < pIn) link(list[i], list[j], pool) - } - } - }) - - // across modules: sparse links between nodes that share no module - for (let i = 0; i < nodes.length; i++) { - for (let j = i + 1; j < nodes.length; j++) { - const a = nodes[i], - b = nodes[j] - if (a.groups.some((g) => b.groups.includes(g))) continue - if (rand() < pOut) link(a, b, CROSS_MODULE_CHANNELS) - } - } - - // 4. group attributes - const groupAttrs = {} - MODULE_DEFS.forEach((m, gi) => { - const keyMembers = members[gi] - .slice() - .sort((a, b) => (degree[b.id] || 0) - (degree[a.id] || 0)) - .slice(0, 3) - .map((n) => n.id) - groupAttrs[m.key] = { - label: m.label, - description: m.description, - category: m.category, - enrichmentFDR: +Math.pow(10, -(2 + rand() * 6)).toPrecision(2), - curated: gi % 2 === 0, - keyMembers, - } - }) - - return { nodes, edges, groupAttrs } -} - -export const SAMPLE_GENERATORS = { - trp: () => buildSampleData(), - // 50 nodes; 5 shared nodes (10%): one bridge per neighbouring module pair - // around the ring, plus one node sitting in three modules - modules50: () => - buildModuleNetwork({ - sizes: [14, 12, 13, 11], - overlaps: [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [0, 1, 2], - ], - pIn: 0.26, - pOut: 0.012, - seed: 50, - }), - // 100 nodes; 10 shared nodes (10%): two bridges per neighbouring pair, - // one across the ring and one node in three modules - modules100: () => - buildModuleNetwork({ - sizes: [28, 24, 26, 22], - overlaps: [ - [0, 1], - [1, 0], - [1, 2], - [2, 1], - [2, 3], - [3, 2], - [3, 0], - [0, 3], - [0, 2], - [1, 2, 3], - ], - pIn: 0.16, - pOut: 0.006, - seed: 100, - }), - tiny: () => generateRandomNetwork(20, { edgeLabels: true, avgExtraEdges: 1 }), - directed: () => generateDirectedMultiNetwork(40), - small: () => generateRandomNetwork(60), - medium: () => generateRandomNetwork(200), - large: () => generateRandomNetwork(800), - massive: () => generateRandomNetwork(5000, { avgExtraEdges: 0.6 }), -} diff --git a/frontend/src/side_panel.ts b/frontend/src/side_panel.ts deleted file mode 100644 index ac5df23..0000000 --- a/frontend/src/side_panel.ts +++ /dev/null @@ -1,104 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) - -/* ---------- side panel: drag to resize, arrow to collapse ---------- */ -// page wiring, run by main.ts in the original order -export function init() { - const app = document.getElementById('app') - const handle = document.getElementById('sideResizer') - const toggle = document.getElementById('sideToggle') - const DEFAULT_W = 300, - MIN_W = 260, - COLLAPSE_BELOW = 150 - const maxW = () => Math.max(MIN_W, Math.min(720, Math.round(window.innerWidth * 0.6))) - let width = DEFAULT_W, - collapsed = false - try { - const saved = JSON.parse(localStorage.getItem('norma3-sidebar') || 'null') - if (saved) { - width = Number(saved.width) || DEFAULT_W - collapsed = !!saved.collapsed - } - } catch (e) {} - const save = () => { - try { - localStorage.setItem('norma3-sidebar', JSON.stringify({ width, collapsed })) - } catch (e) {} - } - function apply() { - width = Math.max(MIN_W, Math.min(maxW(), Math.round(width))) - app.style.setProperty('--side-w', collapsed ? '0px' : width + 'px') - app.classList.toggle('side-collapsed', collapsed) - const label = collapsed ? 'Expand the side panel' : 'Collapse the side panel' - toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true') - toggle.setAttribute('aria-label', label) - toggle.title = label - handle.setAttribute('aria-valuemin', MIN_W) - handle.setAttribute('aria-valuemax', maxW()) - handle.setAttribute('aria-valuenow', width) - } - function setCollapsed(c) { - collapsed = c - apply() - save() - } - toggle.addEventListener('click', () => setCollapsed(!collapsed)) - - handle.addEventListener('pointerdown', (e) => { - if (e.button !== 0) return - e.preventDefault() - handle.setPointerCapture(e.pointerId) - app.classList.add('side-resizing') - const left = app.getBoundingClientRect().left - const beforeW = width - const move = (ev) => { - const w = ev.clientX - left - if (w < COLLAPSE_BELOW) { - if (!collapsed) { - collapsed = true - apply() - } - return - } - collapsed = false - width = w - apply() - } - const up = () => { - app.classList.remove('side-resizing') - // dragged shut: reopen later at the width it had before the drag - if (collapsed) width = beforeW - handle.removeEventListener('pointermove', move) - handle.removeEventListener('pointerup', up) - handle.removeEventListener('pointercancel', up) - save() - } - handle.addEventListener('pointermove', move) - handle.addEventListener('pointerup', up) - handle.addEventListener('pointercancel', up) - }) - handle.addEventListener('dblclick', () => { - width = DEFAULT_W - collapsed = false - apply() - save() - }) - handle.addEventListener('keydown', (e) => { - const step = e.shiftKey ? 50 : 10 - if (e.key === 'ArrowLeft') width -= step - else if (e.key === 'ArrowRight') width += step - else if (e.key === 'Home') width = MIN_W - else if (e.key === 'End') width = maxW() - else if (e.key === 'Enter') { - setCollapsed(true) - toggle.focus() - e.preventDefault() - return - } else return - e.preventDefault() - apply() - save() - }) - window.addEventListener('resize', apply) - apply() -} diff --git a/frontend/src/side_tabs.ts b/frontend/src/side_tabs.ts deleted file mode 100644 index 3cd2473..0000000 --- a/frontend/src/side_tabs.ts +++ /dev/null @@ -1,273 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { NORMA_EXAMPLE_SETS } from './wiring' -import { downloadText } from './layouts/controls' - -/* ---------- Data / Display sub-tabs ---------- */ -const SIDE_TABS = [ - ['data', 'sideTabData', 'panelData'], - ['db', 'sideTabDb', 'panelDb'], - ['display', 'sideTabDisplay', 'panelDisplay'], - ['export', 'sideTabExport', 'panelExport'], -] - -export function switchSideTab(name) { - SIDE_TABS.forEach(([k, tabId, panelId]) => { - const on = k === name - const tab = document.getElementById(tabId) - tab.setAttribute('aria-selected', on ? 'true' : 'false') - tab.tabIndex = on ? 0 : -1 - document.getElementById(panelId).hidden = !on - }) - document.getElementById('sidebar').scrollTop = 0 -} - -/* ---------- example file links (Help > Examples) ---------- - Download links for NORMA's example files as published by the NORMA - server, each paired with the copy bundled in this page (by example set - and file name), which keeps working offline. */ -const NORMA_DOWNLOAD_BASE = - 'https://pavlopoulos-lab-services.org/shiny/app_proxy/a64cd611-0420-4151-a75c-e1e2066f6bc5/session/ef352e36fc9ed86bf6e69f45a83d6216/download/' - -const EXAMPLE_FILE_LINKS = [ - { - title: 'STRING example 1 (TP53 interactors)', - set: 'string-tp53', - files: [ - ['STRING Network file', 'string_net_tp53', 'string_interactions.txt'], - ['STRING Annotation file', 'string_annot', 'string_interactions_groups_comma_duplicate.txt'], - ['STRING Expression file', 'string_expr', 'string_expression_colors.txt'], - ], - }, - { - title: 'STRING example 2 (BCAR3 interactors)', - set: 'string-bcar3', - files: [ - ['STRING Network file', 'string_net_bcar3', 'BCAR3.txt'], - ['GO Annotation - Biological Process', 'string_bp', 'BCAR3_GO_BP.txt'], - ['GO Annotation - Molecular Function', 'string_mf', 'BCAR3_GO_MF.txt'], - ['KEGG pathways', 'string_kegg', 'BCAR3_KEGG.txt'], - ], - }, - { - title: 'Drosophila (Tau) Network', - set: 'tau', - refs: [ - ['PMID:31488613', 'https://pubmed.ncbi.nlm.nih.gov/31488613/'], - ['PMCID:PMC6794924', 'https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6794924/'], - ['DOI:10.1523/JNEUROSCI.0391-19.2019', 'https://doi.org/10.1523/JNEUROSCI.0391-19.2019'], - ], - files: [ - ['Drosophila Network file', 'dros_net', 'TAU_network_DEGs_NORMA.txt'], - ['Drosophila Kegg pathways', 'dros_annot', 'TAU_KEGG_Annotation_NORMA.txt'], - ['Drosophila Louvain automated annotation file', 'dros_louvain', 'TAU_Louvain.txt'], - ['Drosophila Expression file', 'dros_express', 'TAU_expressions.txt'], - ], - }, - { - title: 'Human Gene Co-expression Network', - set: 'coexpr', - refs: [ - ['PMID:19081792', 'https://pubmed.ncbi.nlm.nih.gov/19081792/'], - ['PMCID:PMC2597745', 'https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2597745/'], - ['DOI:10.1371/journal.pone.0003911', 'https://doi.org/10.1371/journal.pone.0003911'], - ['bioinfow.dep.usal.es/coexpression', 'http://bioinfow.dep.usal.es/coexpression/'], - ], - files: [ - ['Gene Co-expression Network', 'co_express', 'NORMA_Human_coexpression_NETWORK.txt'], - [ - 'GO Annotation - Biological Process', - 'co_express_bp', - 'NORMA_Human_coexpression_Annotation_GO_BP.txt', - ], - [ - 'GO Annotation - Molecular Function', - 'co_express_mf', - 'NORMA_Human_coexpression_Annotation_GO_MF.txt', - ], - [ - 'GO Annotation - Cellular Components', - 'co_express_cc', - 'NORMA_Human_coexpression_Annotation_GO_CC.txt', - ], - ['KEGG pathways', 'co_express_kegg', 'NORMA_Human_coexpression_Annotation_KEGG.txt'], - ['MCODE Node coloring', 'co_express_mcode', 'NORMA_Human_coexpression_Expression_MCODE.txt'], - ], - }, - { - title: 'COVID-19: IntAct Database', - set: 'covid', - files: [ - ['COVID-19 Network', 'covid_19_net', 'Intact-data_COVID19_no_self_loops.txt'], - [ - 'HomoSapiens Protein Domains - INTERPRO', - 'covid_19_interpro', - 'HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt', - ], - [ - 'HomoSapiens GO Annotation - Biological Process', - 'covid_19_bp', - 'HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt', - ], - [ - 'HomoSapiens GO Annotation - Molecular Function', - 'covid_19_mf', - 'HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt', - ], - [ - 'HomoSapiens Protein Domains GO Annotation - Cellular Components', - 'covid_19_cc', - 'HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt', - ], - [ - 'HomoSapiens Protein Domains KEGG pathways', - 'covid_19_kegg', - 'HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt', - ], - [ - 'HomoSapiens Protein Domains SMART', - 'covid_19_smart', - 'HomoSapiens_Protein_Domains_SMART_FILTERED.txt', - ], - ], - }, - { - title: 'Gallus gallus: BioGrid Database', - set: 'gallus', - files: [ - ['Gallus gallus Network', 'Gallus_gallus_net', 'Biogrid_no_self_loops.txt'], - [ - 'BioGrid Gallus gallus KEGG pathways', - 'Gallus_gallus_kegg', - 'BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt', - ], - ], - }, -] - -function bundledExampleFile(setKey, fileName) { - const set = NORMA_EXAMPLE_SETS[setKey] - return set ? set.files.find((f) => f.fileName === fileName) || null : null -} - -function renderLocalExampleSets(root) { - Object.values(NORMA_EXAMPLE_SETS) - .filter((set) => set.local) - .forEach((set) => { - const block = document.createElement('div') - block.className = 'dl-group' - const h = document.createElement('h3') - h.textContent = set.title - block.appendChild(h) - const list = document.createElement('ul') - list.className = 'dl-list' - set.files.forEach((f) => { - const li = document.createElement('li') - li.append(`${f.name} `) - const btn = document.createElement('button') - btn.type = 'button' - btn.className = 'dl-copy' - btn.textContent = 'Download' - btn.title = `Download ${f.fileName}` - btn.addEventListener('click', () => downloadText(f.fileName, f.text)) - li.appendChild(btn) - list.appendChild(li) - }) - block.appendChild(list) - root.appendChild(block) - }) -} - -function renderExampleLinks() { - const root = document.getElementById('helpExampleFiles') - if (!root) return - root.innerHTML = '' - renderLocalExampleSets(root) - EXAMPLE_FILE_LINKS.forEach((group) => { - const block = document.createElement('div') - block.className = 'dl-group' - const h = document.createElement('h3') - h.textContent = group.title - block.appendChild(h) - if (group.refs) { - const refs = document.createElement('p') - refs.className = 'dl-refs' - group.refs.forEach(([label, url], i) => { - if (i) refs.append(', ') - const a = document.createElement('a') - a.href = url - a.target = '_blank' - a.rel = 'noopener' - a.textContent = label - refs.appendChild(a) - }) - block.appendChild(refs) - } - const list = document.createElement('ul') - list.className = 'dl-list' - group.files.forEach(([label, slug, fileName]) => { - const li = document.createElement('li') - const a = document.createElement('a') - const local = bundledExampleFile(group.set, fileName) - if (local) { - // the sample file itself, from this page, so it never goes missing - a.href = '#' - a.title = `Download ${fileName}` - a.addEventListener('click', (e) => { - e.preventDefault() - downloadText(fileName, local.text) - }) - } else { - a.href = `${NORMA_DOWNLOAD_BASE}${slug}?w=` - a.target = '_blank' - a.rel = 'noopener' - } - a.textContent = label - li.appendChild(a) - const fn = document.createElement('span') - fn.className = 'dl-name' - fn.textContent = fileName - li.appendChild(fn) - list.appendChild(li) - }) - block.appendChild(list) - root.appendChild(block) - }) -} - -/* ---------- search panel: collapse / expand ---------- */ -function setSearchCollapsed(collapsed) { - const panel = document.getElementById('searchPanel') - const toggle = document.getElementById('searchToggle') - panel.classList.toggle('collapsed', collapsed) - toggle.setAttribute('aria-expanded', String(!collapsed)) - toggle.title = collapsed ? 'Open the search panel' : 'Minimize the search panel' - toggle.setAttribute('aria-label', collapsed ? 'Open node search' : 'Minimize node search') - if (!collapsed) document.getElementById('search').focus() -} - -// page wiring, run by main.ts in the original order -export function init() { - document.querySelectorAll('#sideTabs [role="tab"]').forEach((tab) => { - tab.addEventListener('click', () => switchSideTab(tab.dataset.side)) - tab.addEventListener('keydown', (e) => { - if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return - const keys = SIDE_TABS.map((t) => t[0]) - const i = keys.indexOf(tab.dataset.side) - const next = keys[(i + (e.key === 'ArrowRight' ? 1 : keys.length - 1)) % keys.length] - switchSideTab(next) - document.querySelector(`#sideTabs [data-side="${next}"]`).focus() - }) - }) - - renderExampleLinks() - - document.getElementById('searchToggle').addEventListener('click', () => { - setSearchCollapsed(!document.getElementById('searchPanel').classList.contains('collapsed')) - }) - - document.getElementById('searchMinimize').addEventListener('click', () => { - setSearchCollapsed(true) - document.getElementById('searchToggle').focus() - }) -} diff --git a/frontend/src/state.ts b/frontend/src/state.ts deleted file mode 100644 index a39fe88..0000000 --- a/frontend/src/state.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EDGE_TYPES } from './palette' - -// ponytail: mutable page state that several modules assign; ES module bindings are read-only across files -export const S = { - currentLibView: null, - nodeColorMap: {}, - groupShapes: {}, - groupAttrs: {}, - activeTypes: new Set(Object.keys(EDGE_TYPES)), - activeGroups: new Set(), - layoutRunSeq: 0, - currentNodePalette: 'vivid', - layoutProgress: null, - infoNodeId: null, - autoEdgeIdx: 0, - historySuspended: 0, - dataCache: { version: -1, data: null }, - historyGestureOpen: false, - cyLayoutsRunning: 0, - workRange: [0, 1], - frRequestSeq: 0, - normaEntrySeq: 0, - viewSeq: 0, - activeViewId: null, - DEFAULT_VIEW_CONFIG: null, -} diff --git a/frontend/src/string/groupings.ts b/frontend/src/string/groupings.ts deleted file mode 100644 index 63ed838..0000000 --- a/frontend/src/string/groupings.ts +++ /dev/null @@ -1,185 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { STRING_CATEGORIES, stringCategoryKey, stringCategoryName } from '../view3d/export' -import { - addNormaEntry, - libSelection, - plural, - renderLibraryLists, - setStatus, - startProgress, -} from '../layouts/controls' -import { refreshLibraryView } from '../library' -import { stringCall, stringState } from './requests' -import { stringNetworkInView, stringSettings, updateStringUI } from './ui_state' - -/* ---------- groupings ---------- */ -// task/step: the import's progress; on its own, it shows a progress of its own. -export async function fetchStringGroupings(netEntry, st, notes, task = null, step = 0) { - const meta = netEntry.stringMeta - const nameOf = new Map(Object.entries(meta.nameOf)) - const byPreferred = new Map([...nameOf.values()].map((n) => [n.toLowerCase(), n])) - const ids = meta.stringIds - const enrichment = st.annotMode === 'enrichment' - const own = !task - if (own) { - task = startProgress('stringStatus', [1]) - step = 0 - } - task.step( - step, - enrichment - ? `Running functional enrichment for ${plural(ids.length, 'protein')}…` - : `Fetching functional annotations for ${plural(ids.length, 'protein')}…` - ) - let rows - try { - rows = await stringCall( - enrichment ? 'enrichment' : 'functional_annotation', - { identifiers: ids.join('\r'), species: meta.taxon }, - { onBytes: (got, total) => task.bytes(got, total) } - ) - } catch (err) { - notes.push({ level: 'warn', text: `Groupings couldn't be fetched: ${err.message}` }) - if (own) task.stop() - return [] - } - if (own) task.stop() - const wanted = new Set(st.categories) - const byCat = new Map() - const seenCats = new Map() - ;(rows || []).forEach((r) => { - const cat = stringCategoryKey(r.category) - seenCats.set(cat, (seenCats.get(cat) || 0) + 1) - const known = STRING_CATEGORIES.some((c) => c[0] === cat) - if (!(wanted.has(cat) || (!known && st.otherCategories))) return - if (enrichment && !(Number(r.fdr) <= st.fdr)) return - // members: map STRING ids (inputGenes) or names back to node names - const inputs = Array.isArray(r.inputGenes) - ? r.inputGenes - : String(r.inputGenes || '').split(',') - const names = Array.isArray(r.preferredNames) - ? r.preferredNames - : String(r.preferredNames || '').split(',') - const members = new Set() - inputs.forEach((g, i) => { - const n = - nameOf.get(g) || - byPreferred.get(String(names[i] || g).toLowerCase()) || - byPreferred.get(String(g).toLowerCase()) - if (n) members.add(n) - }) - if (members.size < st.minGroup) return - if (!byCat.has(cat)) byCat.set(cat, []) - byCat.get(cat).push({ - term: r.term, - description: r.description || r.term, - members: [...members], - fdr: Number(r.fdr), - p: Number(r.p_value), - background: Number(r.number_of_genes_in_background), - ratio: Number(r.ratio_in_set), - }) - }) - const entries = [] - const order = [ - ...STRING_CATEGORIES.map((c) => c[0]), - ...[...byCat.keys()].filter((k) => !STRING_CATEGORIES.some((c) => c[0] === k)), - ] - order.forEach((cat) => { - const list = byCat.get(cat) - if (!list || !list.length) return - list.sort((a, b) => - enrichment - ? a.fdr - b.fdr || b.members.length - a.members.length - : b.members.length - a.members.length - ) - const top = list.slice(0, st.maxTerms) - const usedNames = new Set() - const groupMeta = {} - const lines = top.map((t) => { - let name = String(t.description) - .replace(/[\t\r\n]+/g, ' ') - .trim() - if (usedNames.has(name)) name = `${name} (${t.term})` - usedNames.add(name) - const info = { - description: `${stringCategoryName(cat)}${t.term && t.term !== name ? ', ' + t.term : ''}`, - term: t.term, - category: stringCategoryName(cat), - 'proteins in network': t.members.length, - } - if (enrichment) { - info.FDR = t.fdr - if (Number.isFinite(t.p)) info['p-value'] = t.p - if (Number.isFinite(t.background)) info['proteins in genome'] = t.background - } - groupMeta[name] = info - return `${name}\t${t.members.join(',')}` - }) - const label = `${netEntry.name}: ${stringCategoryName(cat)}${enrichment ? ` (FDR ≤ ${st.fdr})` : ''}` - const entry = addNormaEntry('annotation', label, lines.join('\n') + '\n', '', null, {}) - entry.groupMeta = groupMeta - entry.stringCategory = cat - entry.stringCategoryName = stringCategoryName(cat) - entry.forNetwork = netEntry.id - entries.push(entry) - if (list.length > top.length) - notes.push({ - level: 'ok', - text: `${stringCategoryName(cat)}: kept the ${top.length} ${enrichment ? 'most significant' : 'largest'} of ${list.length} terms.`, - }) - }) - const offered = [...seenCats.keys()].map((k) => `${stringCategoryName(k)} (${seenCats.get(k)})`) - if (offered.length) - notes.push({ - level: 'ok', - text: `STRING returned ${enrichment ? 'enriched terms' : 'annotations'} in: ${offered.join(', ')}.`, - }) - const emptyWanted = st.categories.filter((k) => !byCat.has(k) && !(k === 'KEGG' && !enrichment)) - if (emptyWanted.length) - notes.push({ - level: 'warn', - text: `No ${enrichment ? `terms with FDR ≤ ${st.fdr}` : 'annotations'} and at least ${st.minGroup} proteins for: ${emptyWanted.map(stringCategoryName).join(', ')}.`, - }) - if (!enrichment && wanted.has('KEGG')) - notes.push({ - level: 'ok', - text: 'STRING does not share full KEGG annotations for licensing reasons; choose Terms: "Enriched in the network" to get KEGG pathways.', - }) - return entries -} - -export async function runStringGroupings() { - const net = stringNetworkInView() - if (!net) return - const st = stringSettings() - if (!st.categories.length && !st.otherCategories) { - setStatus('stringStatus', [{ level: 'error', text: 'Tick at least one term collection.' }]) - return - } - stringState.busy = true - stringState.cancelled = false - updateStringUI() - const notes = [] - try { - const entries = await fetchStringGroupings(net, st, notes) - if (entries.length) { - libSelection.annotation = entries[0].id - renderLibraryLists() - refreshLibraryView() - } - setStatus('stringStatus', [ - { - level: entries.length ? 'ok' : 'warn', - text: entries.length - ? `Added ${plural(entries.length, 'grouping')} for "${net.name}": ${entries.map((e) => e.stringCategoryName).join(', ')}.` - : `No new groupings for "${net.name}".`, - }, - ...notes, - ]) - } finally { - stringState.busy = false - updateStringUI() - } -} diff --git a/frontend/src/string/import.ts b/frontend/src/string/import.ts deleted file mode 100644 index 01796cf..0000000 --- a/frontend/src/string/import.ts +++ /dev/null @@ -1,292 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { STRING_CHANNELS, STRING_PREFERRED_GROUPING } from '../view3d/export' -import { - addNormaEntry, - libSelection, - listSample, - plural, - renderLibraryLists, - setStatus, - startProgress, -} from '../layouts/controls' -import { - combineStringScores, - scoreValue, - stringBase, - stringCall, - stringState, - stringVersion, -} from './requests' -import { fetchStringGroupings } from './groupings' -import { openInNewView } from '../profiler' -import { refreshLibraryView } from '../library' -import { stringSettings, updateStringUI } from './ui_state' - -/* ---------- import ---------- */ - -export async function runStringImport() { - const st = stringSettings() - if (!st.names.length) { - setStatus('stringStatus', [ - { level: 'error', text: 'Type at least one protein or gene name, for example TP53.' }, - ]) - return - } - if (!/^(\d+|STRG\w+)$/.test(st.taxon)) { - setStatus('stringStatus', [ - { - level: 'error', - text: 'Enter the organism as an NCBI taxon number, for example 9606 for human.', - }, - ]) - return - } - if (!st.channels.length) { - setStatus('stringStatus', [{ level: 'error', text: 'Tick at least one evidence channel.' }]) - return - } - if (st.names.length > 2000) { - setStatus('stringStatus', [ - { level: 'error', text: 'STRING accepts at most 2,000 names per query.' }, - ]) - return - } - stringState.busy = true - stringState.cancelled = false - updateStringUI() - const notes = [] - const withGroupings = !!(st.categories.length || st.otherCategories) - // steps: version, names, network, descriptions, groupings, opening - const task = startProgress('stringStatus', [ - 1, - 2, - 6, - st.descriptions ? 2 : 0, - withGroupings ? 4 : 0, - 1, - ]) - const onBytes = (got, total) => task.bytes(got, total) - try { - task.step(0, 'Checking the STRING version…') - const ver = await stringVersion() - - task.step(1, `Looking up ${plural(st.names.length, 'name')} in ${st.speciesLabel}…`) - const mapped = await stringCall( - 'get_string_ids', - { identifiers: st.names.join('\r'), species: st.taxon, echo_query: 1 }, - { onBytes } - ) - const byQuery = new Map() - ;(mapped || []).forEach((r) => { - const q = r.queryItem ?? st.names[r.queryIndex] - if (q !== undefined && !byQuery.has(q)) byQuery.set(q, r) - }) - const found = st.names.filter((n) => byQuery.has(n)) - const missing = st.names.filter((n) => !byQuery.has(n)) - if (!found.length) - throw new Error( - `STRING found none of the names in ${st.speciesLabel}. Check the spelling and the organism.` - ) - if (missing.length) - notes.push({ - level: 'warn', - text: `Not found in ${st.speciesLabel}: ${listSample(missing)}.`, - }) - const renamed = found.filter( - (n) => - byQuery.get(n).preferredName && - byQuery.get(n).preferredName.toLowerCase() !== n.toLowerCase() - ) - if (renamed.length) - notes.push({ - level: 'ok', - text: `Matched ${listSample(renamed.map((n) => `${n} as ${byQuery.get(n).preferredName}`))}.`, - }) - const queryIds = [...new Set(found.map((n) => byQuery.get(n).stringId))] - const taxonName = byQuery.get(found[0]).taxonName || st.speciesLabel - - task.step( - 2, - st.addNodes - ? `Fetching the network with up to ${st.addNodes} interactors…` - : 'Fetching the network…' - ) - const params = { - identifiers: queryIds.join('\r'), - species: st.taxon, - required_score: Math.round(st.threshold * 1000), - network_type: st.networkType, - } - if (st.addNodes > 0) params.add_nodes = st.addNodes - const rows = await stringCall('network', params, { onBytes }) - const built = buildStringNetwork(rows || [], queryIds, byQuery, st) - if (!built.edges.length) - throw new Error( - `No ${st.networkType} connections pass a confidence of ${st.threshold.toFixed(2)} with the ticked channels. Lower the confidence, tick more channels or add interactors.` - ) - notes.push(...built.notes) - - // descriptions for every protein - if (st.descriptions) { - task.step(3, `Fetching descriptions for ${plural(built.nodeIds.length, 'protein')}…`) - try { - const info = await stringCall( - 'get_string_ids', - { identifiers: built.stringIds.join('\r'), species: st.taxon }, - { onBytes } - ) - ;(info || []).forEach((r) => { - const name = built.nameOf.get(r.stringId) - if (name && r.annotation) built.nodeAttrs[name].description = r.annotation - }) - } catch (err) { - notes.push({ - level: 'warn', - text: `Protein descriptions couldn't be fetched: ${err.message}`, - }) - } - } - - const netName = `STRING ${found - .slice(0, 3) - .map((n) => byQuery.get(n).preferredName || n) - .join(', ')}${found.length > 3 ? ` +${found.length - 3}` : ''} (${shortSpecies(taxonName)})` - const netEntry = addNormaEntry('network', netName, built.text, '', null, {}) - netEntry.nodeAttrs = built.nodeAttrs - netEntry.stringMeta = { - taxon: st.taxon, - taxonName, - version: ver.version, - address: ver.address || stringBase(), - query: found, - stringIds: built.stringIds, - nameOf: Object.fromEntries(built.nameOf), - threshold: st.threshold, - networkType: st.networkType, - channels: st.channels.map((c) => c[1]), - } - - // groupings - let annEntries = [] - if (withGroupings) { - annEntries = await fetchStringGroupings(netEntry, st, notes, task, 4) - } - task.step(5, 'Opening the network…') - - // show it - const preferred = - STRING_PREFERRED_GROUPING.map((k) => annEntries.find((e) => e.stringCategory === k)).find( - Boolean - ) || annEntries[0] - openInNewView(netName, () => { - // a new view starts with its own (empty) file selection - libSelection.networks = new Set([netEntry.id]) - libSelection.annotation = preferred ? preferred.id : '' - libSelection.colors = '' - renderLibraryLists() - refreshLibraryView() - }) - const summary = `Imported ${netEntry.parsed.summary} from STRING${ver.version ? ' ' + ver.version : ''} (${taxonName}, ${st.networkType}, confidence ≥ ${st.threshold.toFixed(2)}).` - const groupText = annEntries.length - ? `Added ${plural(annEntries.length, 'grouping')}: ${annEntries.map((e) => e.stringCategoryName).join(', ')}. Switch between them with the Grouping list at the top.` - : 'No groupings were added.' - setStatus('stringStatus', [ - { level: 'ok', text: summary }, - { level: 'ok', text: groupText }, - ...notes, - ]) - } catch (err) { - setStatus('stringStatus', [{ level: 'error', text: err.message }, ...notes]) - } finally { - task.stop() - stringState.busy = false - stringState.abort = null - updateStringUI() - } -} - -function shortSpecies(name) { - const parts = String(name || '').split(/\s+/) - return parts.length >= 2 ? `${parts[0][0]}. ${parts[1]}` : name -} - -function buildStringNetwork(rows, queryIds, byQuery, st) { - const notes = [] - const nameOf = new Map() // stringId -> node name - const used = new Set() - const nodeAttrs = {} - const querySet = new Set(queryIds) - const addNode = (sid, preferred) => { - if (nameOf.has(sid)) return nameOf.get(sid) - // node names must be unique and can't contain commas (annotation files) - let name = - String(preferred || sid) - .replace(/,/g, ';') - .trim() || sid - if (used.has(name)) name = `${name} (${sid})` - used.add(name) - nameOf.set(sid, name) - nodeAttrs[name] = { stringId: sid, query: querySet.has(sid) } - return name - } - queryIds.forEach((sid) => { - const q = [...byQuery.values()].find((r) => r.stringId === sid) - addNode(sid, q && q.preferredName) - }) - const seen = new Set() - const lines = [] - let dropped = 0, - pairs = 0 - rows.forEach((r) => { - const a = r.stringId_A, - b = r.stringId_B - if (!a || !b || a === b) return - const key = a < b ? a + '\t' + b : b + '\t' + a - if (seen.has(key)) return // STRING may list a pair in both directions - seen.add(key) - const scores = st.channels.map((c) => scoreValue(r[c[0]])) - const combined = - st.channels.length === STRING_CHANNELS.length - ? scoreValue(r.score) - : combineStringScores(scores) - if (combined < st.threshold - 1e-9) { - dropped++ - return - } - const na = addNode(a, r.preferredName_A), - nb = addNode(b, r.preferredName_B) - pairs++ - if (st.edgeMode === 'combined') { - lines.push([na, nb, combined.toFixed(3), 'combined']) - } else { - st.channels.forEach((c, i) => { - if (scores[i] > 0) lines.push([na, nb, scores[i].toFixed(3), c[1]]) - }) - } - }) - if (dropped) - notes.push({ - level: 'ok', - text: `Left out ${plural(dropped, 'connection')} whose score from the ticked channels alone is below ${st.threshold.toFixed(2)}.`, - }) - const connected = new Set(lines.flatMap((l) => [l[0], l[1]])) - const lonely = [...nameOf.values()].filter((n) => !connected.has(n)) - if (lonely.length) - notes.push({ - level: 'warn', - text: `${plural(lonely.length, 'protein has', 'proteins have')} no connection that passes the filters and ${lonely.length === 1 ? 'is' : 'are'} not shown: ${listSample(lonely)}.`, - }) - const text = ['Source\tTarget\tWeight\tType', ...lines.map((l) => l.join('\t'))].join('\n') + '\n' - const kept = [...nameOf.entries()].filter(([, n]) => connected.has(n)) - return { - text, - edges: lines, - pairs, - nodeIds: kept.map(([, n]) => n), - stringIds: kept.map(([sid]) => sid), - nameOf: new Map(kept), - nodeAttrs: Object.fromEntries(kept.map(([, n]) => [n, nodeAttrs[n]])), - notes, - } -} diff --git a/frontend/src/string/requests.ts b/frontend/src/string/requests.ts deleted file mode 100644 index 720fa6a..0000000 --- a/frontend/src/string/requests.ts +++ /dev/null @@ -1,160 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { STRING_CALLER, STRING_PRIOR } from '../view3d/export' -import { readTextWithProgress } from '../layouts/controls' - -/* ---------- requests ---------- */ -export const stringState = { busy: false, abort: null, lastCall: 0, versions: new Map() } - -// How requests reach STRING: through server.py's relay (same origin, so no -// cross-site restrictions) or directly from the browser. -export const stringRoute = { mode: null, checked: false } - -export async function resolveStringRoute() { - const choice = document.getElementById('stringConnect').value - if (choice !== 'auto') { - stringRoute.mode = choice - return choice - } - if (stringRoute.checked) return stringRoute.mode - stringRoute.checked = true - stringRoute.mode = 'direct' - if (/^https?:$/.test(location.protocol)) { - try { - const ctl = new AbortController() - const t = setTimeout(() => ctl.abort(), 2500) - const r = await fetch('string-api/ping', { signal: ctl.signal, cache: 'no-store' }) - clearTimeout(t) - const j = r.ok ? await r.json() : null - if (j && j.norma_proxy) stringRoute.mode = 'proxy' - } catch (e) { - /* no relay: go direct */ - } - } - updateStringConnectState() - return stringRoute.mode -} - -export function updateStringConnectState() { - const el = document.getElementById('stringConnectState') - if (!el) return - const choice = document.getElementById('stringConnect').value - const mode = choice === 'auto' ? stringRoute.mode : choice - el.textContent = !mode - ? 'Decided at the first request.' - : mode === 'proxy' - ? 'Requests go through this server to STRING.' - : 'Requests go from the browser straight to STRING.' -} - -export function stringBase() { - const raw = (document.getElementById('stringAddress').value || 'https://string-db.org') - .trim() - .replace(/\/+$/, '') - return /^https?:\/\//i.test(raw) ? raw : 'https://' + raw -} - -export async function stringCall(method, params, { onBytes = null } = {}) { - // one second between calls, as STRING asks - const wait = stringState.lastCall + 1050 - Date.now() - if (wait > 0) await new Promise((r) => setTimeout(r, wait)) - stringState.lastCall = Date.now() - // The body is sent as a plain string: some environments pass fetch - // requests on with postMessage, which can't copy URLSearchParams objects. - const body = new URLSearchParams({ ...params, caller_identity: STRING_CALLER }).toString() - const route = await resolveStringRoute() - const url = - route === 'proxy' - ? `string-api/json/${method}?upstream=${encodeURIComponent(stringBase())}` - : `${stringBase()}/api/json/${method}` - const controller = new AbortController() - stringState.abort = controller - const timer = setTimeout(() => controller.abort(), 90000) - let response - try { - // a form-encoded POST with a plain-text body is a "simple" request: no CORS preflight - response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body, - signal: controller.signal, - }) - } catch (err) { - if (err.name === 'AbortError') - throw new Error( - stringState.cancelled - ? 'Cancelled.' - : 'STRING did not answer within 90 seconds. Try again, or ask for fewer interactors.' - ) - throw new Error( - route === 'proxy' - ? `The server relay could not be reached (${err.message}). Check that server.py is still running, or choose Connect: Directly.` - : `STRING could not be reached (${err.message}). Check the internet connection. The most reliable setup is to run NORMA with server.py, which passes requests on to STRING; see the README.` - ) - } finally { - clearTimeout(timer) - } - const text = await readTextWithProgress(response, onBytes) - if (!response.ok) { - if (route === 'proxy') { - if (response.status === 502 && /could not be reached from this server/.test(text)) - throw new Error(`${text.slice(0, 200)} Check the server's internet connection.`) - // a plain web server answers relay requests with its own (non-JSON) error page - if (!/^\s*[\[{]/.test(text)) - throw new Error( - 'This server has no STRING relay. Start NORMA with server.py, or choose Connect: Directly.' - ) - } - if (response.status === 404) - throw new Error('STRING found none of the given names for this organism.') - let detail = '' - try { - const j = JSON.parse(text) - detail = (Array.isArray(j) ? j[0] : j)?.message || '' - } catch (e) { - detail = text.slice(0, 160) - } - throw new Error( - `STRING answered with an error (${response.status})${detail ? ': ' + detail : ''}.` - ) - } - try { - return JSON.parse(text) - } catch (e) { - throw new Error('STRING sent an answer that is not JSON. The STRING address may be wrong.') - } -} - -export async function stringVersion() { - const base = stringBase() - if (stringState.versions.has(base)) return stringState.versions.get(base) - try { - const res = await stringCall('version', {}) - const v = Array.isArray(res) ? res[0] : res - const info = { version: v && v.string_version, address: v && v.string_stable_address } - stringState.versions.set(base, info) - return info - } catch (err) { - return { version: null, address: null } - } -} - -/* ---------- scores ---------- */ -// STRING's rule for combining channel scores: remove the prior from each -// channel, combine as independent evidence, then add the prior back. -export function combineStringScores(scores) { - let miss = 1 - scores.forEach((s) => { - if (!(s > 0)) return - const noPrior = Math.max(0, (s - STRING_PRIOR) / (1 - STRING_PRIOR)) - miss *= 1 - noPrior - }) - const combined = 1 - miss - return combined + STRING_PRIOR * (1 - combined) -} - -export function scoreValue(v) { - const x = Number(v) - if (!Number.isFinite(x)) return 0 - return x > 1 ? x / 1000 : x // accept 0-1 or 0-1000 scales -} diff --git a/frontend/src/string/ui_state.ts b/frontend/src/string/ui_state.ts deleted file mode 100644 index 0b8d8c8..0000000 --- a/frontend/src/string/ui_state.ts +++ /dev/null @@ -1,80 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { STRING_CATEGORIES, STRING_CHANNELS, STRING_SPECIES } from '../view3d/export' -import { libEntry } from '../layouts/controls' -import { stringState } from './requests' - -/* ---------- UI state ---------- */ -export function stringSettings() { - const speciesSel = document.getElementById('stringSpecies').value - const taxon = - speciesSel === 'other' ? document.getElementById('stringTaxon').value.trim() : speciesSel - const preset = document.getElementById('stringScorePreset').value - const threshold = - preset === 'custom' - ? parseFloat(document.getElementById('stringScore').value) - : parseFloat(preset) - return { - names: document - .getElementById('stringQuery') - .value.split(/[\s,;]+/) - .map((s) => s.trim()) - .filter(Boolean), - taxon, - speciesLabel: - speciesSel === 'other' - ? `taxon ${taxon}` - : (STRING_SPECIES.find((s) => String(s[0]) === speciesSel) || [0, speciesSel])[1], - networkType: document.getElementById('stringNetType').value, - addNodes: parseInt(document.getElementById('stringAddNodes').value, 10) || 0, - threshold: Number.isFinite(threshold) ? threshold : 0.4, - channels: STRING_CHANNELS.filter((c) => document.getElementById('stringCh_' + c[1]).checked), - edgeMode: document.getElementById('stringEdgeMode').value, - annotMode: document.getElementById('stringAnnotMode').value, - fdr: parseFloat(document.getElementById('stringFdr').value) || 0.05, - categories: STRING_CATEGORIES.map((c) => c[0]).filter( - (k) => document.getElementById('stringCat_' + k).checked - ), - otherCategories: document.getElementById('stringCatOther').checked, - maxTerms: Math.max(1, parseInt(document.getElementById('stringMaxTerms').value, 10) || 30), - minGroup: Math.max(1, parseInt(document.getElementById('stringMinGroup').value, 10) || 2), - descriptions: document.getElementById('stringDescriptions').checked, - } -} - -export function updateStringUI() { - const st = stringSettings() - document.getElementById('stringTaxonRow').hidden = - document.getElementById('stringSpecies').value !== 'other' - document.getElementById('stringAddNodesValue').textContent = st.addNodes - const preset = document.getElementById('stringScorePreset').value - document.getElementById('stringScoreRow').hidden = preset !== 'custom' - document.getElementById('stringScoreValue').textContent = parseFloat( - document.getElementById('stringScore').value - ).toFixed(2) - document.getElementById('stringFdrRow').hidden = st.annotMode !== 'enrichment' - document.getElementById('stringKeggNote').hidden = st.annotMode !== 'annotation' - const busy = stringState.busy - document.getElementById('btnStringFetch').disabled = busy - document.getElementById('btnStringCancel').hidden = !busy - const view = stringNetworkInView() - document.getElementById('btnStringGroupings').disabled = busy || !view - document.getElementById('btnStringGroupings').title = view - ? `Fetch groupings for "${view.name}" with the settings above` - : 'Show a network imported from STRING in the current view first' - let hint = '' - if (st.names.length === 1 && st.addNodes === 0) - hint = 'With a single protein STRING always adds its 10 best interactors.' - else if (st.names.length > 1 && st.addNodes === 0) - hint = 'Only connections among the given proteins are fetched.' - document.getElementById('stringNodesHint').textContent = hint -} - -// The STRING network shown in the current view, if any. -export function stringNetworkInView() { - if (!S.currentLibView) return null - const ids = S.currentLibView.nets.split('|').filter(Boolean) - const nets = ids.map((id) => libEntry('network', id)).filter((e) => e && e.stringMeta) - return nets.length === 1 ? nets[0] : null -} diff --git a/frontend/src/string/wiring.ts b/frontend/src/string/wiring.ts deleted file mode 100644 index ad3937e..0000000 --- a/frontend/src/string/wiring.ts +++ /dev/null @@ -1,191 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EDGE_TYPES } from '../palette' -import { STRING_CATEGORIES, STRING_CHANNELS, STRING_SPECIES } from '../view3d/export' -import { escapeHtml } from '../network_state' -import { runStringGroupings } from './groupings' -import { runStringImport } from './import' -import { stringRoute, stringState, updateStringConnectState } from './requests' -import { updateStringUI } from './ui_state' - -/* ============================================================ - ICONS AND WELCOME CARD - Small line icons (16 x 16, drawn in the current text color) for the - page tabs, sidebar tabs and sidebar sections, and the welcome card - shown while the current view is empty. - ============================================================ */ -const ICON_PATHS = { - code: '', - legend: - '', - gauge: - '', - info: '', - home: '', - graph: - '', - cube: '', - chart: - '', - venn: '', - help: '', - database: - '', - sliders: - '', - sparkle: '', - cloud: - '', - folder: - '', - swap: '', - layout: - '', - groups: - '', - fog: '', - palette: - '', - node: '', - tag: '', - edge: '', - layers: '', - list: '', - play: '', - upload: '', -} - -function iconSvg(name) { - const body = ICON_PATHS[name] - if (!body) return '' - return `` -} - -function iconSpan(name) { - const span = document.createElement('span') - span.className = 'ico' - span.innerHTML = iconSvg(name) - return span -} - -// page wiring, run by main.ts in the original order -export function init() { - /* ---------- wiring ---------- */ - ;(function setupStringImporter() { - const sel = document.getElementById('stringSpecies') - STRING_SPECIES.forEach(([id, name]) => sel.add(new Option(name, String(id)))) - sel.add(new Option('Other organism (enter the taxon ID)…', 'other')) - const chBox = document.getElementById('stringChannels') - STRING_CHANNELS.forEach(([, key, label]) => { - const row = document.createElement('label') - row.className = 'string-check' - row.innerHTML = `${escapeHtml(label)}` - chBox.appendChild(row) - }) - const catBox = document.getElementById('stringCategories') - STRING_CATEGORIES.forEach(([key, label, on]) => { - const row = document.createElement('label') - row.className = 'string-check' - row.innerHTML = `${escapeHtml(label)}` - catBox.appendChild(row) - }) - const other = document.createElement('label') - other.className = 'string-check' - other.innerHTML = - 'Any other collection STRING offers' - catBox.appendChild(other) - document.getElementById('stringSection').addEventListener('input', updateStringUI) - document.getElementById('stringSection').addEventListener('change', updateStringUI) - document.getElementById('btnStringFetch').addEventListener('click', runStringImport) - document.getElementById('btnStringGroupings').addEventListener('click', runStringGroupings) - document.getElementById('btnStringCancel').addEventListener('click', () => { - stringState.cancelled = true - if (stringState.abort) stringState.abort.abort() - }) - document.getElementById('stringQuery').addEventListener('keydown', (ev) => { - if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { - ev.preventDefault() - runStringImport() - } - }) - document.getElementById('stringConnect').addEventListener('change', () => { - stringRoute.checked = false - stringRoute.mode = null - updateStringConnectState() - }) - updateStringConnectState() - document.getElementById('btnStringCatAll').addEventListener('click', () => { - STRING_CATEGORIES.forEach((c) => { - document.getElementById('stringCat_' + c[0]).checked = true - }) - updateStringUI() - }) - document.getElementById('btnStringCatNone').addEventListener('click', () => { - STRING_CATEGORIES.forEach((c) => { - document.getElementById('stringCat_' + c[0]).checked = false - }) - document.getElementById('stringCatOther').checked = false - updateStringUI() - }) - updateStringUI() - })() - - ;(function addIcons() { - const tabIcons = { - welcome: 'home', - about: 'info', - api: 'code', - network: 'graph', - network3d: 'cube', - profiler: 'chart', - compare: 'venn', - help: 'help', - } - document.querySelectorAll('#tabs .tab').forEach((t) => { - if (tabIcons[t.dataset.tab]) t.prepend(iconSpan(tabIcons[t.dataset.tab])) - }) - const sideIcons = { data: 'upload', db: 'database', display: 'sliders', export: 'swap' } - document - .querySelectorAll('#sideTabs [data-side]') - .forEach((t) => t.prepend(iconSpan(sideIcons[t.dataset.side]))) - const sectionIcons = { - Examples: 'sparkle', - STRING: 'cloud', - Files: 'folder', - 'Open saved work': 'upload', - Reactome: 'cloud', - OmniPath: 'cloud', - NDEx: 'cloud', - IntAct: 'cloud', - 'Gene Ontology': 'cloud', - 'NORMA files': 'folder', - Image: 'palette', - Arena3D: 'cube', - 'Other tools': 'swap', - 'Save your work': 'upload', - '3D layout': 'cube', - Layout: 'layout', - 'Node groups': 'groups', - 'Group highlighting': 'fog', - Colors: 'palette', - Nodes: 'node', - Labels: 'tag', - Edges: 'edge', - 'Edge channels': 'layers', - Attributes: 'list', - Legend: 'legend', - Performance: 'gauge', - } - document.querySelectorAll('.section > h3').forEach((h) => { - const title = [...h.childNodes] - .filter((n) => n.nodeType === 3) - .map((n) => n.textContent) - .join('') - .trim() - if (sectionIcons[title]) h.prepend(iconSpan(sectionIcons[title])) - }) - document.querySelectorAll('.ico[data-icon]').forEach((s) => { - s.innerHTML = iconSvg(s.dataset.icon) - }) - })() -} diff --git a/frontend/src/style.css b/frontend/src/style.css deleted file mode 100644 index 4bcf0dc..0000000 --- a/frontend/src/style.css +++ /dev/null @@ -1,3644 +0,0 @@ -:root { - --bg: #0f1420; - --panel: #161d2e; - --panel-2: #1d2740; - --panel-2-hover: #223050; - --line: #2a3554; - --text: #e7ebf5; - --muted: #8993ad; - --accent: #5fd3c4; - --accent-dim: #3a7d74; - --accent-hover: #7fe0d3; - --accent-text: #08221e; - --warn: #e7a15f; - --canvas-dot: #1c2540; - --mono: 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace; - --sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -} -* { - box-sizing: border-box; -} -[hidden] { - display: none !important; -} -#shapePopover { - position: fixed; - z-index: 100; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; - padding: 8px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); -} -.shape-pop-title { - font-size: 11.5px; - color: var(--muted); - margin: 0 2px 6px; - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.shape-grid { - display: grid; - grid-template-columns: repeat(7, 28px); - gap: 3px; -} -.shape-opt { - width: 28px; - height: 28px; - padding: 5px; - display: flex; - align-items: center; - justify-content: center; -} -.shape-opt svg, -.gshape svg { - width: 100%; - height: 100%; - fill: var(--muted); -} -.shape-opt[aria-selected='true'] { - border-color: var(--accent); - background: var(--panel-2-hover); -} -.shape-opt[aria-selected='true'] svg { - fill: var(--accent); -} -.toggle-row .gshape { - width: 20px; - height: 20px; - padding: 3px; - flex-shrink: 0; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; -} -html, -body { - margin: 0; - padding: 0; - height: 100%; - background: var(--bg); - color: var(--text); - font-family: var(--sans); - overflow: hidden; -} -#app { - display: grid; - --side-w: 300px; - position: relative; - grid-template-columns: var(--side-w) minmax(0, 1fr); - grid-template-rows: 52px 42px 36px minmax(0, 1fr); - grid-template-areas: - 'brand viewhead' - 'sidetabs topbar' - 'sidebar ctxbar' - 'sidebar canvas'; - height: 100vh; - width: 100vw; -} -#brand { - grid-area: brand; - display: flex; - align-items: center; - gap: 10px; - padding: 0 18px; - border-bottom: 1px solid var(--line); - border-right: 1px solid var(--line); - background: var(--panel); -} -#brand .dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent); - box-shadow: 0 0 10px var(--accent); -} -#brand .title { - font-weight: 600; - font-size: 14.5px; - letter-spacing: 0.2px; -} -#brand .title small { - display: block; - font-size: 10.5px; - color: var(--muted); - font-weight: 400; - margin-top: 1px; -} -#topbar { - grid-area: topbar; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 18px; - border-bottom: 1px solid var(--line); - background: var(--panel); - gap: 14px; -} -#topbar .group { - display: flex; - align-items: center; - gap: 8px; -} -#search { - background: var(--bg); - border: 1px solid var(--line); - color: var(--text); - padding: 7px 10px; - border-radius: 6px; - font-size: 13px; - width: 220px; - outline: none; -} -#search:focus { - border-color: var(--accent-dim); -} -#stats { - font-size: 12px; - color: var(--muted); - font-family: var(--mono); - white-space: nowrap; - flex-shrink: 0; -} -@media (max-width: 1180px) { - #stats { - font-size: 11px; - } -} -#stats b { - color: var(--text); - font-weight: 600; -} -button { - font-family: var(--sans); - font-size: 12.5px; - background: var(--panel-2); - color: var(--text); - border: 1px solid var(--line); - padding: 7px 12px; - border-radius: 6px; - cursor: pointer; - transition: - border-color 0.15s, - background 0.15s; -} -button:hover { - border-color: var(--accent-dim); - background: var(--panel-2-hover); -} -button.primary { - background: var(--accent); - color: var(--accent-text); - border-color: var(--accent); - font-weight: 600; -} -button.primary:hover { - background: var(--accent-hover); -} -#sidebar { - grid-area: sidebar; - background: var(--panel); - border-right: 1px solid var(--line); - overflow-y: auto; - padding: 16px 16px 40px; -} -#sidebar { - padding-top: 0; -} -.dl-group { - margin: 14px 0 4px; -} -.help-callout { - border: 1px solid color-mix(in srgb, #10b981 45%, var(--line)); - border-left: 4px solid #10b981; - background: color-mix(in srgb, #10b981 8%, var(--panel)); - border-radius: 8px; - padding: 12px 16px; - margin: 8px 0 14px; -} -.help-callout h3 { - margin: 0 0 6px; - font-size: 15px; - color: color-mix(in srgb, #10b981 55%, var(--text)); -} -.help-callout p { - margin: 6px 0; -} -.help-callout-alt { - border-color: color-mix(in srgb, #3b82f6 45%, var(--line)); - border-left-color: #3b82f6; - background: color-mix(in srgb, #3b82f6 8%, var(--panel)); -} -.help-callout-dir { - border-color: color-mix(in srgb, #8b5cf6 45%, var(--line)); - border-left-color: #8b5cf6; - background: color-mix(in srgb, #8b5cf6 8%, var(--panel)); -} -.help-callout-dir h3 { - color: color-mix(in srgb, #8b5cf6 60%, var(--text)); -} -.help-callout-3d { - border-color: color-mix(in srgb, #0ea5e9 45%, var(--line)); - border-left-color: #0ea5e9; - background: color-mix(in srgb, #0ea5e9 8%, var(--panel)); -} -.help-callout-3d h3 { - color: color-mix(in srgb, #0ea5e9 60%, var(--text)); -} -.help-callout-string { - border-color: color-mix(in srgb, #ef4444 40%, var(--line)); - border-left-color: #ef4444; - background: color-mix(in srgb, #ef4444 6%, var(--panel)); -} -.help-callout-string h3 { - color: color-mix(in srgb, #ef4444 60%, var(--text)); -} -.help-callout-alt h3 { - color: color-mix(in srgb, #3b82f6 60%, var(--text)); -} -.help-callout ul { - margin: 6px 0; -} -.help-note { - padding: 8px 12px; - border-left: 3px solid #3b82f6; - background: color-mix(in srgb, #3b82f6 7%, var(--panel)); - border-radius: 6px; -} -.help-body p.cite { - padding-left: 12px; - border-left: 2px solid var(--accent); -} -.dl-group h3 { - margin: 0 0 4px; -} -.dl-refs { - margin: 0 0 6px; - font-size: 12.5px; -} -.dl-list { - margin: 0; - padding-left: 18px; -} -.dl-list li { - margin: 3px 0; -} -.dl-buttons { - white-space: nowrap; -} -.dl-buttons .dl-copy:first-child { - margin-left: 0; -} -.dl-copy { - margin-left: 8px; - padding: 2px 7px; - font-size: 11px; - vertical-align: 1px; -} -#viewBar { - position: sticky; - top: 0; - z-index: 5; - background: var(--panel); - margin: 0 -16px 14px; - padding: 12px 16px 0; - border-bottom: 1px solid var(--line); -} -.viewbar-head { - display: flex; - justify-content: space-between; - align-items: baseline; - margin-bottom: 4px; -} -.viewbar-head label { - font-size: 12px; - font-weight: 600; - color: var(--text); -} -#viewSelect { - font-weight: 600; -} -.view-actions { - margin-top: 6px; -} -.view-actions button { - padding: 5px 8px; - font-size: 11.5px; -} -#sideTabs { - display: flex; - margin-top: 10px; -} -#sideTabs button { - flex: 1; - background: none; - border: none; - border-radius: 0; - border-bottom: 2px solid transparent; - padding: 8px 4px 7px; - font-size: 12.5px; - font-weight: 600; - color: var(--muted); -} -#sideTabs button:hover { - color: var(--text); - background: none; -} -#sideTabs button[aria-selected='true'] { - color: var(--text); - border-bottom-color: var(--accent); -} -.row.stack { - flex-direction: column; - gap: 6px; -} -.seg { - display: flex; - border: 1px solid var(--line); - border-radius: 7px; - padding: 2px; - background: var(--bg); - margin-bottom: 8px; -} -.seg button { - flex: 1; - border: none; - background: none; - border-radius: 5px; - padding: 6px 4px; - font-size: 12px; - font-weight: 600; - color: var(--muted); -} -.seg button:hover { - background: none; - color: var(--text); -} -.seg button[aria-checked='true'] { - background: var(--accent); - color: var(--accent-text); -} -.layout-set { - border: 1px solid var(--line); - border-radius: 7px; - margin: 0 0 8px; - padding: 8px 10px 10px; - min-width: 0; - transition: opacity 0.15s; -} -.layout-set legend { - font-size: 11px; - font-weight: 600; - color: var(--muted); - padding: 0 4px; -} -.layout-set:disabled { - opacity: 0.45; -} -.layout-set:disabled .strategy-fig { - display: none; -} -.layout-set:disabled legend::after { - content: ' (frozen)'; - font-weight: 400; -} -.hint-line { - font-size: 11px; - color: var(--muted); - line-height: 1.4; - margin-top: 4px; -} -#sidebar::-webkit-scrollbar { - width: 8px; -} -#sidebar::-webkit-scrollbar-thumb { - background: var(--line); - border-radius: 4px; -} -.section { - margin-bottom: 22px; -} -.section h3 { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.8px; - color: var(--muted); - margin: 0 0 10px; - font-weight: 600; -} -select { - width: 100%; - background: var(--bg); - color: var(--text); - border: 1px solid var(--line); - padding: 8px 10px; - border-radius: 6px; - font-size: 13px; - outline: none; -} -.row { - display: flex; - gap: 8px; - margin-top: 8px; -} -/* buttons in a row share the width equally and may shrink below their - text width, so a row never sticks out of its (padded) section */ -.row > button { - flex: 1 1 0; - min-width: 0; - padding-left: 6px; - padding-right: 6px; -} -.row.three > button { - font-size: 12px; - padding-left: 4px; - padding-right: 4px; -} -input[type='range'] { - width: 100%; - margin-left: 0; - margin-right: 0; - box-sizing: border-box; -} -.toggle-row { - display: flex; - align-items: center; - gap: 9px; - padding: 6px 0; - font-size: 12.5px; - cursor: pointer; - user-select: none; -} -.toggle-row input { - accent-color: var(--accent); - width: 14px; - height: 14px; - cursor: pointer; -} -.swatch { - width: 22px; - height: 3px; - border-radius: 2px; - flex-shrink: 0; -} -input[type='color'].swatch-picker { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 14px; - flex-shrink: 0; - border: 1px solid var(--line); - padding: 0; - border-radius: 3px; - background: none; - cursor: pointer; -} -input[type='color'].swatch-picker::-webkit-color-swatch-wrapper { - padding: 0; -} -input[type='color'].swatch-picker::-webkit-color-swatch { - border: none; - border-radius: 2px; -} -input[type='color'].swatch-picker::-moz-color-swatch { - border: none; - border-radius: 2px; -} -input[type='color'].swatch-picker.round { - width: 14px; - height: 14px; - border-radius: 50%; -} -input[type='color'].swatch-picker.round::-webkit-color-swatch { - border-radius: 50%; -} -input[type='color'].swatch-picker.round::-moz-color-swatch { - border-radius: 50%; -} -.toggle-row .label { - flex: 1; - color: var(--text); -} -.toggle-row .count { - color: var(--muted); - font-family: var(--mono); - font-size: 11px; -} -#edgeLegend, -#nodeGroupLegend { - max-height: 260px; - overflow-y: auto; - padding-right: 4px; -} -#edgeLegend::-webkit-scrollbar, -#nodeGroupLegend::-webkit-scrollbar { - width: 6px; -} -#edgeLegend::-webkit-scrollbar-thumb, -#nodeGroupLegend::-webkit-scrollbar-thumb { - background: var(--line); - border-radius: 3px; -} -textarea { - width: 100%; - height: 110px; - background: var(--bg); - color: var(--text); - border: 1px solid var(--line); - border-radius: 6px; - font-family: var(--mono); - font-size: 11.5px; - padding: 8px; - resize: vertical; - outline: none; -} -textarea:focus { - border-color: var(--accent-dim); -} -#canvas { - grid-area: canvas; - position: relative; - background: - radial-gradient(circle at 1px 1px, var(--canvas-dot) 1.5px, transparent 0) 0 0/22px 22px, - var(--bg); -} -#groupHullCanvas { - position: absolute; - inset: 0; - pointer-events: none; -} -#cy { - position: absolute; - inset: 0; -} -#cy3d { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - display: block; - cursor: grab; - outline: none; - touch-action: none; -} -#cy3d[hidden] { - display: none !important; -} -#canvas.mode3d #cy, -#canvas.mode3d #groupHullCanvas { - visibility: hidden; -} -#canvas.mode3d { - background: var(--bg); -} -#selectBox3d { - position: absolute; - border: 1px solid var(--accent); - background: color-mix(in srgb, var(--accent) 12%, transparent); - pointer-events: none; -} -#nav3d { - display: flex; - flex-direction: column; - gap: 6px; - margin-top: 4px; - padding-top: 8px; - border-top: 1px solid var(--line); -} -.nav3d-pad { - display: grid; - grid-template-columns: repeat(3, 32px); - gap: 3px; -} -#zoomctl #nav3d button { - width: 32px; - height: 32px; -} -#zoomctl #nav3d #btnAuto3d { - padding: 0 8px; - font-size: 11px; - width: 102px; -} -#zoomctl, -#hint, -#nav3d { - user-select: none; - -webkit-user-select: none; -} -#canvas.mode3d #hint { - left: 132px; -} -#btnAuto3d[aria-pressed='true'] { - background: var(--accent); - color: var(--accent-text); - border-color: var(--accent); -} -#infopanel { - position: absolute; - bottom: 16px; - right: 16px; - width: 280px; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 10px; - padding: 14px 16px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); - display: none; - max-height: 60%; - overflow-y: auto; -} -#infopanel.show { - display: block; -} -#infopanel h4 { - margin: 0 0 2px; - font-size: 15px; - display: flex; - align-items: center; - gap: 8px; -} -#infopanel .nodedot { - width: 12px; - height: 12px; - border-radius: 50%; - flex-shrink: 0; -} -#infopanel .sub { - font-size: 11px; - color: var(--muted); - margin-bottom: 10px; - font-family: var(--mono); -} -#infopanel .connlist { - font-size: 12px; -} -#infopanel .conn { - display: flex; - justify-content: space-between; - gap: 8px; - padding: 5px 0; - border-top: 1px solid var(--line); -} -#infopanel .conn .ename { - color: var(--muted); -} -#infopanel .close { - position: absolute; - top: 10px; - right: 10px; - background: none; - border: none; - color: var(--muted); - cursor: pointer; - font-size: 16px; - padding: 2px 6px; -} -#edgePopup { - position: absolute; - width: 250px; - max-height: 60%; - overflow-y: auto; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 10px; - padding: 13px 15px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); - display: none; - z-index: 30; -} -#edgePopup.show { - display: block; -} -#edgePopup .eptitle { - display: flex; - align-items: center; - gap: 8px; - font-weight: 600; - font-size: 13.5px; - margin: 0 0 8px; -} -#edgePopup .epdot { - width: 11px; - height: 11px; - border-radius: 50%; - flex-shrink: 0; -} -#edgePopup .eprow { - display: flex; - justify-content: space-between; - gap: 10px; - padding: 4px 0; - border-top: 1px solid var(--line); - font-size: 12px; - color: var(--muted); -} -#edgePopup .eprow span:last-child { - color: var(--text); - font-family: var(--mono); - font-size: 11px; - text-align: right; -} -#edgePopup .close { - position: absolute; - top: 8px; - right: 8px; - background: none; - border: none; - color: var(--muted); - cursor: pointer; - font-size: 15px; - padding: 2px 6px; -} -#selectionBar { - position: absolute; - top: 12px; - right: 12px; - z-index: 20; - display: flex; - align-items: center; - gap: 6px; - background: var(--panel); - border: 1px solid var(--accent); - border-radius: 8px; - padding: 5px 6px 5px 10px; - font-size: 12px; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); -} -#selectionBar button { - padding: 4px 8px; - font-size: 11.5px; -} -#selectionText { - font-weight: 600; - margin-right: 2px; -} -.undo-group { - display: flex; - gap: 2px; -} -.undo-group button { - width: 32px; - height: 32px; - padding: 0; - display: grid; - place-items: center; -} -.undo-group svg { - width: 16px; - height: 16px; -} -#searchPanel { - position: absolute; - top: 12px; - left: 12px; - z-index: 20; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; - padding: 8px; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); - width: min(400px, calc(100% - 24px)); -} -#searchPanel .search-row { - display: flex; - gap: 6px; - align-items: center; -} -#search { - flex: 1; - min-width: 0; - width: auto; -} -#search[aria-invalid='true'] { - border-color: var(--warn); -} -#searchMode { - width: auto; - flex-shrink: 0; - padding: 7px 8px; - font-size: 12.5px; -} -#searchPanel .search-opts { - margin-top: 6px; - gap: 12px; - font-size: 11.5px; - color: var(--muted); - flex-wrap: wrap; -} -#searchPanel .search-opts label { - display: flex; - align-items: center; - gap: 5px; - cursor: pointer; - user-select: none; -} -#searchPanel .search-opts input { - accent-color: var(--accent); - margin: 0; -} -#searchStatus { - margin-left: auto; - font-family: var(--mono); - font-size: 11px; -} -#searchStatus.error { - color: var(--warn); - font-family: var(--sans); - margin-left: 0; - flex-basis: 100%; -} -#zoomLevel { - font-family: var(--mono); - font-size: 10px; - color: var(--muted); - text-align: center; - padding-top: 2px; -} -#zoomctl button.fit { - font-size: 11px; -} -#hint { - position: absolute; - bottom: 16px; - left: 64px; - font-size: 11.5px; - color: var(--muted); - background: var(--panel); - border: 1px solid var(--line); - padding: 6px 10px; - border-radius: 6px; - pointer-events: none; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); -} -/* NORMA-2.0 strategy schematic (after Fig. 1 of Karatzas et al. 2022) */ -.strategy-fig { - margin: 8px 0 0; - padding: 8px 8px 6px; - background: var(--bg); - border: 1px solid var(--line); - border-radius: 6px; -} -.strategy-fig svg { - display: none; - width: 100%; - height: auto; -} -.strategy-fig svg.on { - display: block; -} -.strategy-fig .e { - stroke: var(--muted); - stroke-width: 1.3; -} -.strategy-fig .e.add { - stroke-dasharray: 3 3; -} -.strategy-fig .e.heavy { - stroke-width: 3; -} -.strategy-fig .e.faint { - opacity: 0.35; -} -.strategy-fig .na { - fill: #e8a15f; -} -.strategy-fig .nb { - fill: #5fd3c4; -} -.strategy-fig .nab { - fill: url(#figSplit); -} -.strategy-fig .n0 { - fill: #9aa0a6; -} -.strategy-fig .vn { - fill: var(--bg); - stroke: var(--text); - stroke-width: 1.3; - stroke-dasharray: 2 2; -} -.strategy-fig .ring { - fill: none; - stroke: var(--line); - stroke-dasharray: 2 3; -} -.strategy-fig .arrow { - fill: none; - stroke: var(--muted); - stroke-width: 1.5; -} -.strategy-fig text { - fill: var(--muted); - font-size: 9.5px; - font-family: var(--sans); -} -.strategy-fig figcaption { - font-size: 11px; - color: var(--muted); - line-height: 1.45; - margin-top: 6px; -} -/* shared key/value row for node, group and edge attributes */ -.kvrow { - display: flex; - justify-content: space-between; - gap: 8px; - padding: 2px 0; -} -.kvrow span:first-child { - color: var(--muted); - flex-shrink: 0; -} -.kvrow span:last-child { - color: var(--text); - text-align: right; - word-break: break-word; -} -.kvhead { - color: var(--muted); - margin-bottom: 3px; -} -#infopanel .desc { - font-size: 12px; - color: var(--text); - line-height: 1.45; - margin: -4px 0 10px; -} -#infopanel .conn.jump { - cursor: pointer; -} -#infopanel .conn.jump:hover span:first-child { - color: var(--accent); -} -.glink { - background: none; - border: none; - padding: 0; - margin: 0; - font: inherit; - color: inherit; - text-decoration: underline dotted; - text-underline-offset: 2px; - cursor: pointer; -} -.glink:hover { - color: var(--accent); - background: none; -} -.toggle-row .ginfo { - width: 18px; - height: 18px; - padding: 0; - flex-shrink: 0; - border-radius: 50%; - font-size: 10.5px; - font-weight: 600; - line-height: 1; - font-family: var(--mono); - color: var(--muted); -} -.toggle-row .ginfo:hover { - color: var(--text); -} -button:focus-visible, -input:focus-visible, -select:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 1px; -} -/* NORMA files */ -.field-label { - font-size: 11px; - color: var(--muted); - display: block; - margin: 10px 0 3px; -} -.select-row { - display: flex; - gap: 6px; -} -.select-row select { - flex: 1; - min-width: 0; -} -.select-row button { - flex-shrink: 0; - padding: 7px 9px; -} -button:disabled { - opacity: 0.45; - cursor: not-allowed; -} -button:disabled:hover { - border-color: var(--line); - background: var(--panel-2); -} -.text-input { - width: 100%; - background: var(--bg); - color: var(--text); - border: 1px solid var(--line); - border-radius: 6px; - padding: 7px 10px; - font-size: 12.5px; - outline: none; -} -.text-input:focus { - border-color: var(--accent-dim); -} -.status { - margin-top: 10px; -} -.status:empty { - display: none; -} -.status:empty { - display: none; -} -.status .note { - font-size: 11.5px; - line-height: 1.45; - padding: 5px 0 5px 10px; - border-left: 2px solid var(--line); - margin-bottom: 4px; - overflow-wrap: anywhere; -} -.status .note.ok { - border-left-color: var(--accent); -} -.status .note.warn { - border-left-color: var(--warn); - color: var(--text); -} -.status .note.error { - border-left-color: var(--warn); - color: var(--warn); - font-weight: 600; -} -.status .note button { - display: block; - margin-top: 6px; -} -.status .note.busy { - border-left-color: var(--muted); - color: var(--muted); -} -.status .note .progress-row { - display: flex; - align-items: center; - gap: 8px; - margin-top: 6px; -} -.status .note .progress { - position: relative; - flex: 1; - height: 6px; - border-radius: 3px; - overflow: hidden; - background: color-mix(in srgb, var(--muted) 22%, transparent); -} -.status .note .progress > span { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 0; - border-radius: 3px; - background: var(--accent); - transition: width 0.25s ease; -} -.status .note .progress.indeterminate > span { - width: 35%; - animation: progress-slide 1.3s ease-in-out infinite; -} -.status .note .progress-pct { - font-family: var(--mono); - font-size: 11px; - min-width: 3.2em; - text-align: right; - color: var(--text); -} -@keyframes progress-slide { - from { - transform: translateX(-100%); - } - to { - transform: translateX(290%); - } -} -@media (prefers-reduced-motion: reduce) { - .status .note .progress.indeterminate > span { - animation: none; - width: 100%; - opacity: 0.45; - } - .status .note .progress > span { - transition: none; - } -} - -/* sidebar structure */ -.section h3 { - display: flex; - align-items: center; - justify-content: space-between; -} -.helplink { - width: 18px; - height: 18px; - padding: 0; - border-radius: 50%; - font-size: 10.5px; - line-height: 1; - font-weight: 600; - color: var(--muted); - background: transparent; - letter-spacing: 0; - text-transform: none; -} -.helplink:hover { - color: var(--text); -} -.slider-label { - font-size: 11px; - color: var(--muted); - display: flex; - justify-content: space-between; - margin-top: 8px; -} -input[type='range'] { - width: 100%; - accent-color: var(--accent); -} -.num-input { - width: 100%; - background: var(--bg); - color: var(--text); - border: 1px solid var(--line); - border-radius: 6px; - padding: 6px 8px; - font-size: 12.5px; -} -.upload-box { - border: 1px dashed var(--line); - border-radius: 8px; - padding: 8px; - transition: - border-color 0.15s, - background 0.15s; -} -.upload-box.dragging { - border-color: var(--accent); - background: var(--panel-2); -} -.drop-hint { - font-size: 11px; - color: var(--muted); - text-align: center; - margin-top: 6px; -} -.lib-head { - display: flex; - justify-content: space-between; - align-items: baseline; - font-size: 12px; - font-weight: 600; - margin: 12px 0 4px; -} -.lib-hint { - font-size: 11px; - color: var(--muted); - font-weight: 400; -} -.lib-list { - max-height: 176px; - overflow-y: auto; - border: 1px solid var(--line); - border-radius: 6px; - background: var(--bg); -} -.lib-row { - display: flex; - align-items: center; - gap: 8px; - padding: 5px 4px 5px 8px; - font-size: 12px; - cursor: pointer; - border-top: 1px solid var(--line); -} -.lib-row:first-child { - border-top: none; -} -.lib-row:hover { - background: var(--panel-2); -} -.lib-row input { - accent-color: var(--accent); - margin: 0; - flex-shrink: 0; - cursor: pointer; -} -.lib-text { - flex: 1; - min-width: 0; -} -.lib-name { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.lib-meta { - display: block; - font-size: 10.5px; - color: var(--muted); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.lib-remove { - flex-shrink: 0; - width: 22px; - height: 22px; - padding: 0; - background: transparent; - border-color: transparent; - color: var(--muted); - font-size: 12px; -} -.lib-remove:hover { - color: var(--warn); - border-color: var(--line); -} -.lib-empty { - font-size: 11.5px; - color: var(--muted); - padding: 7px 8px; -} -.refresh-hint { - font-size: 11px; - color: var(--warn); - margin-top: 5px; -} -#btnRefreshView.pending { - box-shadow: - 0 0 0 2px var(--panel), - 0 0 0 3px var(--warn); -} - -/* tabs */ -#tabs { - display: flex; - gap: 2px; - height: 100%; - align-items: stretch; -} -.tab { - background: none; - border: none; - border-radius: 0; - border-bottom: 2px solid transparent; - padding: 0 12px; - font-size: 13px; - color: var(--muted); -} -.tab:hover { - background: none; - color: var(--text); - border-color: transparent; - border-bottom-color: var(--line); -} -.tab[aria-selected='true'] { - color: var(--text); - border-bottom-color: var(--accent); - font-weight: 600; -} -#topTools { - margin-right: auto; - margin-left: 10px; -} -#topbar { - padding: 0 18px 0 8px; -} - -/* full-page views */ -.page { - grid-area: canvas; - overflow-y: auto; - background: var(--bg); - display: none; -} -.page.active { - display: block; -} -#canvas.inactive { - display: none; -} -.page-inner { - max-width: 1180px; - padding: 26px 32px 64px; -} -.page-head h2 { - margin: 0 0 6px; - font-size: 20px; - font-weight: 600; -} -.page-head p { - margin: 0 0 18px; - color: var(--muted); - font-size: 13px; - line-height: 1.5; - max-width: 72ch; -} - -/* profiler */ -.prof-controls { - display: flex; - gap: 18px; - align-items: flex-end; - flex-wrap: wrap; -} -.prof-netlist { - display: flex; - flex-wrap: wrap; - gap: 6px; - max-width: 760px; -} -.prof-netlist label { - display: flex; - align-items: center; - gap: 6px; - border: 1px solid var(--line); - border-radius: 6px; - background: var(--panel); - padding: 5px 9px; - font-size: 12px; - cursor: pointer; -} -.prof-netlist input { - accent-color: var(--accent); - margin: 0; -} -.prof-actions .toggle-row { - margin: 0 6px 0 0; -} -.prof-actions { - display: flex; - gap: 8px; -} -.prof-section { - margin-top: 28px; -} -.prof-section h3 { - font-size: 14px; - font-weight: 600; - margin: 0 0 10px; -} -.prof-section .sub { - font-size: 12px; - color: var(--muted); - margin: -4px 0 10px; -} -.table-wrap { - overflow-x: auto; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); -} -table.data { - border-collapse: collapse; - width: 100%; - font-size: 12.5px; -} -table.data th, -table.data td { - padding: 6px 12px; - border-bottom: 1px solid var(--line); - text-align: right; - white-space: nowrap; -} -table.data th:first-child, -table.data td:first-child { - text-align: left; -} -table.data thead th { - font-weight: 600; - background: var(--panel-2); - position: sticky; - top: 0; -} -table.data tbody tr:last-child td { - border-bottom: none; -} -table.data td.num { - font-family: var(--mono); - font-size: 12px; -} -table.data td .stat-desc { - display: block; - font-size: 11px; - color: var(--muted); - line-height: 1.35; - max-width: 34ch; - white-space: normal; - margin-top: 1px; -} -.cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 14px; -} -.card { - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - padding: 12px 14px; - min-width: 0; -} -.card h4 { - margin: 0 0 8px; - font-size: 13px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.card .meta { - font-size: 11.5px; - color: var(--muted); - margin: 0 0 8px; - line-height: 1.45; -} -.chart svg { - display: block; - width: 100%; - height: auto; -} -.chart .bar { - fill: var(--accent); -} -.chart .dot { - fill: var(--accent); -} -.chart .axis { - stroke: var(--line); -} -.chart text { - fill: var(--muted); - font-size: 10px; - font-family: var(--sans); -} -.inline-controls { - display: flex; - gap: 14px; - align-items: center; - font-size: 12px; - margin-bottom: 10px; - flex-wrap: wrap; -} -.inline-controls label { - display: flex; - gap: 6px; - align-items: center; - cursor: pointer; -} -.inline-controls select { - width: auto; -} -.linkish { - background: none; - border: none; - padding: 0; - color: var(--text); - font: inherit; - text-decoration: underline dotted; - text-underline-offset: 2px; - cursor: pointer; -} -.linkish:hover { - background: none; - color: var(--accent); -} -.size-strip { - display: flex; - height: 10px; - border-radius: 3px; - overflow: hidden; - margin: 6px 0 10px; - background: var(--panel-2); -} -.size-strip span { - display: block; - height: 100%; - border-right: 1px solid var(--panel); -} - -/* help */ -.help-layout { - display: grid; - grid-template-columns: 220px minmax(0, 1fr); - gap: 28px; - padding: 26px 32px 80px; - max-width: 1100px; -} -.help-nav { - position: sticky; - top: 20px; - align-self: start; - display: flex; - flex-direction: column; - gap: 1px; - max-height: calc(100vh - 170px); - overflow-y: auto; - overscroll-behavior: contain; - padding-right: 6px; - scrollbar-width: thin; -} -.help-nav a { - flex-shrink: 0; -} -.help-nav-group { - margin: 14px 0 4px; - padding: 0 10px; - font-size: 10.5px; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--muted); - flex-shrink: 0; -} -.help-nav-group:first-child { - margin-top: 0; -} -.help-nav .help-nav-about { - color: var(--accent); -} -.cite-box { - margin: 0 0 18px; - padding: 14px 18px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line)); - border-left: 5px solid var(--accent); - background: color-mix(in srgb, var(--accent) 8%, var(--panel)); -} -.cite-box > b { - display: block; - font-size: 15px; - margin-bottom: 4px; -} -.cite-list { - margin: 6px 0 8px; - padding-left: 20px; -} -.cite-list li { - margin: 6px 0; - font-size: 13px; - line-height: 1.5; - padding-left: 2px; -} -.cite-list li::marker { - color: var(--accent); -} -.cite-note { - color: var(--muted); - font-size: 12px; -} -.cite-box p { - margin: 6px 0; - font-size: 13px; - line-height: 1.5; -} -.cite-box .cite-more { - color: var(--muted); - font-size: 12.5px; -} -.about-cite-box h2 { - margin-top: 0 !important; -} -.help-nav .help-nav-cite { - font-weight: 600; - color: var(--accent); - margin-bottom: 4px; -} -.api-title { - margin: 0 0 8px; - font-size: 26px; -} -.api-ways { - margin: 12px 0 8px; -} -.api-base { - color: var(--muted); - font-size: 13px; -} -.api-code-wrap { - position: relative; - margin: 8px 0 14px; -} -.api-code { - margin: 0; - padding: 12px 14px; - border-radius: 8px; - background: var(--panel-2, var(--panel)); - border: 1px solid var(--line); - overflow-x: auto; - font-size: 12.5px; - line-height: 1.5; -} -.api-copy { - position: absolute; - top: 6px; - right: 6px; - padding: 2px 10px; - font-size: 11.5px; - z-index: 1; -} -.api-payload { - width: 100%; - min-height: 300px; - font-family: var(--mono); - font-size: 12.5px; - line-height: 1.45; -} -.api-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 10px 0; -} -.api-result { - display: flex; - gap: 8px; - align-items: center; - margin: 6px 0; -} -.api-result input { - flex: 1; - font-family: var(--mono); - font-size: 12px; -} -.help-part { - margin: 34px 0 -6px; - padding-top: 14px; - border-top: 2px solid var(--line); - font-size: 11.5px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--accent); -} -.help-intro { - font-size: 14px; - color: var(--muted); -} -.brand-home { - display: inline-flex; - border-radius: 6px; -} -.brand-home:hover .brand-logo { - filter: brightness(1.06); -} -.brand-home:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} -.about-layout { - max-width: 960px; - margin: 0 auto; - padding: 28px 32px 60px; - line-height: 1.6; - font-size: 14px; -} -.about-layout h2 { - margin: 34px 0 10px; - font-size: 20px; - scroll-margin-top: 12px; -} -.about-layout h3 { - margin: 18px 0 6px; - font-size: 15px; -} -.about-hero { - display: flex; - align-items: center; - gap: 24px; - margin-bottom: 14px; -} -.about-hero h1 { - margin: 0 0 6px; - font-size: 26px; -} -.about-hero h1 span { - font-weight: 500; - color: var(--muted); - font-size: 18px; - margin-left: 6px; -} -.about-hero p { - margin: 0; - color: var(--muted); - max-width: 70ch; -} -.about-logo { - flex-shrink: 0; -} -.about-logo-svg { - height: 84px !important; - width: auto; -} -.about-toc { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin: 10px 0 16px; -} -.about-toc a { - padding: 4px 10px; - border-radius: 999px; - border: 1px solid var(--line); - background: var(--panel); - font-size: 12px; - text-decoration: none; - color: var(--text); -} -.about-toc a:hover { - border-color: var(--accent); -} -.about-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 10px; -} -.about-card { - display: flex; - flex-direction: column; - gap: 4px; - padding: 12px 14px; - border-radius: 10px; - border: 1px solid var(--line); - background: var(--panel); - border-top: 3px solid var(--accent); -} -.about-card span { - color: var(--muted); - font-size: 12.5px; - line-height: 1.45; -} -.about-layout h2.flash { - animation: flash 1.2s ease-out; -} -@media (max-width: 760px) { - .about-hero { - flex-direction: column; - align-items: flex-start; - } -} -.help-nav a { - color: var(--muted); - text-decoration: none; - font-size: 12.5px; - padding: 4px 10px; - border-left: 2px solid var(--line); -} -.help-nav a:hover, -.help-nav a.current { - color: var(--text); - border-left-color: var(--accent); -} -.help-body { - max-width: 74ch; - font-size: 14px; - line-height: 1.6; -} -.help-body h2 { - font-size: 18px; - font-weight: 600; - margin: 34px 0 8px; - scroll-margin-top: 16px; -} -.help-body h2:first-child { - margin-top: 0; -} -.help-body h3 { - font-size: 14px; - font-weight: 600; - margin: 18px 0 4px; -} -.help-body p, -.help-body li { - color: var(--text); -} -.help-body a, -.about-layout a { - color: var(--accent); -} -.help-body code { - font-family: var(--mono); - font-size: 12.5px; - background: var(--panel-2); - padding: 1px 4px; - border-radius: 3px; -} -.help-body pre { - font-family: var(--mono); - font-size: 12px; - line-height: 1.5; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 6px; - padding: 10px 12px; - overflow-x: auto; - tab-size: 12; -} -.help-body h2.flash { - animation: flash 1.2s ease-out; -} -@keyframes flash { - from { - color: var(--accent); - } - to { - color: var(--text); - } -} -.help-table { - border-collapse: collapse; - font-size: 12.5px; - line-height: 1.45; - margin: 8px 0 12px; - width: 100%; -} -.help-table th, -.help-table td { - text-align: left; - vertical-align: top; - padding: 6px 10px 6px 0; - border-bottom: 1px solid var(--line); -} -.help-table th { - font-weight: 600; -} -.help-table code { - font-size: 11px; -} -@media (prefers-reduced-motion: reduce) { - .help-body h2.flash { - animation: none; - } -} -@media (max-width: 900px) { - .help-layout { - grid-template-columns: 1fr; - padding: 18px; - } - .help-nav { - position: static; - flex-direction: row; - flex-wrap: wrap; - } -} -#attrSchema .schema-block { - margin-bottom: 10px; -} -#attrSchema .schema-head { - display: flex; - justify-content: space-between; - font-size: 12px; - font-weight: 600; - padding-bottom: 3px; - border-bottom: 1px solid var(--line); - margin-bottom: 3px; -} -#attrSchema .schema-head span { - color: var(--muted); - font-weight: 400; - font-family: var(--mono); - font-size: 11px; -} -#attrSchema .schema-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0 8px; - padding: 3px 0; - font-size: 12px; -} -#attrSchema .schema-row .k { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -#attrSchema .schema-row .cov { - color: var(--muted); - font-family: var(--mono); - font-size: 11px; - text-align: right; -} -#attrSchema .schema-row .kind { - grid-column: 1 / -1; - color: var(--muted); - font-size: 11px; -} -#attrSchema .schema-empty { - font-size: 11.5px; - color: var(--muted); - margin: 0 0 4px; -} -#zoomctl { - position: absolute; - bottom: 16px; - left: 16px; - display: flex; - flex-direction: column; - gap: 6px; -} -#zoomctl button { - width: 32px; - height: 32px; - padding: 0; - font-size: 16px; - line-height: 1; - border-radius: 8px; -} - -/* ---------- comparison page ---------- */ -.cmp-controls { - display: flex; - gap: 18px; - align-items: flex-end; - flex-wrap: wrap; -} -.cmp-pick { - flex: 1; - min-width: 320px; -} -.cmp-count { - color: var(--muted); - font-weight: 400; - margin-left: 6px; -} -.cmp-netlist { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); - gap: 4px 12px; - padding: 10px 12px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - max-height: 240px; - overflow-y: auto; -} -.cmp-group-head { - grid-column: 1 / -1; - font-size: 11px; - font-weight: 600; - color: var(--muted); - margin-top: 4px; -} -.cmp-option { - display: flex; - gap: 7px; - align-items: flex-start; - font-size: 12.5px; - cursor: pointer; - padding: 3px 0; - min-width: 0; -} -.cmp-option input { - margin-top: 2px; - accent-color: var(--accent); -} -.cmp-option input:disabled + span { - opacity: 0.45; -} -.cmp-name { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.cmp-meta { - display: block; - font-size: 11px; - color: var(--muted); -} -.cmp-overview { - display: flex; - gap: 10px; - flex-wrap: wrap; -} -.cmp-card { - display: flex; - gap: 10px; - align-items: center; - min-width: 200px; - max-width: 280px; - flex: 1; - padding: 8px 12px; - border-radius: 8px; - background: color-mix(in srgb, var(--c) 9%, var(--panel)); - border: 1px solid color-mix(in srgb, var(--c) 35%, var(--line)); -} -.cmp-letter { - width: 26px; - height: 26px; - border-radius: 50%; - background: var(--c); - color: #fff; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} -.cmp-card-body { - min-width: 0; -} -.cmp-card-name { - font-weight: 600; - font-size: 13px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.cmp-card-meta { - font-size: 11.5px; - color: var(--muted); -} -.cmp-legend { - display: flex; - gap: 14px; - flex-wrap: wrap; - font-size: 12px; - margin: 0 0 8px; -} -.cmp-legend i, -.cmp-matrix i, -.cmp-topo i { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 5px; - vertical-align: -1px; -} -.cmp-row { - display: flex; - gap: 14px; - flex-wrap: wrap; - align-items: flex-start; -} -.cmp-venn { - flex: 0 0 340px; -} -.cmp-upset { - flex: 1 1 420px; - overflow-x: auto; -} -.cmp-ccdf { - max-width: 620px; -} -.chart .venn-count { - fill: var(--text); - font-size: 13px; - font-weight: 600; -} -.chart .venn-set { - font-size: 12px; - font-weight: 600; -} -.chart .upset-num { - fill: var(--text); - font-size: 9.5px; -} -.chart .upset-set { - fill: var(--text); - font-size: 11px; -} -.chart .upset-on { - fill: var(--text); -} -.chart .upset-off { - fill: var(--line); -} -.chart .upset-link { - stroke: var(--text); - stroke-width: 2; -} -.chart .upset-stripe { - fill: color-mix(in srgb, var(--text) 4%, transparent); -} -.cmp-subhead { - margin: 16px 0 2px; - font-size: 13px; -} -.cmp-matrix { - border-collapse: collapse; - font-size: 12.5px; - margin-top: 6px; -} -.cmp-matrix th, -.cmp-matrix td { - border: 1px solid var(--line); - padding: 6px 10px; - text-align: center; - min-width: 52px; -} -.cmp-matrix th { - background: var(--panel); - font-weight: 600; -} -.cmp-matrix td { - font-family: var(--mono); -} -.cmp-matrix td.na { - color: var(--muted); -} -.cmp-topo th.cmp-rowhead { - text-align: left; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--muted); - background: var(--panel); -} -.cmp-topo td.cmp-max { - font-weight: 700; - color: color-mix(in srgb, #2563eb 70%, var(--text)); -} - -/* ---------- search panel collapse ---------- */ -#searchPanel .search-toggle { - flex-shrink: 0; - min-width: 28px; - height: 28px; - padding: 0 7px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} -#searchPanel .search-toggle svg, -#searchPanel .search-min svg { - width: 14px; - height: 14px; - flex-shrink: 0; -} -#searchPanel .search-min { - flex-shrink: 0; - width: 28px; - height: 28px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; -} -#searchPanel .search-toggle-text { - display: none; - font-size: 12.5px; - font-weight: 600; -} -#searchPanel.collapsed { - width: auto; - padding: 5px; -} -#searchPanel.collapsed .search-row > :not(.search-toggle), -#searchPanel.collapsed .search-opts { - display: none; -} -#searchPanel.collapsed .search-toggle-text { - display: inline; -} -#searchPanel .search-badge { - display: none; - font-size: 11px; - font-family: var(--mono); - color: var(--muted); -} -#searchPanel.collapsed .search-badge:not(:empty) { - display: inline; -} - -/* ---------- label color controls ---------- */ -.label-color-row { - display: flex; - gap: 6px; - align-items: center; -} -.label-color-row select { - flex: 1; - min-width: 0; -} -.label-color-row input[type='color'] { - width: 34px; - height: 32px; - padding: 2px; - border: 1px solid var(--line); - border-radius: 6px; - background: var(--bg); - flex-shrink: 0; -} -.info-shapes { - display: flex; - flex-wrap: wrap; - gap: 3px; - margin: 4px 0 10px; -} -.info-shapes .shape-opt { - width: 24px; - height: 24px; - padding: 4px; -} - -/* ---------- brand, banner, empty state, icons ---------- */ -#brand .dot { - display: none; -} -.brand-logo { - height: 38px; - width: auto; - flex-shrink: 0; - display: block; - margin-left: -4px; -} -.brand-logo .lg-edge { - stroke: var(--text); - opacity: 0.88; -} -.brand-logo .lg-ring { - stroke: var(--panel); -} -#brand .title { - display: flex; - flex-direction: column; - line-height: 1.1; -} -.brand-version { - font-size: 12px; - font-weight: 700; - color: var(--accent); -} -.norma-banner { - width: 100%; - height: auto; - display: block; - --bn-card: var(--panel); - --bn-line: var(--line); - --bn-edge: var(--muted); - --bn-stroke: var(--bg); - --bn-accent: var(--accent); - font-family: var(--sans); -} -.norma-banner .bn-cap { - font-size: 15px; - font-weight: 700; - fill: var(--text); -} -.norma-banner .bn-sub { - font-size: 12px; - fill: var(--muted); -} -.norma-banner .bn-tick { - font-size: 10px; - fill: var(--muted); -} -.help-banner { - margin: 0 0 18px; - width: min(920px, calc(100vw - 560px)); - max-width: none; -} -#emptyState { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - z-index: 3; - pointer-events: none; -} -#emptyState[hidden] { - display: none !important; -} -.welcome-page { - min-height: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - padding: 28px 24px 40px; -} -.welcome-page .welcome { - max-height: none; - overflow: visible; -} -.empty-card.compact { - max-width: 560px; - padding: 20px 24px; -} -.empty-card.compact h2 { - margin-top: 0; -} -.welcome { - pointer-events: auto; - width: min(1080px, 100%); - max-height: 100%; - overflow: auto; - background: color-mix(in srgb, var(--panel) 95%, transparent); - border: 1px solid var(--line); - border-radius: 18px; - padding: 22px 28px 18px; - box-shadow: 0 18px 50px rgba(0, 0, 0, 0.12); -} -.welcome-head { - display: flex; - align-items: center; - gap: 22px; - margin-bottom: 12px; -} -.welcome-logo-svg { - height: 72px !important; - width: auto; -} -.welcome-title h1 { - margin: 0 0 4px; - font-size: 22px; - letter-spacing: 0.2px; -} -.welcome-title p { - margin: 0; - color: var(--muted); - font-size: 13.5px; - line-height: 1.5; - max-width: 70ch; -} -.welcome .empty-banner { - margin: 6px 0 12px; -} -.welcome-steps { - list-style: none; - counter-reset: step; - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; - padding: 0; - margin: 0 0 14px; -} -.welcome-steps li { - counter-increment: step; - display: flex; - flex-direction: column; - gap: 2px; - padding: 10px 12px 10px 44px; - position: relative; - border-radius: 10px; - background: var(--bg); - border: 1px solid var(--line); - font-size: 13px; -} -.welcome-steps li::before { - content: counter(step); - position: absolute; - left: 12px; - top: 10px; - width: 22px; - height: 22px; - border-radius: 50%; - background: var(--accent); - color: #fff; - font-weight: 700; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; -} -.welcome-steps span { - color: var(--muted); - font-size: 12px; -} -button.danger { - background: #dc2626; - border-color: #dc2626; - color: #fff; - font-weight: 600; -} -button.danger:hover { - background: #b91c1c; - border-color: #b91c1c; -} -.danger-link { - background: none; - border: none; - color: var(--muted); - font-size: 12px; - padding: 2px 0; - margin-left: auto; - text-decoration: underline; -} -.danger-link:hover { - color: #dc2626; - background: none; -} -.danger-link:disabled { - opacity: 0.5; - text-decoration: none; -} -.lib-danger-row { - margin-top: 4px; -} -.delete-text p { - margin: 0 0 8px; - font-size: 13px; - line-height: 1.5; -} -.delete-choices { - margin: 4px 0 12px; - padding: 8px 10px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--bg); -} -.delete-choices label { - display: flex; - gap: 8px; - align-items: flex-start; - padding: 4px 0; - font-size: 13px; - cursor: pointer; -} -.delete-choices label small { - display: block; - color: var(--muted); - font-size: 11.5px; -} -.icon-del { - padding: 2px 7px; - min-width: 0; - line-height: 1.2; - color: var(--muted); -} -.icon-del:not(:disabled):hover { - color: #dc2626; - border-color: #dc2626; -} -.del-btn:hover { - color: #dc2626; - border-color: #dc2626; -} -#deleteDialog { - width: min(520px, calc(100vw - 32px)); -} -.list-empty { - margin: 0; - padding: 6px 2px; - font-size: 12.5px; - line-height: 1.45; - color: var(--muted); -} -.group-tools { - display: flex; - gap: 8px; - align-items: center; - margin-top: 6px; -} -.group-tools label { - font-size: 12px; - color: var(--muted); - flex-shrink: 0; -} -.group-tools select { - flex: 1; - min-width: 0; -} -.site-footer { - margin: 14px 0 0; - text-align: center; - font-size: 12px; - line-height: 1.7; - color: var(--text); -} -.site-footer .dot { - margin: 0 7px; - color: var(--muted); -} -.site-footer a { - color: var(--accent); -} -#siteNotice { - position: fixed; - left: 50%; - top: 8px; - transform: translateX(-50%); - z-index: 50; - max-width: min(760px, 92vw); - display: flex; - gap: 12px; - align-items: center; - padding: 8px 12px 8px 16px; - border-radius: 10px; - background: #fef3c7; - color: #78350f; - border: 1px solid #f59e0b; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); - font-size: 13px; -} -#siteNotice[hidden] { - display: none; -} -#siteNotice button { - padding: 2px 8px; - background: transparent; - border-color: #d97706; - color: #78350f; -} -.api-template-box { - margin-top: 6px; -} -.api-template-usage { - margin-left: 8px; - font-size: 12px; - color: var(--muted); -} -.api-caption { - caption-side: top; - text-align: left; - font-weight: 600; - padding: 0 0 6px; -} -.dl-name { - margin-left: 8px; - font-family: var(--mono); - font-size: 11px; - color: var(--muted); -} -.welcome-foot { - margin: 12px 0 0; - text-align: center; - color: var(--muted); - font-size: 11.5px; -} -#canvas.is-empty #searchPanel, -#canvas.is-empty #zoomctl, -#canvas.is-empty #hint, -#canvas.is-empty #nav3d, -#canvas.is-empty #frameBadge, -#canvas.is-empty #legendPanel { - display: none !important; -} -@media (max-width: 900px) { - .welcome-steps { - grid-template-columns: 1fr; - } - .welcome-head { - flex-direction: column; - align-items: flex-start; - } -} -.empty-card { - pointer-events: auto; - max-width: 980px; - width: 100%; - background: color-mix(in srgb, var(--panel) 92%, transparent); - border: 1px solid var(--line); - border-radius: 16px; - padding: 22px 26px; - box-shadow: 0 18px 50px rgba(0, 0, 0, 0.12); - text-align: center; -} -.empty-card h2 { - margin: 14px 0 4px; - font-size: 20px; -} -.empty-card p { - margin: 0 auto 14px; - color: var(--muted); - max-width: 62ch; - font-size: 13.5px; - line-height: 1.5; -} -.empty-actions { - display: flex; - gap: 8px; - justify-content: center; - flex-wrap: wrap; -} -.empty-actions button { - display: inline-flex; - align-items: center; - gap: 7px; - padding: 8px 14px; -} -.ico { - display: inline-flex; - width: 15px; - height: 15px; - flex-shrink: 0; -} -.ico svg { - width: 100%; - height: 100%; -} -.section > h3 .ico { - margin-right: 7px; - width: 14px; - height: 14px; - color: color-mix(in srgb, var(--tint) 75%, var(--text)); -} -.section > h3 { - justify-content: flex-start; -} -.section > h3 .helplink { - margin-left: auto; -} -.tab .ico, -#sideTabs .ico { - margin-right: 6px; - vertical-align: -2px; - opacity: 0.85; -} -#sideTabs button { - display: inline-flex; - align-items: center; - justify-content: center; -} -.string-brand { - display: flex; - align-items: center; - gap: 8px; - margin: -2px 0 10px; - text-decoration: none; - color: var(--muted); -} -.string-brand img { - height: 28px; - width: auto; - display: block; -} -.string-wordmark { - font-weight: 800; - letter-spacing: 1.5px; - font-size: 15px; - color: var(--text); - padding: 2px 8px; - border: 1.5px solid var(--line); - border-radius: 6px; -} -.string-tag { - font-size: 10.5px; - line-height: 1.25; -} -/* ---------- numeric values, legend, separation, benchmark ---------- */ -.value-box { - margin-top: 8px; - padding: 8px 10px; - border-radius: 6px; - border: 1px dashed color-mix(in srgb, var(--tint) 35%, var(--line)); -} -.value-scale-preview { - height: 10px; - border-radius: 3px; - margin-top: 6px; - border: 1px solid var(--line); -} -.inline-num { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--muted); - white-space: nowrap; -} -.inline-num .num-input { - width: 80px; -} -.inline-color { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - color: var(--muted); - margin-top: 8px; -} -.inline-color input { - width: 34px; - height: 24px; - padding: 1px; - border: 1px solid var(--line); - border-radius: 4px; - background: var(--bg); -} -#legendPanel { - position: absolute; - right: 16px; - bottom: 16px; - z-index: 4; - background: color-mix(in srgb, var(--panel) 94%, transparent); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.14); - max-height: calc(100% - 140px); - overflow: auto; -} -#legendPanel[hidden] { - display: none; -} -#canvas:has(#infopanel.show) #legendPanel, -#canvas:has(#edgePopup.show) #legendPanel { - display: none; -} -#legendPanel svg { - display: block; -} -.sep-box { - margin-top: 12px; - padding: 8px 10px; - border-radius: 6px; - background: var(--panel); - border: 1px solid color-mix(in srgb, var(--tint) 25%, var(--line)); -} -.sep-head { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - font-weight: 600; - margin-bottom: 4px; -} -.sep-row { - display: grid; - grid-template-columns: 1fr auto; - align-items: center; - gap: 2px 8px; - font-size: 12px; - color: var(--muted); - padding: 1px 0; -} -.sep-row b { - color: var(--text); - font-family: var(--mono); - font-weight: 600; -} -.sep-bar { - grid-column: 1 / -1; - position: relative; - height: 5px; - border-radius: 3px; - background: linear-gradient(90deg, #ef4444, #f59e0b 50%, #10b981); - opacity: 0.75; -} -.sep-bar i { - position: absolute; - top: -3px; - width: 3px; - height: 11px; - margin-left: -1.5px; - background: var(--text); - border-radius: 1px; -} -.sep-empty { - font-size: 12px; - color: var(--muted); -} -.cm-controls, -.bench-controls { - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; - margin: 4px 0 10px; -} -.bench-table td .sd { - color: var(--muted); - font-size: 11px; -} -.bench-table .bench-use { - padding: 2px 8px; - font-size: 11.5px; -} -#benchSection { - margin-top: 18px; -} -/* ---------- analysis, frames, exports ---------- */ -.frames { - margin-top: 10px; - padding-top: 8px; - border-top: 1px solid var(--line); -} -.frame-row { - display: flex; - gap: 6px; - align-items: center; - margin-bottom: 6px; -} -.frame-row button { - padding: 4px 9px; -} -.frame-row select { - width: auto; - flex: 1; -} -#frameBadge { - position: absolute; - top: 16px; - left: 50%; - transform: translateX(-50%); - z-index: 4; - padding: 4px 12px; - border-radius: 999px; - font-size: 13px; - font-weight: 600; - background: color-mix(in srgb, var(--panel) 92%, transparent); - border: 1px solid var(--line); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - pointer-events: none; -} -#frameBadge[hidden] { - display: none; -} -.export-grid { - display: grid !important; - grid-template-columns: 1fr 1fr; - gap: 6px; -} -.info-explore { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 4px; - font-size: 11px; - color: var(--muted); - margin: 0 0 10px; -} -.info-explore[hidden] { - display: none; -} -.info-explore button { - padding: 2px 7px; - font-size: 11px; -} -#btnSelNeighbours[hidden], -#btnSelPaths[hidden] { - display: none; -} -.cmp-arena { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 10px; - margin-top: 10px; - padding: 8px 10px; - border-radius: 8px; - border: 1px dashed var(--line); -} -.cmp-arena[hidden] { - display: none; -} -.cmp-arena-title { - font-weight: 600; - font-size: 12.5px; - display: inline-flex; - align-items: center; - gap: 6px; -} -.cmp-arena .status { - flex-basis: 100%; -} -.toast { - position: absolute; - left: 50%; - bottom: 56px; - transform: translateX(-50%); - z-index: 6; - max-width: 70%; - padding: 8px 14px; - border-radius: 8px; - font-size: 12.5px; - background: var(--panel); - border: 1px solid var(--line); - border-left: 4px solid var(--accent); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16); -} -.toast.warn { - border-left-color: #f59e0b; -} -.toast[hidden] { - display: none; -} -.ga-sub { - margin: 18px 0 2px; - font-size: 14px; -} -.ga-grid { - display: grid; - grid-template-columns: auto minmax(200px, 360px); - justify-content: start; - gap: 6px 12px; - align-items: center; - margin: 6px 0; - font-size: 12.5px; -} -.ga-grid .num-input { - width: 100px; -} -#gaSetsFromRow[hidden] { - display: none; -} -.nodedot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - vertical-align: -1px; -} -#gaSection, -#runtimeSection { - margin-top: 18px; -} - -/* ---------- STRING import ---------- */ -.string-query { - resize: vertical; - min-height: 44px; - font-family: var(--mono); - font-size: 12px; - width: 100%; -} -.string-more { - margin-top: 10px; - border-top: 1px solid color-mix(in srgb, var(--tint) 20%, var(--line)); - padding-top: 6px; -} -.string-more > summary { - cursor: pointer; - font-size: 12px; - font-weight: 600; - color: var(--text); - padding: 3px 0; -} -.string-checks { - display: flex; - flex-direction: column; - gap: 2px; - margin-top: 6px; - max-height: 230px; - overflow-y: auto; -} -.string-check { - display: flex; - align-items: center; - gap: 7px; - font-size: 12px; - cursor: pointer; - padding: 1px 0; -} -.string-check input { - margin: 0; - accent-color: var(--accent); -} -.row button .ico { - margin-right: 6px; - vertical-align: -2px; -} -.arena-more { - border-top-color: var(--line); -} -.string-check .swatch { - width: 14px; - height: 4px; - border-radius: 2px; - flex-shrink: 0; -} -/* ---------- image export ---------- */ -.export-btn { - display: flex; - align-items: center; - gap: 6px; - padding: 5px 10px; - font-size: 12px; -} -.export-btn svg { - width: 15px; - height: 15px; -} -@media (max-width: 1500px) { - .export-btn span { - display: none; - } - .export-btn { - padding: 5px 8px; - } - #viewHeader .vh-count { - display: none; - } -} -#exportDialog, -#deleteDialog { - border: 1px solid var(--line); - border-radius: 12px; - padding: 0; - background: var(--panel); - color: var(--text); - width: min(560px, calc(100vw - 32px)); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); -} -#exportDialog::backdrop, -#deleteDialog::backdrop { - background: rgba(15, 20, 32, 0.45); -} -.dlg { - padding: 18px 20px 16px; - margin: 0; -} -.dlg h2 { - margin: 0 0 2px; - font-size: 17px; -} -.dlg-sub { - margin: 0 0 12px; - font-size: 12.5px; - color: var(--muted); -} -.fmt-grid { - border: none; - padding: 0; - margin: 0 0 12px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 6px; -} -.fmt-grid legend { - font-size: 12px; - font-weight: 600; - margin-bottom: 6px; - padding: 0; -} -.fmt-grid label { - display: flex; - gap: 8px; - align-items: flex-start; - cursor: pointer; - border: 1px solid var(--line); - border-radius: 8px; - padding: 8px 10px; - background: var(--bg); -} -.fmt-grid label:has(input:checked) { - border-color: var(--accent); - background: color-mix(in srgb, var(--accent) 10%, var(--bg)); -} -.fmt-grid input { - margin-top: 3px; - accent-color: var(--accent); -} -.fmt-grid b { - display: block; - font-size: 13px; -} -.fmt-grid small { - display: block; - font-size: 11px; - color: var(--muted); - line-height: 1.35; -} -.dlg-grid { - display: grid; - grid-template-columns: 100px 1fr; - gap: 8px 12px; - align-items: center; - font-size: 12.5px; -} -.dlg-grid > label { - color: var(--muted); -} -.dlg-size { - margin: -2px 0 2px; - font-size: 11.5px; - color: var(--muted); - font-family: var(--mono); -} -.dlg-file { - display: flex; - align-items: center; - gap: 4px; -} -.dlg-file span { - font-family: var(--mono); - font-size: 12px; - color: var(--muted); -} -.dlg-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 12px; -} -.dlg-actions button { - min-width: 110px; -} -/* ---------- header: view switcher ---------- */ -#viewHeader { - grid-area: viewhead; - display: flex; - align-items: center; - gap: 10px; - padding: 0 18px 0 14px; - background: color-mix(in srgb, #6366f1 9%, var(--panel)); - border-bottom: 1px solid color-mix(in srgb, #6366f1 25%, var(--line)); - min-width: 0; -} -.vh-label { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.2px; - color: color-mix(in srgb, #6366f1 70%, var(--text)); - flex-shrink: 0; -} -.vh-label svg { - width: 16px; - height: 16px; -} -.vh-select { - display: flex; - gap: 6px; - align-items: center; - min-width: 0; -} -.vh-sub { - margin-left: 6px; - color: color-mix(in srgb, #f59e0b 70%, var(--text)); -} -#viewGrouping { - width: 270px; - min-width: 120px; - flex: 0 1 270px; - padding: 6px 10px; - font-weight: 600; - background: color-mix(in srgb, #f59e0b 8%, var(--bg)); - border-color: color-mix(in srgb, #f59e0b 40%, var(--line)); -} -#viewGrouping:disabled { - opacity: 0.75; - font-weight: 400; -} - -#viewSelect { - width: 230px; - min-width: 140px; - font-weight: 600; - padding: 6px 10px; - border-color: color-mix(in srgb, #6366f1 35%, var(--line)); -} -.vh-select { - flex: 0 1 auto; -} -#viewNameInput { - width: 240px; -} -.vh-count { - font-size: 11.5px; - color: var(--muted); - white-space: nowrap; -} -#viewHeader .view-actions { - display: flex; - gap: 4px; - margin-top: 0; -} -#viewHeader .view-actions button { - padding: 5px 9px; - font-size: 12px; -} -#viewHeader #topTools { - margin-left: auto; - margin-right: 0; - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} -#viewHeader { - overflow: hidden; -} -#viewHeader .view-actions { - flex-shrink: 0; -} -#brand { - border-bottom: 1px solid color-mix(in srgb, #6366f1 25%, var(--line)); -} - -/* ---------- sidebar tabs sit in the grid, aligned with the page tabs ---------- */ -#sideTabs { - grid-area: sidetabs; - display: flex; - margin: 0; - background: var(--panel); - border-right: 1px solid var(--line); - border-bottom: 1px solid var(--line); - padding: 0 10px; -} -#sideTabs { - padding: 0 4px; - gap: 2px; -} -#sideTabs button { - padding: 0 2px; - font-size: 11px; - line-height: 1.15; - white-space: normal; - text-align: center; - flex-direction: row; - gap: 3px; -} -#sideTabs button .ico { - margin-right: 2px; - width: 13px; - height: 13px; - flex-shrink: 0; -} -#sideTabs button[data-side='data'][aria-selected='true'] { - border-bottom-color: #2563eb; - color: color-mix(in srgb, #2563eb 60%, var(--text)); -} -#sideTabs button[data-side='db'][aria-selected='true'] { - border-bottom-color: #dc2626; - color: color-mix(in srgb, #dc2626 60%, var(--text)); -} -#sideTabs button[data-side='display'][aria-selected='true'] { - border-bottom-color: #16a34a; - color: color-mix(in srgb, #16a34a 55%, var(--text)); -} -#sideTabs button[data-side='export'][aria-selected='true'] { - border-bottom-color: #7c3aed; - color: color-mix(in srgb, #7c3aed 60%, var(--text)); -} -#sidebar { - padding-top: 14px; -} - -/* ---------- resizable / collapsible sidebar ---------- */ -#brand, -#sideTabs, -#sidebar { - min-width: 0; -} -#brand { - overflow: hidden; -} -#sideResizer { - position: absolute; - top: 0; - bottom: 0; - left: calc(var(--side-w) - 4px); - width: 8px; - cursor: col-resize; - z-index: 30; - touch-action: none; - outline: none; -} -#sideResizer::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 3px; - width: 2px; - background: transparent; - transition: background 0.15s; -} -#sideResizer:hover::after, -#sideResizer:focus-visible::after, -#app.side-resizing #sideResizer::after { - background: var(--accent); -} -#sideToggle { - position: absolute; - top: 50%; - left: calc(var(--side-w) - 1px); - z-index: 31; - transform: translateY(-50%); - width: 16px; - height: 44px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--line); - border-left: none; - border-radius: 0 6px 6px 0; - background: var(--panel); - color: var(--muted); -} -#sideToggle:hover { - color: var(--text); - border-color: var(--accent-dim); - background: var(--panel-2-hover); -} -#sideToggle:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 1px; -} -#sideToggle svg { - width: 10px; - height: 10px; - transition: transform 0.15s; -} -#app.side-collapsed #brand, -#app.side-collapsed #sideTabs, -#app.side-collapsed #sidebar { - display: none; -} -#app.side-collapsed #sideResizer { - display: none; -} -#app.side-collapsed #sideToggle { - left: 0; -} -#app.side-collapsed #sideToggle svg { - transform: rotate(180deg); -} -#app.side-resizing, -#app.side-resizing * { - cursor: col-resize !important; - user-select: none !important; -} - -/* ---------- what the current view shows ---------- */ -#topbar { - gap: 12px; -} -#ctxbar { - grid-area: ctxbar; - display: flex; - align-items: center; - gap: 14px; - padding: 0 18px; - background: color-mix(in srgb, var(--text) 2.5%, var(--panel)); - border-bottom: 1px solid var(--line); - min-width: 0; -} -#contextInfo { - flex: 1 1 0; - min-width: 0; - display: flex; - gap: 6px; - align-items: center; - overflow: hidden; -} -.ctx-chip { - --tint: #64748b; - display: inline-flex; - align-items: center; - gap: 5px; - min-width: 0; - max-width: 40%; - padding: 3px 9px; - border-radius: 999px; - font-size: 12px; - white-space: nowrap; - background: color-mix(in srgb, var(--tint) 12%, var(--panel)); - border: 1px solid color-mix(in srgb, var(--tint) 30%, var(--line)); -} -.ctx-chip b { - font-weight: 600; - color: color-mix(in srgb, var(--tint) 65%, var(--text)); - flex-shrink: 0; -} -.ctx-chip span { - overflow: hidden; - text-overflow: ellipsis; -} -.ctx-chip[data-tint='data'] { - --tint: #2563eb; -} -.ctx-chip[data-tint='groups'] { - --tint: #d97706; -} -.ctx-chip[data-tint='colors'] { - --tint: #db2777; -} -.ctx-empty { - font-size: 12px; - color: var(--muted); -} -#stats { - display: flex; - gap: 12px; - flex-shrink: 0; - align-items: center; -} -.stat-dir-off { - cursor: pointer; - opacity: 0.85; - border-style: dashed !important; -} -.stat-dir { - font-family: var(--sans); - font-size: 11px; - padding: 1px 7px; - border-radius: 999px; - border: 1px solid color-mix(in srgb, #06b6d4 45%, var(--line)); - background: color-mix(in srgb, #06b6d4 12%, var(--panel)); - color: color-mix(in srgb, #06b6d4 55%, var(--text)); -} -@media (max-width: 1380px) { - #stats { - gap: 8px; - font-size: 11px; - } - .vh-count { - display: none; - } -} - -/* ---------- tinted sidebar sections ---------- */ -.section { - --tint: #64748b; - background: color-mix(in srgb, var(--tint) 6%, var(--panel)); - border: 1px solid color-mix(in srgb, var(--tint) 20%, var(--line)); - border-left: 3px solid color-mix(in srgb, var(--tint) 70%, var(--panel)); - border-radius: 8px; - padding: 10px 12px 12px; - margin-bottom: 12px; -} -.section > h3 { - color: color-mix(in srgb, var(--tint) 60%, var(--text)); -} -/* Data tab */ -.section[data-tint='examples'] { - --tint: #059669; -} -.section[data-tint='string'] { - --tint: #dc2626; -} -.section[data-tint='files'] { - --tint: #2563eb; -} -.section[data-tint='saved'] { - --tint: #7c3aed; -} -/* Database importers tab */ -.section[data-tint='reactome'] { - --tint: #2563eb; -} -.section[data-tint='omnipath'] { - --tint: #0d9488; -} -.section[data-tint='ndex'] { - --tint: #ea580c; -} -.section[data-tint='intact'] { - --tint: #16a34a; -} -.section[data-tint='go'] { - --tint: #7c3aed; -} -/* Export tab */ -.section[data-tint='exfiles'] { - --tint: #2563eb; -} -.section[data-tint='eximage'] { - --tint: #db2777; -} -.section[data-tint='exarena'] { - --tint: #0d9488; -} -.section[data-tint='exother'] { - --tint: #ea580c; -} -.section[data-tint='exsave'] { - --tint: #475569; -} -.section.collapsible > h3 { - cursor: pointer; - user-select: none; -} -.section.collapsible > h3::after { - content: '▾'; - margin-left: 6px; - font-size: 11px; - color: var(--muted); - order: 3; -} -.section.collapsible.collapsed > h3::after { - content: '▸'; -} -.section.collapsible.collapsed > h3 { - margin-bottom: 0; -} -.section.collapsible > h3:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - border-radius: 4px; -} -.section.collapsible.collapsed > *:not(h3) { - display: none !important; -} -.section.collapsible > h3 .helplink { - order: 2; -} -.db-tag { - margin: -2px 0 8px; - font-size: 11px; - color: var(--muted); - line-height: 1.35; -} -.db-tag a { - color: inherit; -} -.db-pick { - width: 100%; - margin-top: 6px; - font-size: 12px; -} -.menu-sep { - display: flex; - align-items: center; - gap: 8px; - margin: 14px 0 8px; - font-size: 10.5px; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - color: color-mix(in srgb, var(--tint) 60%, var(--muted)); -} -.menu-sep::after { - content: ''; - flex: 1; - height: 1px; - background: color-mix(in srgb, var(--tint) 28%, var(--line)); -} -.menu-sep > span { - display: inline-flex; - align-items: baseline; - gap: 6px; - flex-wrap: wrap; -} -.menu-sep .lib-hint { - text-transform: none; - letter-spacing: 0; - font-weight: 400; - font-size: 11px; - color: var(--muted); -} -.section > h3 + .menu-sep, -.db-tag + .menu-sep, -.db-body > .menu-sep:first-child, -details > summary + .menu-sep { - margin-top: 4px; -} -.panel-intro { - margin: 0 2px 12px; - font-size: 12px; - color: var(--muted); - line-height: 1.45; -} -/* Display tab, in order; neighbours always differ clearly */ -.section[data-tint='groups'] { - --tint: #d97706; -} -.section[data-tint='hulls'] { - --tint: #a21caf; -} -.section[data-tint='layout3d'] { - --tint: #0d9488; -} -.section[data-tint='layout'] { - --tint: #16a34a; -} -.section[data-tint='colors'] { - --tint: #db2777; -} -.section[data-tint='nodes'] { - --tint: #0284c7; -} -.section[data-tint='labels'] { - --tint: #ea580c; -} -.section[data-tint='edges'] { - --tint: #475569; -} -.section[data-tint='channels'] { - --tint: #65a30d; -} -.section[data-tint='legend'] { - --tint: #be123c; -} -.section[data-tint='attrs'] { - --tint: #0891b2; -} -.section[data-tint='perf'] { - --tint: #92400e; -} -.section .upload-box, -.section .lib-list, -.section .strategy-fig { - background: var(--panel); -} -::selection { - background: #fde68a; - color: #111827; -} diff --git a/frontend/src/themes.ts b/frontend/src/themes.ts deleted file mode 100644 index 5770be1..0000000 --- a/frontend/src/themes.ts +++ /dev/null @@ -1,142 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { SELECTION_YELLOW, setStyle } from './cy' -import { applyEdgeMerge } from './parallel_edges' -import { updateLabelStyle } from './profiler' - -/* ---------- UI themes (chrome + canvas; independent of node/edge data palettes) ---------- */ -const THEMES = { - dark: { - bg: '#0f1420', - panel: '#161d2e', - panel2: '#1d2740', - panel2Hover: '#223050', - line: '#2a3554', - text: '#e7ebf5', - muted: '#8993ad', - accent: '#5fd3c4', - accentDim: '#3a7d74', - accentHover: '#7fe0d3', - accentText: '#08221e', - warn: '#e7a15f', - canvasDot: '#1c2540', - nodeBorder: 'rgba(255,255,255,0.35)', - }, - midnight: { - bg: '#050608', - panel: '#0c0e12', - panel2: '#131620', - panel2Hover: '#1a1e2b', - line: '#1f2430', - text: '#dfe3ee', - muted: '#767c8c', - accent: '#7c5cff', - accentDim: '#5540b3', - accentHover: '#9b82ff', - accentText: '#0a0612', - warn: '#e7a15f', - canvasDot: '#111420', - nodeBorder: 'rgba(255,255,255,0.3)', - }, - light: { - bg: '#f4f5f8', - panel: '#ffffff', - panel2: '#eef1f6', - panel2Hover: '#e2e7ef', - line: '#d7dce6', - text: '#1b2333', - muted: '#6b7387', - accent: '#2a9d8f', - accentDim: '#1f7a6f', - accentHover: '#39b7a7', - accentText: '#ffffff', - warn: '#c76b1f', - canvasDot: '#e4e8f0', - nodeBorder: 'rgba(20,25,40,0.25)', - }, - white: { - bg: '#ffffff', - panel: '#ffffff', - panel2: '#f5f5f7', - panel2Hover: '#e9e9ec', - line: '#e0e0e5', - text: '#111114', - muted: '#6e6e76', - accent: '#0f766e', - accentDim: '#0b5c56', - accentHover: '#14958a', - accentText: '#ffffff', - warn: '#b45309', - canvasDot: '#eeeeee', - nodeBorder: 'rgba(0,0,0,0.2)', - }, - gray: { - bg: '#2b2e33', - panel: '#34383f', - panel2: '#3d424a', - panel2Hover: '#464b54', - line: '#4a4f58', - text: '#e7e8ea', - muted: '#9a9fa8', - accent: '#8fb8e8', - accentDim: '#5f83ad', - accentHover: '#a9cdf5', - accentText: '#12161c', - warn: '#e0a458', - canvasDot: '#3a3f47', - nodeBorder: 'rgba(255,255,255,0.2)', - }, - paper: { - bg: '#f3ecdd', - panel: '#fbf6ea', - panel2: '#efe6d2', - panel2Hover: '#e6dac0', - line: '#ddd0b0', - text: '#3a2f1f', - muted: '#8a7a5c', - accent: '#b3541e', - accentDim: '#8a4017', - accentHover: '#d06a2c', - accentText: '#fff7ec', - warn: '#7a5a1e', - canvasDot: '#e9ddc2', - nodeBorder: 'rgba(58,47,31,0.3)', - }, -} - -export let currentTheme = THEMES.dark - -export function applyTheme(name) { - const t = THEMES[name] || THEMES.dark - currentTheme = t - - const root = document.documentElement.style - root.setProperty('--bg', t.bg) - root.setProperty('--panel', t.panel) - root.setProperty('--panel-2', t.panel2) - root.setProperty('--panel-2-hover', t.panel2Hover) - root.setProperty('--line', t.line) - root.setProperty('--text', t.text) - root.setProperty('--muted', t.muted) - root.setProperty('--accent', t.accent) - root.setProperty('--accent-dim', t.accentDim) - root.setProperty('--accent-hover', t.accentHover) - root.setProperty('--accent-text', t.accentText) - root.setProperty('--warn', t.warn) - root.setProperty('--canvas-dot', t.canvasDot) - - // cytoscape doesn't read CSS variables, so the canvas-drawn parts - // (node label color/halo, node border, highlight color) are set directly - setStyle('node', { color: t.text, 'text-outline-color': t.bg, 'border-color': t.nodeBorder }) - setStyle('node.highlighted', { 'border-color': t.accent }) - // selected nodes and edges are marked in yellow on every theme - setStyle('node:selected', { - 'border-color': SELECTION_YELLOW, - 'underlay-color': SELECTION_YELLOW, - }) - setStyle('edge:selected', { 'underlay-color': SELECTION_YELLOW }) - setStyle('core', { 'selection-box-color': t.accent, 'selection-box-border-color': t.accent }) - - updateLabelStyle() // edge label color/halo also depend on the theme - applyEdgeMerge() // merged edges use the theme's muted color -} diff --git a/frontend/src/uploads.ts b/frontend/src/uploads.ts deleted file mode 100644 index cf762ce..0000000 --- a/frontend/src/uploads.ts +++ /dev/null @@ -1,192 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { - NORMA_KIND_NAMES, - addNormaEntry, - annotationText, - articleFor, - detectNormaKind, - downloadText, - fileStem, - libSelection, - listSample, - normaLibrary, - plural, - renderLibraryLists, - selectedNetworks, - setStatus, -} from './layouts/controls' -import { convertArena3dNetwork, isArena3dNetworkText } from './arena3d' - -/* ---------- uploads ---------- */ -export function readFileText(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = (ev) => resolve(ev.target.result) - reader.onerror = () => - reject(new Error('The file could not be read. Check that it is a plain text file.')) - reader.readAsText(file) - }) -} - -// Splits an annotation's members into those present in `known` and those not. -function checkAnnotationAgainst(entry, known) { - const missing = new Set() - const emptied = [] - const kept = [] - entry.parsed.groups.forEach(({ name, members }) => { - const present = members.filter((m) => known.has(m)) - members.forEach((m) => { - if (!known.has(m)) missing.add(m) - }) - if (present.length) kept.push({ name, members: present }) - else emptied.push(name) - }) - return { missing: [...missing], emptied, kept } -} - -export async function handleNormaUploads(fileList) { - const files = [...fileList] - if (!files.length) return - const chosenKind = document.getElementById('normaKind').value - const nameInput = document.getElementById('normaName') - const typedName = files.length === 1 ? nameInput.value.trim() : '' - const notes = [] - const added = [] - const addedNotes = new Map() - for (const file of files) { - try { - const text = await readFileText(file) - if ((chosenKind === 'auto' || chosenKind === 'network') && isArena3dNetworkText(text)) { - // an Arena3D network file: a network plus its layers as groups - const conv = convertArena3dNetwork(text) - const base = typedName || file.name.replace(/\.[^.]+$/, '') || file.name - const netEntry = addNormaEntry('network', base, conv.network, file.name, null, { - directed: document.getElementById('normaDirected').value === 'directed', - }) - const annEntry = addNormaEntry( - 'annotation', - `${base} layers`, - conv.annotation, - file.name, - null, - {} - ) - ;[netEntry, annEntry].forEach((entry) => { - added.push(entry) - const note = { - level: 'ok', - text: `Added ${articleFor(NORMA_KIND_NAMES[entry.kind])} ${NORMA_KIND_NAMES[entry.kind]} "${entry.name}" from an Arena3D network file: ${entry.parsed.summary}.`, - } - addedNotes.set(entry, note) - notes.push(note) - entry.parsed.notes.forEach((t) => - notes.push({ level: 'warn', text: `${entry.name}: ${t}` }) - ) - }) - continue - } - const kind = chosenKind === 'auto' ? detectNormaKind(text) : chosenKind - if (!kind) throw new Error('The file is empty.') - const name = typedName || file.name.replace(/\.[^.]+$/, '') || file.name - const entry = addNormaEntry(kind, name, text, file.name, null, { - directed: document.getElementById('normaDirected').value === 'directed', - }) - added.push(entry) - const addedNote = { - level: 'ok', - text: `Added ${articleFor(NORMA_KIND_NAMES[kind])} ${NORMA_KIND_NAMES[kind]} "${entry.name}": ${entry.parsed.summary}.`, - } - addedNotes.set(entry, addedNote) - notes.push(addedNote) - entry.parsed.notes.forEach((t) => notes.push({ level: 'warn', text: `${entry.name}: ${t}` })) - } catch (err) { - const kindText = - chosenKind === 'auto' - ? 'a file' - : `${articleFor(NORMA_KIND_NAMES[chosenKind])} ${NORMA_KIND_NAMES[chosenKind]}` - notes.push({ - level: 'error', - text: `"${file.name}" wasn't added as ${kindText}. ${err.message}`, - }) - } - } - // Check new annotations against the networks they will most likely be - // used with: networks uploaded together with them, otherwise the ticked - // networks, otherwise every network in the list. Names missing from all - // of those are discarded now, with a download of the corrected file. - const newAnnotations = added.filter((e) => e.kind === 'annotation') - if (newAnnotations.length) { - const batchNets = added.filter((e) => e.kind === 'network') - const tickedNets = selectedNetworks() - const refNets = batchNets.length - ? batchNets - : tickedNets.length - ? tickedNets - : normaLibrary.network - const refNames = - refNets.length === 1 - ? `"${refNets[0].name}"` - : `the ${refNets.length} ${batchNets.length ? 'uploaded' : tickedNets.length ? 'ticked' : 'listed'} networks` - if (!refNets.length) { - notes.push({ - level: 'ok', - text: 'No network is loaded yet, so annotation names will be checked when you show them with a network.', - }) - } - const known = new Set(refNets.flatMap((e) => e.parsed.nodes)) - newAnnotations.forEach((entry) => { - if (!refNets.length) return - const check = checkAnnotationAgainst(entry, known) - if (!check.missing.length) return - if (!check.kept.length) { - normaLibrary.annotation = normaLibrary.annotation.filter((x) => x !== entry) - added.splice(added.indexOf(entry), 1) - notes.splice(notes.indexOf(addedNotes.get(entry)), 1) - notes.push({ - level: 'error', - text: `"${entry.name}" was not kept: none of its ${plural(check.missing.length, 'node name')} are in ${refNames}. Check that the annotation belongs to this network.`, - }) - return - } - entry.parsed.groups = check.kept - entry.parsed.summary = plural(check.kept.length, 'group') - addedNotes.get(entry).text = - `Added the annotation "${entry.name}": ${entry.parsed.summary} after the check below.` - entry.correctedText = annotationText(check.kept) - const parts = [ - `Discarded ${plural(check.missing.length, 'node name')} from "${entry.name}" that ${check.missing.length === 1 ? 'is' : 'are'} not in ${refNames}: ${listSample(check.missing)}.`, - ] - if (check.emptied.length) - parts.push( - `${plural(check.emptied.length, 'group')} left with no nodes ${check.emptied.length === 1 ? 'was' : 'were'} removed: ${listSample(check.emptied)}.` - ) - const fileName = `${fileStem(entry.name)}-corrected.txt` - notes.push({ - level: 'warn', - text: parts.join(' '), - action: { - label: 'Download corrected annotation', - run: () => downloadText(fileName, entry.correctedText), - }, - }) - }) - } - if (added.length) { - nameInput.value = '' - // Select what was just added: new networks replace the network - // selection; the last new annotation / expression becomes the choice. - const newNets = added.filter((e) => e.kind === 'network') - if (newNets.length) libSelection.networks = new Set(newNets.map((e) => e.id)) - const lastAnn = added.filter((e) => e.kind === 'annotation').pop() - if (lastAnn) libSelection.annotation = lastAnn.id - const lastCol = added.filter((e) => e.kind === 'colors').pop() - if (lastCol) libSelection.colors = lastCol.id - notes.push({ - level: 'ok', - text: 'The new files are ticked. Choose Show in this view or Open in new view to see them.', - }) - } - renderLibraryLists() - setStatus('normaStatus', notes) -} diff --git a/frontend/src/view3d/cache.ts b/frontend/src/view3d/cache.ts deleted file mode 100644 index f89b367..0000000 --- a/frontend/src/view3d/cache.ts +++ /dev/null @@ -1,102 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { PIE_MAX_SLICES, effectiveGroupsFor } from '../network_state' -import { cy } from '../cy' -import { net3d } from './state' - -/* ---------- element cache: everything read from the 2D elements ---------- */ -export function buildCache3d() { - const nodes = [] - const byId = new Map() - cy.nodes().forEach((n) => { - if (!n.visible()) return - const pies = [] - if (n.style('shape') === 'ellipse') { - for (let i = 1; i <= PIE_MAX_SLICES; i++) { - const size = parseFloat(n.data('pieSize' + i)) || 0 - if (size > 0) pies.push([n.data('pieColor' + i), size / 100]) - } - } - const rec = { - id: n.id(), - el: n, - size: n.width(), - color: n.style('background-color'), - pies, - shape: n.style('shape'), - border: n.style('border-color'), - borderW: n.pstyle('border-width').pfValue, - opacity: n.effectiveOpacity(), - underlay: - n.pstyle('underlay-opacity').value > 0 - ? { - color: n.style('underlay-color'), - alpha: n.pstyle('underlay-opacity').value, - pad: n.pstyle('underlay-padding').pfValue, - } - : null, - label: n.style('label'), - labelColor: n.style('color'), - halo: n.style('text-outline-color'), - haloW: n.pstyle('text-outline-width').pfValue, - fontPx: n.pstyle('font-size').pfValue, - valign: n.style('text-valign'), - halign: n.style('text-halign'), - marginX: n.pstyle('text-margin-x').pfValue || 0, - marginY: n.pstyle('text-margin-y').pfValue || 0, - groups: effectiveGroupsFor(n), - highlighted: n.hasClass('highlighted'), - } - nodes.push(rec) - byId.set(rec.id, rec) - }) - const edges = [] - const pairCount = new Map() - cy.edges().forEach((e) => { - if (!e.visible()) return - const s = e.data('source'), - t = e.data('target') - if (!byId.has(s) || !byId.has(t) || s === t) return - const pair = s < t ? s + '\t' + t : t + '\t' + s - const k = pairCount.get(pair) || 0 - pairCount.set(pair, k + 1) - const arrow = e.style('target-arrow-shape') - edges.push({ - id: e.id(), - el: e, - s, - t, - pair, - index: k, - color: e.style('line-color'), - width: e.pstyle('width').pfValue, - opacity: e.effectiveOpacity(), - arrow: arrow && arrow !== 'none' ? arrow : null, - arrowColor: e.style('target-arrow-color'), - arrowScale: e.pstyle('arrow-scale').value || 1, - label: e.style('label'), - labelColor: e.style('color'), - labelBg: e.style('text-background-color'), - fontPx: e.pstyle('font-size').pfValue, - rotate: e.style('text-rotation') === 'autorotate', - underlay: e.pstyle('underlay-opacity').value > 0 ? e.style('underlay-color') : null, - }) - }) - edges.forEach((e) => { - e.siblings = pairCount.get(e.pair) - }) - net3d.cache = { nodes, edges, byId } - net3d.cacheDirty = false -} - -/* ---------- positions ---------- */ -export function pos3dOf(id) { - let p = net3d.pos.get(id) - if (!p) { - const n = cy.getElementById(id) - const q = n.length ? n.position() : { x: 0, y: 0 } - p = [q.x, q.y, 0] - net3d.pos.set(id, p) - } - return p -} diff --git a/frontend/src/view3d/camera.ts b/frontend/src/view3d/camera.ts deleted file mode 100644 index b948474..0000000 --- a/frontend/src/view3d/camera.ts +++ /dev/null @@ -1,182 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { buildCache3d, pos3dOf } from './cache' -import { canvas3d, el3, net3d, requestRender3d } from './state' -import { cy } from '../cy' -import { formatZoom } from '../profiler' -import { render3d } from './draw' - -/* ---------- camera and projection ---------- */ -function perspectiveMode() { - return el3('persp3d').value -} - -export function focalLength(H) { - const fov = perspectiveMode() === 'strong' ? 65 : 30 - return H / 2 / Math.tan((fov * Math.PI) / 360) -} - -// Returns a projector for a given viewport size and camera. -export function makeProjector(W, H, cam) { - const cyaw = Math.cos(cam.yaw), - syaw = Math.sin(cam.yaw) - const cp = Math.cos(cam.pitch), - sp = Math.sin(cam.pitch) - const f = focalLength(H) - const ortho = perspectiveMode() === 'none' - const orthoScale = f / cam.dist - const near = cam.dist * 0.02 - return (p) => { - const qx = p[0] - cam.tx, - qy = p[1] - cam.ty, - qz = p[2] - cam.tz - const x1 = cyaw * qx - syaw * qz - const z1 = syaw * qx + cyaw * qz - const y2 = cp * qy - sp * z1 - const z2 = sp * qy + cp * z1 - const depth = cam.dist + z2 - if (!ortho && depth < near) return null - const scale = ortho ? orthoScale : f / depth - return { x: W / 2 + x1 * scale, y: H / 2 + y2 * scale, depth, scale } - } -} - -// Inverse of the rotation: a screen-plane vector (dx, dy) to world units at a given scale. -export function screenToWorldDelta(dx, dy, scale, cam) { - const cyaw = Math.cos(cam.yaw), - syaw = Math.sin(cam.yaw) - const cp = Math.cos(cam.pitch), - sp = Math.sin(cam.pitch) - const x1 = dx / scale, - y2 = dy / scale - // camera-space vector (x1, y2, 0) back to world space - const qy = cp * y2 - const z1 = -sp * y2 - const qx = cyaw * x1 + syaw * z1 - const qz = -syaw * x1 + cyaw * z1 - return [qx, qy, qz] -} - -/* ---------- camera helpers ---------- */ -export function shownIds3d(eles) { - if (eles && eles.length) - return eles.filter((e) => (e.isNode ? e.isNode() : true)).map((n) => n.id()) - if (net3d.cacheDirty || !net3d.cache) buildCache3d() - return net3d.cache.nodes.map((n) => n.id) -} - -export function fit3d(eles, animate = true) { - const ids = shownIds3d( - eles && eles.nodes - ? eles.nodes().union(eles.connectedNodes ? eles.connectedNodes() : cy.collection()) - : eles - ) - if (!ids.length) return - const pts = ids.map((id) => ({ p: pos3dOf(id), r: (cy.getElementById(id).width() || 40) / 2 })) - // aim at the middle of the bounding box - const lo = [Infinity, Infinity, Infinity], - hi = [-Infinity, -Infinity, -Infinity] - pts.forEach(({ p }) => { - for (let d = 0; d < 3; d++) { - lo[d] = Math.min(lo[d], p[d]) - hi[d] = Math.max(hi[d], p[d]) - } - }) - const cam = { - ...net3d.cam, - tx: (lo[0] + hi[0]) / 2, - ty: (lo[1] + hi[1]) / 2, - tz: (lo[2] + hi[2]) / 2, - } - const cv = canvas3d() - const W = cv.clientWidth || 800, - H = cv.clientHeight || 600 - const margin = 40 - cam.dist = Math.max(60, Math.hypot(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) * 1.2) - // refine: project, measure how far the picture reaches, rescale the distance - for (let it = 0; it < 6; it++) { - const project = makeProjector(W, H, cam) - let need = 0 - for (const { p, r } of pts) { - const q = project(p) - if (!q) { - need = Infinity - break - } - const ex = Math.abs(q.x - W / 2) + r * q.scale, - ey = Math.abs(q.y - H / 2) + r * q.scale - need = Math.max(need, ex / (W / 2 - margin), ey / (H / 2 - margin)) - } - if (!Number.isFinite(need)) { - cam.dist *= 2 - continue - } - if (Math.abs(need - 1) < 0.02) break - cam.dist *= perspectiveMode() === 'none' ? need : Math.max(0.3, need) - } - moveCamera3d({ tx: cam.tx, ty: cam.ty, tz: cam.tz, dist: cam.dist }, animate) -} - -export function moveCamera3d(target, animate = true) { - const from = { ...net3d.cam } - const to = { ...from, ...target } - if (!animate) { - Object.assign(net3d.cam, to) - requestRender3d() - return - } - // turn the short way round (both angles are unlimited) - ;['yaw', 'pitch'].forEach((k) => { - if (target[k] === undefined) return - to[k] = from[k] + wrapAngle(to[k] - from[k]) - }) - const start = performance.now() - const step = (now) => { - const t = Math.min(1, (now - start) / 450) - const e = 1 - Math.pow(1 - t, 3) - Object.keys(to).forEach((key) => { - net3d.cam[key] = from[key] + (to[key] - from[key]) * e - }) - if (t >= 1) { - net3d.cam.yaw = wrapAngle(net3d.cam.yaw) - net3d.cam.pitch = wrapAngle(net3d.cam.pitch) - } - render3d() - if (t < 1) requestAnimationFrame(step) - } - requestAnimationFrame(step) -} - -export function zoom3d(factor) { - net3d.cam.dist = Math.min(1e7, Math.max(5, net3d.cam.dist / factor)) - requestRender3d() -} - -// keeps an angle in (-pi, pi] -export function wrapAngle(a) { - a = a % (Math.PI * 2) - if (a > Math.PI) a -= Math.PI * 2 - if (a <= -Math.PI) a += Math.PI * 2 - return a -} - -export function rotate3d(dyaw, dpitch) { - // turning and tilting have no limits: the view can go over the poles - // as often as wanted - net3d.cam.yaw = wrapAngle(net3d.cam.yaw + dyaw) - net3d.cam.pitch = wrapAngle(net3d.cam.pitch + dpitch) - requestRender3d() -} - -export const VIEW_PRESETS_3D = { - front: { yaw: 0, pitch: 0 }, - top: { yaw: 0, pitch: Math.PI / 2 - 0.02 }, - side: { yaw: Math.PI / 2, pitch: 0 }, - tilted: { yaw: 0.6, pitch: 0.45 }, -} - -export function updateZoomReadout3d() { - const H = canvas3d().clientHeight || 600 - const scale = focalLength(H) / net3d.cam.dist - document.getElementById('zoomLevel').textContent = formatZoom(scale) -} diff --git a/frontend/src/view3d/draw.ts b/frontend/src/view3d/draw.ts deleted file mode 100644 index aec2d6d..0000000 --- a/frontend/src/view3d/draw.ts +++ /dev/null @@ -1,395 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { UNGROUPED, getUsedGroups } from '../network_state' -import { blobPath, capsulePath } from '../export/shading' -import { bubbleContour, bubblePathD } from '../clustering/wiring' -import { buildCache3d, pos3dOf } from './cache' -import { canvas3d, el3, mixColor, net3d } from './state' -import { canvasPainter } from './painters' -import { convexHull, inflateHull } from '../hulls' -import { currentTheme } from '../themes' -import { cy } from '../cy' -import { makeProjector, updateZoomReadout3d } from './camera' - -/* ---------- drawing ---------- */ -function nodeShapePoints(shape, x, y, r) { - const def = cy.renderer().nodeShapes[shape] - if (!def || !def.points) return null - const pts = [] - for (let i = 0; i < def.points.length; i += 2) - pts.push([x + def.points[i] * r, y + def.points[i + 1] * r]) - return pts -} - -export function drawScene3d(painter, W, H, opts) { - if (net3d.cacheDirty || !net3d.cache) buildCache3d() - const { nodes, edges, byId } = net3d.cache - const cam = opts.cam || net3d.cam - const project = makeProjector(W, H, cam) - const fast = !!opts.fast - const bg = opts.bg - painter.clear(W, H, bg) - const fogColor = opts.fogColor || currentTheme.bg - const fogAmount = parseFloat(el3('fog3d').value) || 0 - const shaded = el3('style3d').value === 'shaded' - const k = opts.pixelScale || 1 // extra scaling for high-resolution export - - const T = { t: performance.now() } - const mark = (k) => { - const now = performance.now() - T[k] = Math.round(now - T.t) - T.t = now - } - // project nodes - const P = new Map() - let dMin = Infinity, - dMax = -Infinity - nodes.forEach((nd) => { - const pr = project(pos3dOf(nd.id)) - if (!pr) return - pr.x *= k - pr.y *= k - pr.scale *= k - P.set(nd.id, pr) - if (pr.depth < dMin) dMin = pr.depth - if (pr.depth > dMax) dMax = pr.depth - }) - const span = Math.max(1e-6, dMax - dMin) - const fogOf = (depth) => (fogAmount * 0.75 * (depth - dMin)) / span - - mark('project') - const floorPx = labelFloor3d() * k - const nodeLabelMin = labelCutoff3d( - nodes.filter((nd) => nd.label && P.has(nd.id)).map((nd) => nd.fontPx * P.get(nd.id).scale), - floorPx, - 500 - ) - const edgeLabelMin = labelCutoff3d( - edges - .filter((e) => e.label && P.has(e.s) && P.has(e.t)) - .map((e) => (e.fontPx * (P.get(e.s).scale + P.get(e.t).scale)) / 2), - floorPx, - 300 - ) - // floor grid and axes - if (el3('grid3d').checked && nodes.length) drawFloor3d(painter, project, k) - - // group shading, from the projected positions - if (document.getElementById('showGroupHulls').checked) { - const style = document.getElementById('hullStyle').value - const opacity = parseFloat(document.getElementById('hullOpacity').value) || 0.25 - const groups = getUsedGroups().filter((g) => S.activeGroups.has(g) && g !== UNGROUPED) - const bubbles = style === 'bubble' && !fast - const projected = bubbles - ? nodes - .filter((nd) => P.has(nd.id)) - .map((nd) => { - const pr = P.get(nd.id) - return { x: pr.x, y: pr.y, r: (nd.size / 2) * pr.scale, groups: nd.groups } - }) - : null - groups.forEach((g) => { - const pts = [] - nodes.forEach((nd) => { - if (!nd.groups.includes(g)) return - const pr = P.get(nd.id) - if (pr) pts.push({ x: pr.x, y: pr.y, r: (nd.size / 2) * pr.scale }) - }) - if (!pts.length) return - const color = S.nodeColorMap[g] || '#888888' - if (bubbles) { - const loops = bubbleContour( - pts, - projected.filter((p) => !p.groups.includes(g)), - { margin: 30 * k, edgeRadius: 21 * k, avoid: 12 * k, cell: 3 * k, maxCells: 120000 } - ) - const d = bubblePathD(loops) - if (d) - painter.pathD(d, { - fill: color, - fillAlpha: opacity, - stroke: color, - strokeAlpha: Math.min(1, opacity + 0.45), - width: 1.6 * k, - evenodd: true, - }) - return - } - const pad = 18 * k - let d - if (pts.length === 1) - d = `M${pts[0].x - pts[0].r - pad},${pts[0].y}a${pts[0].r + pad},${pts[0].r + pad} 0 1 0 ${2 * (pts[0].r + pad)},0a${pts[0].r + pad},${pts[0].r + pad} 0 1 0 ${-2 * (pts[0].r + pad)},0Z` - else if (pts.length === 2) d = capsulePath(pts[0], pts[1], Math.max(pts[0].r, pts[1].r) + pad) - else d = blobPath(inflateHull(convexHull(pts), pad + Math.max(...pts.map((p) => p.r)))) - painter.pathD( - d, - style === 'fog' - ? { fill: color, fillAlpha: opacity, blur: 14 * k } - : { - fill: color, - fillAlpha: opacity, - stroke: color, - strokeAlpha: Math.min(1, opacity + 0.35), - width: 1.5 * k, - } - ) - }) - } - - mark('hulls') - // edges, far to near - const curveStyle = document.getElementById('edgeCurveStyle').value - const fan = curveStyle === 'bezier' || curveStyle === 'bundled' - const fanStep = (parseInt(document.getElementById('edgeCurvature').value, 10) || 40) * 0.6 * k - const edgeList = [] - edges.forEach((e) => { - const a = P.get(e.s), - b = P.get(e.t) - if (!a || !b) return - edgeList.push({ e, a, b, depth: (a.depth + b.depth) / 2 }) - }) - edgeList.sort((p, q) => q.depth - p.depth) - const projEdges = [] - const labelsLater = [] - edgeList.forEach(({ e, a, b, depth }) => { - const fog = fogOf(depth) - const color = mixColor(e.color, fogColor, fog) - const avgScale = (a.scale + b.scale) / 2 - const width = Math.max(0.6 * k, Math.min(e.width * avgScale, 12 * k)) - let cx, cy0 - if (fan && e.siblings > 1) { - const off = (e.index - (e.siblings - 1) / 2) * fanStep * Math.min(1.5, avgScale) - // perpendicular in a direction that doesn't depend on edge orientation - const [p, q] = e.s < e.t ? [a, b] : [b, a] - const dx = q.x - p.x, - dy = q.y - p.y, - len = Math.hypot(dx, dy) || 1 - cx = (a.x + b.x) / 2 - (dy / len) * off - cy0 = (a.y + b.y) / 2 + (dx / len) * off - } - // shorten the line at the target so an arrow sits on the node's rim - let x2 = b.x, - y2 = b.y - const rb = (byId.get(e.t).size / 2) * b.scale - if (e.arrow) { - const fx = cx !== undefined ? cx : a.x, - fy = cx !== undefined ? cy0 : a.y - const dx = b.x - fx, - dy = b.y - fy, - len = Math.hypot(dx, dy) || 1 - const ux = dx / len, - uy = dy / len - const tipX = b.x - ux * rb, - tipY = b.y - uy * rb - const size = Math.max(Math.pow(e.width * 13.37, 0.9), 29) * e.arrowScale * avgScale - x2 = tipX - ux * size * 0.25 - y2 = tipY - uy * size * 0.25 - if (e.underlay) painter.line(a.x, a.y, x2, y2, e.underlay, width + 6 * k, 0.4, cx, cy0) - painter.line(a.x, a.y, x2, y2, color, width, e.opacity, cx, cy0) - const def = cy.renderer().arrowShapes[e.arrow] - if (def && def.points && !fast) { - const pts = [] - for (let i = 0; i < def.points.length; i += 2) { - const px = def.points[i] * size, - py = def.points[i + 1] * size - pts.push([tipX - uy * px + ux * py, tipY + ux * px + uy * py]) - } - painter.poly(pts, mixColor(e.arrowColor, fogColor, fog), e.opacity) - } - } else { - if (e.underlay) painter.line(a.x, a.y, x2, y2, e.underlay, width + 6 * k, 0.4, cx, cy0) - painter.line(a.x, a.y, x2, y2, color, width, e.opacity, cx, cy0) - } - const mx = cx !== undefined ? 0.25 * a.x + 0.5 * cx + 0.25 * b.x : (a.x + b.x) / 2 - const my = cx !== undefined ? 0.25 * a.y + 0.5 * cy0 + 0.25 * b.y : (a.y + b.y) / 2 - projEdges.push({ id: e.id, ax: a.x, ay: a.y, bx: b.x, by: b.y, cx, cy: cy0, mx, my, width }) - if (e.label && !fast) { - const size = e.fontPx * avgScale - if (size >= edgeLabelMin) { - let angle = 0 - if (e.rotate) { - angle = Math.atan2(b.y - a.y, b.x - a.x) - if (angle > Math.PI / 2 || angle < -Math.PI / 2) angle += Math.PI - } - labelsLater.push({ - str: e.label, - x: mx, - y: my, - o: { - size, - color: e.labelColor, - alpha: e.opacity, - align: 'center', - baseline: 'middle', - rotate: angle, - bg: e.labelBg, - }, - }) - } - } - }) - - mark('edges') - // nodes, far to near - const order = nodes - .filter((nd) => P.has(nd.id)) - .sort((p, q) => P.get(q.id).depth - P.get(p.id).depth) - const proj = [] - order.forEach((nd) => { - const pr = P.get(nd.id) - const r = Math.max(1.2 * k, (nd.size / 2) * pr.scale) - const fog = fogOf(pr.depth) - const alpha = nd.opacity - if (nd.underlay) - painter.circle( - pr.x, - pr.y, - r + nd.underlay.pad * pr.scale, - nd.underlay.color, - nd.underlay.alpha * alpha - ) - const bw = nd.borderW > 0 ? Math.max(0.5 * k, Math.min(nd.borderW * pr.scale, 8 * k)) : 0 - const border = mixColor(nd.border, fogColor, fog) - if (nd.shape === 'ellipse' || fast || r < 2.5 * k) { - if (nd.pies.length > 1 && !fast && r > 3 * k) { - painter.circle(pr.x, pr.y, r, mixColor(nd.color, fogColor, fog), alpha) - let a0 = -Math.PI / 2 - nd.pies.forEach(([c, share]) => { - const a1 = a0 + share * Math.PI * 2 - painter.wedge(pr.x, pr.y, r, a0, a1, mixColor(c, fogColor, fog), alpha) - a0 = a1 - }) - if (bw) painter.circle(pr.x, pr.y, r, null, alpha, border, bw) - } else { - const fill = nd.pies.length === 1 ? nd.pies[0][0] : nd.color - painter.circle(pr.x, pr.y, r, mixColor(fill, fogColor, fog), alpha, bw ? border : null, bw) - } - if (shaded && !fast && r > 3 * k) painter.shade(pr.x, pr.y, r, alpha) - } else { - const pts = - nodeShapePoints(nd.shape, pr.x, pr.y, r) || nodeShapePoints('rectangle', pr.x, pr.y, r) - painter.poly(pts, mixColor(nd.color, fogColor, fog), alpha, bw ? border : null, bw) - } - proj.push({ id: nd.id, x: pr.x, y: pr.y, r, depth: pr.depth, scale: pr.scale }) - if (nd.label && !fast) { - const size = nd.fontPx * pr.scale - if (size >= nodeLabelMin) { - let x = pr.x + nd.marginX * pr.scale, - y = pr.y + nd.marginY * pr.scale - let align = 'center', - baseline = 'middle' - if (nd.valign === 'top') { - y = pr.y - r + nd.marginY * pr.scale - baseline = 'bottom' - } else if (nd.valign === 'bottom') { - y = pr.y + r + nd.marginY * pr.scale - baseline = 'top' - } - if (nd.halign === 'left') { - x = pr.x - r + nd.marginX * pr.scale - align = 'right' - } else if (nd.halign === 'right') { - x = pr.x + r + nd.marginX * pr.scale - align = 'left' - } - painter.text(nd.label, x, y, { - size, - color: mixColor(nd.labelColor, fogColor, fog * 0.6), - alpha, - align, - baseline, - halo: nd.halo, - haloWidth: nd.haloW * Math.min(1.5, pr.scale), - }) - } - } - }) - mark('nodes') - labelsLater.forEach((l) => painter.text(l.str, l.x, l.y, l.o)) - painter.done() - mark('labels') - net3d.timing = T - if (!opts.offscreen) { - net3d.proj = proj.reverse() // nearest first, for picking - net3d.projEdges = projEdges - } -} - -// Smallest label size worth drawing: the user's floor, raised so that at -// most `budget` labels are drawn (the largest, i.e. nearest, ones win). -function labelCutoff3d(sizes, floor, budget) { - if (sizes.length <= budget) return floor - const sorted = sizes.filter((v) => v >= floor).sort((a, b) => b - a) - return sorted.length > budget ? Math.max(floor, sorted[budget]) + 1e-9 : floor -} - -function labelFloor3d() { - const min = parseFloat(document.getElementById('labelMinScreenSize').value) || 0 - return Math.max(3, min) -} - -function drawFloor3d(painter, project, k) { - // a square grid under the network, plus short x/y/z axes at the centre - let minY = Infinity, - maxY = -Infinity, - R = 0 - net3d.cache.nodes.forEach((nd) => { - const p = pos3dOf(nd.id) - if (p[1] < minY) minY = p[1] - if (p[1] > maxY) maxY = p[1] - R = Math.max(R, Math.hypot(p[0] - net3d.cam.tx, p[2] - net3d.cam.tz)) - }) - const floorY = maxY + 40 - const half = Math.ceil((R + 60) / 100) * 100 - const step = half / 5 - const color = currentTheme.muted - for (let i = -5; i <= 5; i++) { - const v = i * step - const a = project([net3d.cam.tx + v, floorY, net3d.cam.tz - half]), - b = project([net3d.cam.tx + v, floorY, net3d.cam.tz + half]) - const c = project([net3d.cam.tx - half, floorY, net3d.cam.tz + v]), - d = project([net3d.cam.tx + half, floorY, net3d.cam.tz + v]) - if (a && b) painter.line(a.x * k, a.y * k, b.x * k, b.y * k, color, k, 0.18) - if (c && d) painter.line(c.x * k, c.y * k, d.x * k, d.y * k, color, k, 0.18) - } - const o = [net3d.cam.tx, net3d.cam.ty, net3d.cam.tz] - const len = step - ;[ - [[len, 0, 0], '#e05252', 'x'], - [[0, -len, 0], '#3fae5a', 'y'], - [[0, 0, len], '#3b7ddd', 'z'], - ].forEach(([v, col, name]) => { - const a = project(o), - b = project([o[0] + v[0], o[1] + v[1], o[2] + v[2]]) - if (!a || !b) return - painter.line(a.x * k, a.y * k, b.x * k, b.y * k, col, 2 * k, 0.8) - painter.text(name, b.x * k, b.y * k, { - size: 11 * k, - color: col, - alpha: 0.9, - align: 'center', - baseline: 'middle', - }) - }) -} - -export function render3d() { - if (!net3d.active) return - const cv = canvas3d() - const rect = cv.getBoundingClientRect() - const dpr = window.devicePixelRatio || 1 - const W = Math.max(1, Math.round(rect.width)), - H = Math.max(1, Math.round(rect.height)) - if (cv.width !== Math.round(W * dpr) || cv.height !== Math.round(H * dpr)) { - cv.width = Math.round(W * dpr) - cv.height = Math.round(H * dpr) - } - const ctx = cv.getContext('2d') - ctx.setTransform(dpr, 0, 0, dpr, 0, 0) - const big = - (net3d.cache ? net3d.cache.nodes.length + net3d.cache.edges.length : cy.elements().length) > - 3000 - drawScene3d(canvasPainter(ctx), W, H, { fast: net3d.interacting && big }) - updateZoomReadout3d() -} diff --git a/frontend/src/view3d/export.ts b/frontend/src/view3d/export.ts deleted file mode 100644 index b8dc6b6..0000000 --- a/frontend/src/view3d/export.ts +++ /dev/null @@ -1,189 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { EXPORT_MAX_PIXELS, EXPORT_MAX_SIDE } from '../export/draw' -import { canvas3d, net3d } from './state' -import { canvasPainter, svgPainter } from './painters' -import { currentTheme } from '../themes' -import { drawScene3d } from './draw' -import { fit3d } from './camera' - -/* ---------- image export of the 3D view ---------- */ -export function export3dSize(scale) { - const cv = canvas3d() - const W = cv.clientWidth || 800, - H = cv.clientHeight || 600 - let k = scale - const shrink = Math.min( - 1, - EXPORT_MAX_SIDE / (W * k), - EXPORT_MAX_SIDE / (H * k), - Math.sqrt(EXPORT_MAX_PIXELS / (W * H * k * k)) - ) - k *= shrink - return { W, H, k, outW: Math.round(W * k), outH: Math.round(H * k), limited: shrink < 1 } -} - -function export3dCamera(area) { - if (area !== 'full') return { ...net3d.cam } - const saved = { ...net3d.cam } - fit3d(null, false) - const cam = { ...net3d.cam } - Object.assign(net3d.cam, saved) - return cam -} - -export function renderExportCanvas3d(o) { - const { W, H, k, outW, outH } = export3dSize(o.scale) - const canvas = document.createElement('canvas') - canvas.width = outW - canvas.height = outH - const ctx = canvas.getContext('2d') - const savedHulls = document.getElementById('showGroupHulls').checked - if (!o.hulls) document.getElementById('showGroupHulls').checked = false - try { - drawScene3d(canvasPainter(ctx), W, H, { - cam: export3dCamera(o.area), - bg: o.bg, - pixelScale: k, - offscreen: true, - fogColor: o.bg || currentTheme.bg, - }) - } finally { - document.getElementById('showGroupHulls').checked = savedHulls - } - return canvas -} - -export function buildSvg3d(o) { - const { W, H } = export3dSize(1) - const s = Math.min(o.scale, 8) - const painter = svgPainter(Math.round(W * s), Math.round(H * s)) - const savedHulls = document.getElementById('showGroupHulls').checked - if (!o.hulls) document.getElementById('showGroupHulls').checked = false - try { - drawScene3d(painter, W, H, { - cam: export3dCamera(o.area), - bg: o.bg, - pixelScale: s, - offscreen: true, - fogColor: o.bg || currentTheme.bg, - }) - } finally { - document.getElementById('showGroupHulls').checked = savedHulls - } - return new Blob([painter.toString(o.title)], { type: 'image/svg+xml' }) -} - -/* ============================================================ - STRING IMPORT - Queries the STRING database (https://string-db.org, Szklarczyk et al.) - through its public API: - 1. get_string_ids resolve the typed names for the chosen organism - 2. network the query proteins plus their best interactors - 3. get_string_ids descriptions for every protein in the network - 4. enrichment or functional_annotation term collections for the - network's proteins (GO, KEGG, Reactome, keywords, domains, - diseases, tissues, ...), turned into NORMA groupings - The network and each term collection become ordinary files in the - library, so everything else in NORMA works on them unchanged. - STRING asks callers to wait a second between requests and to identify - themselves (caller_identity); both are respected here. - ============================================================ */ -export const STRING_CALLER = 'NORMA3' - -export const STRING_SPECIES = [ - [9606, 'Homo sapiens (human)'], - [10090, 'Mus musculus (mouse)'], - [10116, 'Rattus norvegicus (rat)'], - [7955, 'Danio rerio (zebrafish)'], - [7227, 'Drosophila melanogaster (fruit fly)'], - [6239, 'Caenorhabditis elegans (nematode)'], - [4932, 'Saccharomyces cerevisiae (baker\u2019s yeast)'], - [4896, 'Schizosaccharomyces pombe (fission yeast)'], - [3702, 'Arabidopsis thaliana (thale cress)'], - [39947, 'Oryza sativa japonica (rice)'], - [511145, 'Escherichia coli K-12 MG1655'], - [224308, 'Bacillus subtilis 168'], - [83332, 'Mycobacterium tuberculosis H37Rv'], - [9031, 'Gallus gallus (chicken)'], - [9913, 'Bos taurus (cattle)'], - [9823, 'Sus scrofa (pig)'], - [9615, 'Canis lupus familiaris (dog)'], - [8364, 'Xenopus tropicalis (western clawed frog)'], - [36329, 'Plasmodium falciparum 3D7 (malaria parasite)'], -] - -// score field -> NORMA channel (the classic palette uses STRING's colors) -export const STRING_CHANNELS = [ - ['nscore', 'neighborhood', 'Gene neighborhood'], - ['fscore', 'fusion', 'Gene fusion'], - ['pscore', 'cooccurrence', 'Co-occurrence'], - ['ascore', 'coexpression', 'Co-expression'], - ['escore', 'experiments', 'Experiments'], - ['dscore', 'database', 'Curated databases'], - ['tscore', 'textmining', 'Text mining'], -] - -// STRING category key -> readable name, in the order they are offered -export const STRING_CATEGORIES = [ - ['Process', 'GO Biological Process', true], - ['Function', 'GO Molecular Function', true], - ['Component', 'GO Cellular Component', true], - ['KEGG', 'KEGG pathways', true], - ['RCTM', 'Reactome pathways', true], - ['WikiPathways', 'WikiPathways', false], - ['Keyword', 'UniProt keywords', true], - ['Pfam', 'Pfam domains', false], - ['InterPro', 'InterPro domains and features', true], - ['SMART', 'SMART domains', false], - ['DISEASES', 'Diseases (DISEASES)', true], - ['TISSUES', 'Tissues (TISSUES)', false], - ['COMPARTMENTS', 'Subcellular localization (COMPARTMENTS)', false], - ['HPO', 'Human phenotypes (Monarch)', false], - ['MPO', 'Mammalian phenotypes (Monarch)', false], - ['DPO', 'Drosophila phenotypes (Monarch)', false], - ['WPO', 'C. elegans phenotypes (Monarch)', false], - ['ZPO', 'Zebrafish phenotypes (Monarch)', false], - ['FYPO', 'Fission yeast phenotypes (Monarch)', false], - ['NetworkNeighborAL', 'Local network clusters (STRING)', false], - ['PMID', 'Reference publications (PubMed)', false], -] - -const STRING_CATEGORY_ALIASES = { - 'go process': 'Process', - 'biological process': 'Process', - 'go function': 'Function', - 'molecular function': 'Function', - 'go component': 'Component', - 'cellular component': 'Component', - 'kegg pathways': 'KEGG', - reactome: 'RCTM', - 'reactome pathways': 'RCTM', - 'uniprot keywords': 'Keyword', - keywords: 'Keyword', -} - -export const STRING_PRIOR = 0.041 - -export const STRING_PREFERRED_GROUPING = [ - 'KEGG', - 'RCTM', - 'Process', - 'WikiPathways', - 'Function', - 'Component', - 'Keyword', - 'InterPro', - 'DISEASES', -] - -export function stringCategoryKey(raw) { - if (!raw) return 'Other' - if (STRING_CATEGORIES.some((c) => c[0] === raw)) return raw - return STRING_CATEGORY_ALIASES[String(raw).toLowerCase()] || raw -} - -export function stringCategoryName(key) { - const c = STRING_CATEGORIES.find((x) => x[0] === key) - return c ? c[1] : key -} diff --git a/frontend/src/view3d/input.ts b/frontend/src/view3d/input.ts deleted file mode 100644 index 76fc7f3..0000000 --- a/frontend/src/view3d/input.ts +++ /dev/null @@ -1,367 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { - VIEW_PRESETS_3D, - fit3d, - focalLength, - moveCamera3d, - rotate3d, - screenToWorldDelta, - zoom3d, -} from './camera' -import { applySpread3d, runLayout3d, updateSpread3dReadout } from './layouts' -import { canvas3d, el3, markDirty3d, net3d, requestRender3d } from './state' -import { cy, onCommitStyle } from '../cy' -import { hideEdgePopup, hideInfo } from '../profiler' -import { pos3dOf } from './cache' -import { toggleAutoRotate3d } from './tab' - -/* ---------- picking ---------- */ -function pickNode3d(x, y) { - for (const p of net3d.proj) { - if (Math.hypot(p.x - x, p.y - y) <= Math.max(p.r, 4)) return p - } - return null -} - -function pickEdge3d(x, y) { - let best = null, - bestD = 6 - for (const e of net3d.projEdges) { - let d - if (e.cx !== undefined) { - d = Infinity - let px = e.ax, - py = e.ay - for (let i = 1; i <= 12; i++) { - const t = i / 12 - const qx = (1 - t) * (1 - t) * e.ax + 2 * (1 - t) * t * e.cx + t * t * e.bx - const qy = (1 - t) * (1 - t) * e.ay + 2 * (1 - t) * t * e.cy + t * t * e.by - d = Math.min(d, segDist(x, y, px, py, qx, qy)) - px = qx - py = qy - } - } else d = segDist(x, y, e.ax, e.ay, e.bx, e.by) - if (d < bestD + e.width / 2) { - bestD = d - best = e - } - } - return best -} - -function segDist(x, y, x1, y1, x2, y2) { - const dx = x2 - x1, - dy = y2 - y1 - const l2 = dx * dx + dy * dy - const t = l2 ? Math.max(0, Math.min(1, ((x - x1) * dx + (y - y1) * dy) / l2)) : 0 - return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy)) -} - -function updateLook3dReadouts() { - el3('fog3dValue').textContent = Math.round((parseFloat(el3('fog3d').value) || 0) * 100) + '%' - el3('layer3dRow').hidden = !['layers', 'hierarchy', 'degree'].includes(el3('layout3d').value) - el3('layer3dSpacingValue').textContent = el3('layer3dSpacing').value -} - -// page wiring, run by main.ts in the original order -export function init() { - /* ---------- mouse, touch and keyboard ---------- */ - ;(function setupInput3d() { - const cv = canvas3d() - const box = el3('selectBox3d') - let drag = null - const local = (ev) => { - const r = cv.getBoundingClientRect() - return { x: ev.clientX - r.left, y: ev.clientY - r.top } - } - const startInteracting = () => { - net3d.interacting = true - clearTimeout(net3d.interactTimer) - } - const stopInteracting = () => { - clearTimeout(net3d.interactTimer) - net3d.interactTimer = setTimeout(() => { - net3d.interacting = false - requestRender3d() - }, 160) - } - - cv.addEventListener('contextmenu', (ev) => ev.preventDefault()) - cv.addEventListener('pointerdown', (ev) => { - cv.focus() - const p = local(ev) - const node = pickNode3d(p.x, p.y) - let mode - if (ev.button === 2 || ev.button === 1 || ev.altKey) mode = 'pan' - else if (ev.shiftKey && !node) mode = 'box' - else if (node && !ev.shiftKey) mode = 'node' - else mode = 'rotate' - drag = { - mode, - x0: p.x, - y0: p.y, - x: p.x, - y: p.y, - moved: false, - node, - shift: ev.shiftKey, - meta: ev.metaKey || ev.ctrlKey, - cam: { ...net3d.cam }, - } - if (mode === 'node') { - const sel = cy.getElementById(node.id) - drag.group = sel.selected() ? cy.nodes(':selected').map((n) => n.id()) : [node.id] - } - cv.setPointerCapture(ev.pointerId) - ev.preventDefault() - }) - cv.addEventListener('pointermove', (ev) => { - if (!drag) { - const p = local(ev) - cv.style.cursor = pickNode3d(p.x, p.y) ? 'pointer' : 'grab' - return - } - const p = local(ev) - const dx = p.x - drag.x, - dy = p.y - drag.y - if (!drag.moved && Math.hypot(p.x - drag.x0, p.y - drag.y0) < 3) return - if (!drag.moved) { - drag.moved = true - startInteracting() - } - drag.x = p.x - drag.y = p.y - if (drag.mode === 'rotate') { - cv.style.cursor = 'grabbing' - rotate3d(dx * 0.008, dy * 0.008) - } else if (drag.mode === 'pan') { - const H = cv.clientHeight - const scale = focalLength(H) / net3d.cam.dist - const d = screenToWorldDelta(-dx, -dy, scale, net3d.cam) - net3d.cam.tx += d[0] - net3d.cam.ty += d[1] - net3d.cam.tz += d[2] - requestRender3d() - } else if (drag.mode === 'node') { - const d = screenToWorldDelta(dx, dy, drag.node.scale, net3d.cam) - drag.group.forEach((id) => { - const q = pos3dOf(id) - net3d.pos.set(id, [q[0] + d[0], q[1] + d[1], q[2] + d[2]]) - }) - requestRender3d() - } else if (drag.mode === 'box') { - box.hidden = false - box.style.left = Math.min(drag.x0, p.x) + 'px' - box.style.top = Math.min(drag.y0, p.y) + 'px' - box.style.width = Math.abs(p.x - drag.x0) + 'px' - box.style.height = Math.abs(p.y - drag.y0) + 'px' - } - }) - const finish = (ev) => { - if (!drag) return - const p = local(ev) - const d = drag - drag = null - cv.style.cursor = 'grab' - box.hidden = true - if (d.moved) { - stopInteracting() - if (d.mode === 'box') { - const x1 = Math.min(d.x0, p.x), - x2 = Math.max(d.x0, p.x), - y1 = Math.min(d.y0, p.y), - y2 = Math.max(d.y0, p.y) - const ids = net3d.proj - .filter((q) => q.x >= x1 && q.x <= x2 && q.y >= y1 && q.y <= y2) - .map((q) => q.id) - cy.batch(() => ids.forEach((id) => cy.getElementById(id).select())) - } - return - } - // a click - if (d.node) { - const n = cy.getElementById(d.node.id) - if (d.shift || d.meta) { - n.selected() ? n.unselect() : n.select() - return - } - n.emit('tap') - return - } - const edge = pickEdge3d(p.x, p.y) - if (edge) { - cy.getElementById(edge.id).emit('tap') - // place the edge details next to the click - const popup = document.getElementById('edgePopup') - const rect = document.getElementById('canvas').getBoundingClientRect() - popup.style.left = Math.max(10, Math.min(p.x + 14, rect.width - 266)) + 'px' - popup.style.top = Math.max(10, Math.min(p.y + 14, rect.height - 180)) + 'px' - return - } - cy.elements().removeClass('dimmed highlighted') - hideInfo() - hideEdgePopup() - if (!d.shift) cy.$(':selected').unselect() - } - cv.addEventListener('pointerup', finish) - cv.addEventListener('pointercancel', finish) - cv.addEventListener('dblclick', (ev) => { - const p = local(ev) - const node = pickNode3d(p.x, p.y) - if (node) { - const q = pos3dOf(node.id) - moveCamera3d({ tx: q[0], ty: q[1], tz: q[2] }) - } else fit3d() - }) - cv.addEventListener( - 'wheel', - (ev) => { - ev.preventDefault() - startInteracting() - zoom3d(Math.exp(-ev.deltaY * 0.0015)) - stopInteracting() - }, - { passive: false } - ) - cv.addEventListener('keydown', (ev) => { - const k = ev.key - const step = ev.shiftKey ? 0.25 : 0.08 - if (k === 'ArrowLeft') rotate3d(-step, 0) - else if (k === 'ArrowRight') rotate3d(step, 0) - else if (k === 'ArrowUp') rotate3d(0, -step) - else if (k === 'ArrowDown') rotate3d(0, step) - else if (k === '+' || k === '=') zoom3d(1.25) - else if (k === '-' || k === '_') zoom3d(0.8) - else if (k === '0' || k === 'f') fit3d() - else return - ev.preventDefault() - }) - new ResizeObserver(() => requestRender3d()).observe(cv) - })() - - /* ---------- controls ---------- */ - document.getElementById('zoomIn').addEventListener( - 'click', - (ev) => { - if (net3d.active) { - ev.stopImmediatePropagation() - zoom3d(1.25) - } - }, - true - ) - - document.getElementById('zoomOut').addEventListener( - 'click', - (ev) => { - if (net3d.active) { - ev.stopImmediatePropagation() - zoom3d(0.8) - } - }, - true - ) - - document.getElementById('zoomFit').addEventListener( - 'click', - (ev) => { - if (net3d.active) { - ev.stopImmediatePropagation() - fit3d() - } - }, - true - ) - - el3('btnRotL3d').addEventListener('click', () => - moveCamera3d({ yaw: net3d.cam.yaw - Math.PI / 8 }) - ) - - el3('btnRotR3d').addEventListener('click', () => - moveCamera3d({ yaw: net3d.cam.yaw + Math.PI / 8 }) - ) - - el3('btnRotU3d').addEventListener('click', () => - moveCamera3d({ pitch: net3d.cam.pitch - Math.PI / 8 }) - ) - - el3('btnRotD3d').addEventListener('click', () => - moveCamera3d({ pitch: net3d.cam.pitch + Math.PI / 8 }) - ) - - el3('btnReset3d').addEventListener('click', () => { - moveCamera3d({ ...VIEW_PRESETS_3D.tilted }, false) - fit3d() - }) - - el3('btnAuto3d').addEventListener('click', () => { - const on = el3('btnAuto3d').getAttribute('aria-pressed') !== 'true' - el3('autoRotate3d').checked = on - toggleAutoRotate3d(on) - }) - - el3('autoRotate3d').addEventListener('change', () => - toggleAutoRotate3d(el3('autoRotate3d').checked) - ) - - document.querySelectorAll('[data-view3d]').forEach((btn) => { - btn.addEventListener('click', () => { - moveCamera3d({ ...VIEW_PRESETS_3D[btn.dataset.view3d] }, false) - fit3d() - }) - }) - - el3('btnRun3d').addEventListener('click', () => runLayout3d()) - - el3('btnFit3d').addEventListener('click', () => fit3d()) - - el3('layout3d').addEventListener('change', () => { - const algo = el3('layout3d').value - el3('layer3dRow').hidden = !['layers', 'hierarchy', 'degree'].includes(algo) - }) - - el3('layer3dSpacing').addEventListener('input', () => { - el3('layer3dSpacingValue').textContent = el3('layer3dSpacing').value - }) - - el3('spread3d').addEventListener('input', applySpread3d) - - el3('spread3d').addEventListener('dblclick', () => { - el3('spread3d').value = 0 - applySpread3d() - }) - - ;['persp3d', 'fog3d', 'style3d', 'grid3d'].forEach((id) => { - el3(id).addEventListener('input', () => { - updateLook3dReadouts() - requestRender3d() - }) - el3(id).addEventListener('change', () => { - updateLook3dReadouts() - requestRender3d() - }) - }) - - updateLook3dReadouts() - - updateSpread3dReadout() - - // settings that change what the 2D view draws also change the 3D picture - onCommitStyle(() => { - if (net3d.active) markDirty3d() - else net3d.cacheDirty = true - }) - - ;[ - 'showGroupHulls', - 'hullStyle', - 'hullOpacity', - 'edgeCurveStyle', - 'edgeCurvature', - 'labelMinScreenSize', - ].forEach((id) => { - document.getElementById(id).addEventListener('input', requestRender3d) - document.getElementById(id).addEventListener('change', requestRender3d) - }) -} diff --git a/frontend/src/view3d/layouts.ts b/frontend/src/view3d/layouts.ts deleted file mode 100644 index f8c024e..0000000 --- a/frontend/src/view3d/layouts.ts +++ /dev/null @@ -1,411 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from '../state' -import { UNGROUPED, effectiveGroupsFor, getUsedGroups } from '../network_state' -import { VIEW_PRESETS_3D, fit3d, moveCamera3d, shownIds3d } from './camera' -import { - anyEdgeDirected, - edgeIsDirected, - fibonacciBall, - fibonacciSphere, - fr3dAsync, - fr3dLayout, -} from '../export/dialog' -import { computeMetricValues, shownEdges, shownNodes, targetNodeSpacing } from '../metrics' -import { cy } from '../cy' -import { el3, net3d, requestRender3d } from './state' -import { mulberry32 } from '../sample_data' -import { pos3dOf } from './cache' -import { render3d } from './draw' -import { setStatus } from '../layouts/controls' - -/* ---------- layouts ---------- */ -function setLayoutStatus3d(items) { - setStatus('layout3dStatus', items) -} - -function positions2dOf(ids) { - const out = {} - ids.forEach((id) => { - const p = cy.getElementById(id).position() - out[id] = [p.x, p.y] - }) - return out -} - -function centerPositions(map) { - const ids = Object.keys(map) - if (!ids.length) return map - const c = [0, 0, 0] - ids.forEach((id) => { - c[0] += map[id][0] - c[1] += map[id][1] - c[2] += map[id][2] - }) - c.forEach((v, i) => { - c[i] = v / ids.length - }) - ids.forEach((id) => { - map[id] = [map[id][0] - c[0], map[id][1] - c[1], map[id][2] - c[2]] - }) - return map -} - -// Layer index (0, 1, ...) for each node: its ticked groups in legend order; -// nodes in several groups sit between their layers. -function groupLayerOf(nodes) { - const groups = getUsedGroups().filter((g) => S.activeGroups.has(g)) - const rank = new Map(groups.map((g, i) => [g, i])) - const out = {} - nodes.forEach((n) => { - const gs = effectiveGroupsFor(n).filter((g) => rank.has(g)) - out[n.id()] = gs.length ? gs.reduce((s, g) => s + rank.get(g), 0) / gs.length : groups.length - }) - return out -} - -function hierarchyLevels(nodes, edges) { - const ids = nodes.map((n) => n.id()) - const out = new Map(ids.map((id) => [id, []])) - const indeg = new Map(ids.map((id) => [id, 0])) - edges.forEach((e) => { - const s = e.data('source'), - t = e.data('target') - if (!out.has(s) || !out.has(t) || s === t) return - out.get(s).push(t) - indeg.set(t, indeg.get(t) + 1) - if (!edgeIsDirected(e)) { - out.get(t).push(s) - indeg.set(s, indeg.get(s) + 1) - } - }) - const level = new Map() - const directed = anyEdgeDirected(edges) - // roots: nodes nothing points to (directed) or the best-connected node of each component - const byDegree = [...ids].sort((a, b) => out.get(b).length - out.get(a).length) - const roots = directed ? ids.filter((id) => indeg.get(id) === 0 && out.get(id).length) : [] - const bfs = (starts) => { - const queue = [] - starts.forEach((s) => { - if (!level.has(s)) { - level.set(s, 0) - queue.push(s) - } - }) - for (let q = 0; q < queue.length; q++) { - const v = queue[q] - out.get(v).forEach((w) => { - if (!level.has(w)) { - level.set(w, level.get(v) + 1) - queue.push(w) - } - }) - } - } - bfs(roots) - byDegree.forEach((id) => { - if (!level.has(id)) bfs([id]) - }) - return level -} - -async function computeLayout3d(algo) { - const nodes = shownNodes() - const channelOnly = el3('layout3dChannels').checked - const edges = shownEdges(channelOnly) - const ids = nodes.map((n) => n.id()) - const spacing = targetNodeSpacing() - const layerGap = parseFloat(el3('layer3dSpacing').value) || 150 - const pos2 = positions2dOf(ids) - const result = {} - if (!ids.length) return result - - if (algo === 'fr3d') { - const init = {} - const r = await fr3dAsync( - ids, - edges.map((e) => ({ s: e.data('source'), t: e.data('target'), w: e.data('weight') })), - { spacing, seed: 123 }, - net3d.onLayoutProgress - ) - return r - } - if (algo === 'flat') { - ids.forEach((id) => { - result[id] = [pos2[id][0], pos2[id][1], 0] - }) - return centerPositions(result) - } - if (algo === 'layers' || algo === 'hierarchy' || algo === 'degree') { - // the 2D layout lies flat; height (y) comes from groups, levels or degree - let height - if (algo === 'layers') { - const layer = groupLayerOf(nodes) - height = (id) => layer[id] * layerGap - } else if (algo === 'hierarchy') { - const level = hierarchyLevels(nodes, edges) - height = (id) => (level.get(id) || 0) * layerGap - } else { - const deg = computeMetricValues('degree', channelOnly) - const max = Math.max(1, ...Object.values(deg)) - height = (id) => (-(deg[id] || 0) / max) * layerGap * 3 - } - ids.forEach((id) => { - result[id] = [pos2[id][0], height(id), pos2[id][1]] - }) - return centerPositions(result) - } - if (algo === 'sphere') { - const R = spacing * Math.sqrt(ids.length) * 0.5 + spacing - const order = [...nodes].sort((a, b) => b.degree() - a.degree()) - fibonacciSphere(order.length).forEach((p, i) => { - result[order[i].id()] = [p[0] * R, p[1] * R, p[2] * R] - }) - return result - } - if (algo === 'cube') { - const side = Math.ceil(Math.cbrt(ids.length)) - const order = [...nodes].sort((a, b) => b.degree() - a.degree()) - order.forEach((n, i) => { - const x = i % side, - y = Math.floor(i / side) % side, - z = Math.floor(i / (side * side)) - const step = spacing * 1.8 - result[n.id()] = [ - (x - (side - 1) / 2) * step, - (y - (side - 1) / 2) * step, - (z - (side - 1) / 2) * step, - ] - }) - return result - } - if (algo === 'random') { - const rand = mulberry32(7) - const R = spacing * Math.cbrt(ids.length) * 0.9 - ids.forEach((id) => { - result[id] = [(rand() * 2 - 1) * R, (rand() * 2 - 1) * R, (rand() * 2 - 1) * R] - }) - return result - } - if (algo === 'groups3d') { - // each ticked group becomes a ball of nodes (hubs in the middle); the - // balls are placed by a 3D force layout of the group network and then - // pushed apart until they no longer overlap - const groups = getUsedGroups().filter((g) => S.activeGroups.has(g)) - const members = new Map(groups.map((g) => [g, []])) - const memberOf = {} - nodes.forEach((n) => { - let gs = effectiveGroupsFor(n).filter((g) => members.has(g)) - if (!gs.length) { - if (!members.has(UNGROUPED)) members.set(UNGROUPED, []) - gs = [UNGROUPED] - } - memberOf[n.id()] = gs - gs.forEach((g) => members.get(g).push(n)) - }) - const keys = [...members.keys()].filter((g) => members.get(g).length) - const radius = new Map( - keys.map((g) => [g, spacing * 0.55 * Math.cbrt(members.get(g).length) + spacing * 0.3]) - ) - const weight = new Map() - edges.forEach((e) => { - const ga = memberOf[e.data('source')], - gb = memberOf[e.data('target')] - if (!ga || !gb) return - ga.forEach((a) => - gb.forEach((b) => { - if (a === b) return - const key = a < b ? a + '\u0000' + b : b + '\u0000' + a - weight.set(key, (weight.get(key) || 0) + 1) - }) - ) - }) - const metaEdges = [...weight].map(([key, w]) => { - const [s, t] = key.split('\u0000') - return { s, t, w } - }) - const maxR = Math.max(...radius.values()) - const centers = fr3dLayout(keys, metaEdges, { spacing: maxR * 2.4, seed: 11, iterations: 300 }) - const gap = spacing * 0.6 - const needOf = (a, b) => radius.get(a) + radius.get(b) + gap - if (keys.length > 1) { - // scale so that each group's nearest neighbour is about touching, - // then pull far-away groups (often unconnected ones) back in - const ratios = keys - .map((a) => { - let best = Infinity, - need = 1 - keys.forEach((b) => { - if (a === b) return - const d = - Math.hypot( - centers[a][0] - centers[b][0], - centers[a][1] - centers[b][1], - centers[a][2] - centers[b][2] - ) || 0.01 - if (d / needOf(a, b) < best) { - best = d / needOf(a, b) - need = needOf(a, b) - } - }) - return best - }) - .sort((x, y) => x - y) - const factor = 1.15 / ratios[Math.floor(ratios.length / 2)] - keys.forEach((g) => { - centers[g] = centers[g].map((v) => v * factor) - }) - keys.forEach((a) => { - let nearest = null, - best = Infinity - keys.forEach((b) => { - if (a === b) return - const d = Math.hypot( - centers[a][0] - centers[b][0], - centers[a][1] - centers[b][1], - centers[a][2] - centers[b][2] - ) - if (d < best) { - best = d - nearest = b - } - }) - const need = needOf(a, nearest) - if (best > 2.5 * need) { - const t = 1 - (1.6 * need) / best - centers[a] = centers[a].map((v, d) => v + (centers[nearest][d] - v) * t) - } - }) - } - // separate overlapping balls - for (let it = 0; it < 80; it++) { - let moved = false - for (let i = 0; i < keys.length; i++) - for (let j = i + 1; j < keys.length; j++) { - const a = centers[keys[i]], - b = centers[keys[j]] - const dx = b[0] - a[0], - dy = b[1] - a[1], - dz = b[2] - a[2] - const d = Math.hypot(dx, dy, dz) || 0.01 - const need = radius.get(keys[i]) + radius.get(keys[j]) + gap - if (d < need) { - const push = (need - d) / 2 / d - a[0] -= dx * push - a[1] -= dy * push - a[2] -= dz * push - b[0] += dx * push - b[1] += dy * push - b[2] += dz * push - moved = true - } - } - if (!moved) break - } - const acc = {} - keys.forEach((g) => { - const list = [...members.get(g)].sort((a, b) => b.degree() - a.degree()) - const ball = fibonacciBall(list.length, radius.get(g)) - const c = centers[g] - list.forEach((n, i) => { - const id = n.id() - ;(acc[id] = acc[id] || []).push([c[0] + ball[i][0], c[1] + ball[i][1], c[2] + ball[i][2]]) - }) - }) - Object.entries(acc).forEach(([id, list]) => { - result[id] = [0, 1, 2].map((d) => list.reduce((s, p) => s + p[d], 0) / list.length) - }) - return centerPositions(result) - } - return result -} - -export async function runLayout3d(algo) { - algo = algo || el3('layout3d').value - const seq = ++net3d.layoutSeq - el3('btnRun3d').disabled = true - const big3d = shownNodes().length > 300 - setLayoutStatus3d( - big3d ? [{ level: 'busy', text: 'Computing the 3D layout…', progress: null }] : [] - ) - net3d.onLayoutProgress = (f) => { - if (big3d && seq === net3d.layoutSeq) - setLayoutStatus3d([{ level: 'busy', text: 'Computing the 3D layout…', progress: f }]) - } - await new Promise((r) => setTimeout(r, 20)) - try { - const result = await computeLayout3d(algo) - if (seq !== net3d.layoutSeq || !result) return - net3d.spreadApplied = 0 - el3('spread3d').value = 0 - updateSpread3dReadout() - animatePositions3d(result) - net3d.hasLayout = true - if (algo === 'layers' || algo === 'hierarchy' || algo === 'degree') { - moveCamera3d({ ...VIEW_PRESETS_3D.tilted, pitch: 0.55 }, false) - } - setTimeout(() => fit3d(null, true), 520) - setLayoutStatus3d([]) - } catch (err) { - setLayoutStatus3d([ - { level: 'error', text: `The 3D layout couldn't be computed: ${err.message}` }, - ]) - } finally { - if (seq === net3d.layoutSeq) el3('btnRun3d').disabled = false - } -} - -function animatePositions3d(target) { - const ids = Object.keys(target) - const from = new Map(ids.map((id) => [id, pos3dOf(id).slice()])) - const start = performance.now() - const step = (now) => { - const t = Math.min(1, (now - start) / 500) - const e = 1 - Math.pow(1 - t, 3) - ids.forEach((id) => { - const a = from.get(id), - b = target[id] - net3d.pos.set(id, [ - a[0] + (b[0] - a[0]) * e, - a[1] + (b[1] - a[1]) * e, - a[2] + (b[2] - a[2]) * e, - ]) - }) - render3d() - if (t < 1) requestAnimationFrame(step) - } - requestAnimationFrame(step) -} - -export function updateSpread3dReadout() { - const v = parseFloat(el3('spread3d').value) || 0 - el3('spread3dValue').textContent = Math.pow(2, v).toFixed(2) + '×' -} - -export function applySpread3d() { - const v = parseFloat(el3('spread3d').value) || 0 - const ratio = Math.pow(2, v - net3d.spreadApplied) - net3d.spreadApplied = v - updateSpread3dReadout() - const ids = shownIds3d() - if (!ids.length) return - const c = [0, 0, 0] - ids.forEach((id) => { - const p = pos3dOf(id) - c[0] += p[0] - c[1] += p[1] - c[2] += p[2] - }) - c.forEach((x, i) => { - c[i] = x / ids.length - }) - ids.forEach((id) => { - const p = pos3dOf(id) - net3d.pos.set(id, [ - c[0] + (p[0] - c[0]) * ratio, - c[1] + (p[1] - c[1]) * ratio, - c[2] + (p[2] - c[2]) * ratio, - ]) - }) - requestRender3d() -} diff --git a/frontend/src/view3d/painters.ts b/frontend/src/view3d/painters.ts deleted file mode 100644 index dd98753..0000000 --- a/frontend/src/view3d/painters.ts +++ /dev/null @@ -1,226 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -/* ---------- painters: the same drawing code writes to a canvas or to SVG ---------- */ -export function canvasPainter(ctx) { - return { - kind: 'canvas', - clear(W, H, bg) { - ctx.clearRect(0, 0, W, H) - if (bg) { - ctx.fillStyle = bg - ctx.fillRect(0, 0, W, H) - } - }, - pathD(d, st) { - const path = new Path2D(d) - ctx.save() - if (st.blur) ctx.filter = `blur(${st.blur}px)` - if (st.fill) { - ctx.globalAlpha = st.fillAlpha ?? 1 - ctx.fillStyle = st.fill - ctx.fill(path, st.evenodd ? 'evenodd' : 'nonzero') - } - if (st.stroke) { - ctx.globalAlpha = st.strokeAlpha ?? 1 - ctx.strokeStyle = st.stroke - ctx.lineWidth = st.width || 1 - ctx.stroke(path) - } - ctx.restore() - }, - line(x1, y1, x2, y2, color, width, alpha, cx, cy0) { - ctx.globalAlpha = alpha - ctx.strokeStyle = color - ctx.lineWidth = width - ctx.beginPath() - ctx.moveTo(x1, y1) - if (cx !== undefined) ctx.quadraticCurveTo(cx, cy0, x2, y2) - else ctx.lineTo(x2, y2) - ctx.stroke() - }, - circle(x, y, r, fill, alpha, stroke, width) { - ctx.globalAlpha = alpha - ctx.beginPath() - ctx.arc(x, y, r, 0, Math.PI * 2) - if (fill) { - ctx.fillStyle = fill - ctx.fill() - } - if (stroke && width > 0) { - ctx.strokeStyle = stroke - ctx.lineWidth = width - ctx.stroke() - } - }, - wedge(x, y, r, a0, a1, fill, alpha) { - ctx.globalAlpha = alpha - ctx.fillStyle = fill - ctx.beginPath() - ctx.moveTo(x, y) - ctx.arc(x, y, r, a0, a1) - ctx.closePath() - ctx.fill() - }, - poly(pts, fill, alpha, stroke, width) { - ctx.globalAlpha = alpha - ctx.beginPath() - pts.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]))) - ctx.closePath() - if (fill) { - ctx.fillStyle = fill - ctx.fill() - } - if (stroke && width > 0) { - ctx.strokeStyle = stroke - ctx.lineWidth = width - ctx.stroke() - } - }, - shade(x, y, r, alpha) { - const g = ctx.createRadialGradient(x - r * 0.35, y - r * 0.35, r * 0.1, x, y, r) - g.addColorStop(0, 'rgba(255,255,255,0.55)') - g.addColorStop(0.45, 'rgba(255,255,255,0.05)') - g.addColorStop(1, 'rgba(0,0,0,0.35)') - ctx.globalAlpha = alpha - ctx.fillStyle = g - ctx.beginPath() - ctx.arc(x, y, r, 0, Math.PI * 2) - ctx.fill() - }, - text(str, x, y, o) { - const font = `${Math.round(o.size * 2) / 2}px Inter, Helvetica, Arial, sans-serif` - if (!o.rotate && !o.bg) { - // common case: no state to save - ctx.globalAlpha = o.alpha - if (ctx.font !== font) ctx.font = font - ctx.textAlign = o.align - ctx.textBaseline = o.baseline - if (o.halo && o.haloWidth > 0) { - ctx.lineWidth = o.haloWidth * 2 - ctx.strokeStyle = o.halo - ctx.lineJoin = 'round' - ctx.strokeText(str, x, y) - } - ctx.fillStyle = o.color - ctx.fillText(str, x, y) - return - } - ctx.save() - ctx.globalAlpha = o.alpha - ctx.font = font - ctx.textAlign = o.align - ctx.textBaseline = o.baseline - ctx.translate(x, y) - if (o.rotate) ctx.rotate(o.rotate) - if (o.bg) { - const w = ctx.measureText(str).width - const h = o.size * 1.2 - const left = o.align === 'center' ? -w / 2 : o.align === 'right' ? -w : 0 - ctx.fillStyle = o.bg - ctx.globalAlpha = o.alpha * 0.85 - ctx.fillRect(left - 2, -h / 2, w + 4, h) - ctx.globalAlpha = o.alpha - } - if (o.halo && o.haloWidth > 0) { - ctx.lineWidth = o.haloWidth * 2 - ctx.strokeStyle = o.halo - ctx.lineJoin = 'round' - ctx.strokeText(str, 0, 0) - } - ctx.fillStyle = o.color - ctx.fillText(str, 0, 0) - ctx.restore() - }, - done() { - ctx.globalAlpha = 1 - }, - } -} - -export function svgPainter(W, H) { - const out = [] - const f = (v) => (Math.round(v * 100) / 100).toString() - const esc = (v) => - String(v).replace( - /[&<>"]/g, - (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] - ) - const op = (a) => (a < 1 ? ` opacity="${f(a)}"` : '') - let blurId = 0 - const defs = [ - '', - ] - return { - kind: 'svg', - clear(w, h, bg) { - if (bg) out.push(``) - }, - pathD(d, st) { - let filter = '' - if (st.blur) { - const id = 'b' + ++blurId - defs.push( - `` - ) - filter = ` filter="url(#${id})"` - } - out.push( - `` - ) - }, - line(x1, y1, x2, y2, color, width, alpha, cx, cy0) { - const d = - cx !== undefined - ? `M${f(x1)},${f(y1)}Q${f(cx)},${f(cy0)} ${f(x2)},${f(y2)}` - : `M${f(x1)},${f(y1)}L${f(x2)},${f(y2)}` - out.push( - `` - ) - }, - circle(x, y, r, fill, alpha, stroke, width) { - out.push( - ` 0 ? ` stroke="${stroke}" stroke-width="${f(width)}"` : ''}${op(alpha)}/>` - ) - }, - wedge(x, y, r, a0, a1, fill, alpha) { - const large = a1 - a0 > Math.PI ? 1 : 0 - out.push( - `` - ) - }, - poly(pts, fill, alpha, stroke, width) { - out.push( - ` 0 ? ` stroke="${stroke}" stroke-width="${f(width)}"` : ''}${op(alpha)}/>` - ) - }, - shade(x, y, r, alpha) { - out.push(``) - }, - text(str, x, y, o) { - const anchor = o.align === 'center' ? 'middle' : o.align === 'right' ? 'end' : 'start' - const base = - o.baseline === 'middle' - ? 'central' - : o.baseline === 'top' - ? 'text-before-edge' - : 'text-after-edge' - const rot = o.rotate - ? ` transform="rotate(${f((o.rotate * 180) / Math.PI)} ${f(x)} ${f(y)})"` - : '' - const halo = - o.halo && o.haloWidth > 0 - ? ` stroke="${o.halo}" stroke-width="${f(o.haloWidth * 2)}" stroke-linejoin="round" paint-order="stroke"` - : '' - const bgHalo = o.bg - ? ` stroke="${o.bg}" stroke-width="${f(o.size * 0.5)}" stroke-linejoin="round" paint-order="stroke"` - : '' - out.push( - `${esc(str)}` - ) - }, - done() {}, - toString(title) { - return `\n\n${esc(title || 'NORMA 3D network')}\n${defs.join('')}\n${out.join('\n')}\n` - }, - } -} diff --git a/frontend/src/view3d/state.ts b/frontend/src/view3d/state.ts deleted file mode 100644 index 822397b..0000000 --- a/frontend/src/view3d/state.ts +++ /dev/null @@ -1,86 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { cy } from '../cy' -import { normalizeCssColor } from '../layouts/controls' -import { render3d } from './draw' - -/* ---------- state ---------- */ -export const net3d = { - active: false, - pos: new Map(), // node id -> [x, y, z] - cam: { yaw: 0.6, pitch: 0.35, dist: 900, tx: 0, ty: 0, tz: 0 }, - hasLayout: false, - cache: null, - cacheDirty: true, - frameQueued: false, - interacting: false, - interactTimer: null, - autoTimer: null, - lastFrame: 0, - proj: [], // last projected nodes, near first - projEdges: [], - layoutSeq: 0, - anim: null, - spreadApplied: 0, -} - -export function el3(id) { - return document.getElementById(id) -} - -export function canvas3d() { - return el3('cy3d') -} - -export function markDirty3d() { - net3d.cacheDirty = true - requestRender3d() -} - -export function requestRender3d() { - if (!net3d.active || net3d.frameQueued) return - net3d.frameQueued = true - requestAnimationFrame(() => { - net3d.frameQueued = false - render3d() - }) -} - -/* ---------- colors ---------- */ -const colorCache3d = new Map() - -export function rgbOf(color) { - if (colorCache3d.has(color)) return colorCache3d.get(color) - let rgb = [136, 136, 136] - const s = String(color || '').trim() - let m - if ((m = /^#([0-9a-f]{3})$/i.exec(s))) rgb = [...m[1]].map((c) => parseInt(c + c, 16)) - else if ((m = /^#([0-9a-f]{6})/i.exec(s))) - rgb = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)) - else if ((m = /rgba?\(([^)]+)\)/i.exec(s))) - rgb = m[1] - .split(',') - .slice(0, 3) - .map((v) => parseFloat(v)) - else { - const hex = typeof normalizeCssColor === 'function' ? normalizeCssColor(s) : null - if (hex) return rgbOf(hex) - } - colorCache3d.set(color, rgb) - return rgb -} - -export function mixColor(color, bg, t) { - if (t <= 0) return color - const a = rgbOf(color), - b = rgbOf(bg) - return `rgb(${Math.round(a[0] + (b[0] - a[0]) * t)},${Math.round(a[1] + (b[1] - a[1]) * t)},${Math.round(a[2] + (b[2] - a[2]) * t)})` -} - -// page wiring, run by main.ts in the original order -export function init() { - cy.on('add remove data select unselect style class', () => { - if (net3d.active) markDirty3d() - else net3d.cacheDirty = true - }) -} diff --git a/frontend/src/view3d/tab.ts b/frontend/src/view3d/tab.ts deleted file mode 100644 index 5daa36a..0000000 --- a/frontend/src/view3d/tab.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { cy } from '../cy' -import { el3, net3d, requestRender3d } from './state' -import { fit3d, wrapAngle } from './camera' -import { render3d } from './draw' -import { runLayout3d, updateSpread3dReadout } from './layouts' -import { updateZoomReadout } from '../profiler' - -/* ---------- entering and leaving the 3D tab ---------- */ -export function setMode3d(on) { - net3d.active = on - document.getElementById('canvas').classList.toggle('mode3d', on) - el3('cy3d').hidden = !on - el3('nav3d').hidden = !on - el3('section3d').hidden = !on - const layout2d = document.getElementById('layoutSection') - if (layout2d) layout2d.hidden = on - document.getElementById('hint').textContent = on - ? 'Drag to rotate · drag a node to move it · right-drag to pan · scroll to zoom · Shift-drag to select · double-click to fit' - : 'Drag nodes · Shift-drag or Shift-click to select several · scroll to zoom · double-click to recenter' - if (on) { - net3d.cacheDirty = true - if (!net3d.hasLayout && cy.nodes().length) { - // first visit for this view: start from the 2D picture, then lay out in 3D - cy.nodes().forEach((n) => { - if (!net3d.pos.has(n.id())) { - const p = n.position() - net3d.pos.set(n.id(), [p.x, p.y, 0]) - } - }) - fit3d(null, false) - runLayout3d(el3('layout3d').value) - } else { - render3d() - } - toggleAutoRotate3d(el3('autoRotate3d').checked) - } else { - toggleAutoRotate3d(false) - updateZoomReadout() - } -} - -// per-view state -export function capture3d() { - const pos = {} - net3d.pos.forEach((p, id) => { - pos[id] = p - }) - return { pos, cam: { ...net3d.cam }, hasLayout: net3d.hasLayout, spread: net3d.spreadApplied } -} - -export function restore3d(state) { - net3d.pos = new Map(state && state.pos ? Object.entries(state.pos) : []) - net3d.cam = - state && state.cam - ? { ...state.cam } - : { yaw: 0.6, pitch: 0.35, dist: 900, tx: 0, ty: 0, tz: 0 } - net3d.hasLayout = !!(state && state.hasLayout) - net3d.spreadApplied = (state && state.spread) || 0 - el3('spread3d').value = net3d.spreadApplied - updateSpread3dReadout() - net3d.cacheDirty = true - net3d.layoutSeq++ - if (net3d.active) { - if (!net3d.hasLayout && cy.nodes().length) { - cy.nodes().forEach((n) => { - if (!net3d.pos.has(n.id())) { - const p = n.position() - net3d.pos.set(n.id(), [p.x, p.y, 0]) - } - }) - fit3d(null, false) - runLayout3d(el3('layout3d').value) - } else render3d() - } -} - -// new data in the current view: 3D positions start over -export function reset3dForNewData() { - net3d.pos = new Map() - net3d.hasLayout = false - net3d.cacheDirty = true - net3d.layoutSeq++ - if (net3d.active && cy.nodes().length) { - cy.nodes().forEach((n) => { - const p = n.position() - net3d.pos.set(n.id(), [p.x, p.y, 0]) - }) - fit3d(null, false) - runLayout3d(el3('layout3d').value) - } -} - -/* ---------- auto-rotate ---------- */ -export function toggleAutoRotate3d(on) { - cancelAnimationFrame(net3d.autoTimer) - net3d.autoTimer = null - el3('btnAuto3d').setAttribute('aria-pressed', on ? 'true' : 'false') - if (!on || !net3d.active) return - let last = performance.now() - const tick = (now) => { - const speed = parseFloat(el3('autoSpeed3d').value) || 1 - net3d.cam.yaw = wrapAngle(net3d.cam.yaw + ((now - last) / 1000) * 0.35 * speed) - last = now - const big = net3d.cache && net3d.cache.nodes.length + net3d.cache.edges.length > 3000 - if (big) { - net3d.interacting = true - clearTimeout(net3d.interactTimer) - net3d.interactTimer = setTimeout(() => { - net3d.interacting = false - requestRender3d() - }, 200) - } - render3d() - net3d.autoTimer = requestAnimationFrame(tick) - } - net3d.autoTimer = requestAnimationFrame(tick) -} diff --git a/frontend/src/viewbar.ts b/frontend/src/viewbar.ts deleted file mode 100644 index f23e936..0000000 --- a/frontend/src/viewbar.ts +++ /dev/null @@ -1,54 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { activeView, createView, renderViewBar, switchView, uniqueViewName } from './profiler' - -function setRenaming(on) { - document.getElementById('viewSelectRow').hidden = on - document.getElementById('viewRenameRow').hidden = !on - const input = document.getElementById('viewNameInput') - if (on) { - input.value = activeView() ? activeView().name : '' - input.focus() - input.select() - } else { - document.getElementById('viewSelect').focus() - } -} - -function saveRename() { - const v = activeView() - const name = document.getElementById('viewNameInput').value.trim() - if (v && name && name !== v.name) { - v.name = uniqueViewName(name) - v.autoName = false - } - setRenaming(false) - renderViewBar() -} - -// page wiring, run by main.ts in the original order -export function init() { - /* ---------- view bar controls ---------- */ - document - .getElementById('viewSelect') - .addEventListener('change', (e) => switchView(e.target.value)) - - document.getElementById('btnViewDuplicate').addEventListener('click', () => { - const cur = activeView() - createView(`${cur ? cur.name : 'View'} copy`, { copy: true }) - }) - - document.getElementById('btnViewRename').addEventListener('click', () => setRenaming(true)) - - document.getElementById('btnViewRenameSave').addEventListener('click', saveRename) - - document.getElementById('viewNameInput').addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault() - saveRename() - } else if (e.key === 'Escape') { - e.preventDefault() - setRenaming(false) - } - }) -} diff --git a/frontend/src/welcome.ts b/frontend/src/welcome.ts deleted file mode 100644 index 6fe6950..0000000 --- a/frontend/src/welcome.ts +++ /dev/null @@ -1,132 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { cy } from './cy' -import { switchSideTab } from './side_tabs' -import { switchTab } from './wiring' - -/* ---------- welcome card ---------- */ -export function updateEmptyState() { - const el = document.getElementById('emptyState') - if (!el) return - const empty = !cy.nodes().length - el.hidden = !empty - document.getElementById('canvas').classList.toggle('is-empty', empty) -} - -function openWelcomeExample() { - switchSideTab('data') - const sel = document.getElementById('sampleSelect') - sel.value = 'norma:cascade' - document.getElementById('btnSample').click() - switchTab('network') -} - -/* ============================================================ - COMMUNITY DETECTION - All algorithms work on the profiler's simple undirected graph - ({ ids, n, m, adj }) and return { membership, count, sizes, modularity }. - Randomized steps use a fixed seed, so results are repeatable. - louvain Blondel et al. 2008 (resolution parameter) - leidenCommunities Traag, Waltman & van Eck 2019 - labelPropagation Raghavan, Albert & Kumara 2007 - walktrap Pons & Latapy 2005 (random walks of length t) - markovClustering van Dongen 2000 (MCL, inflation parameter) - ============================================================ */ -export const COMMUNITY_ALGORITHMS = { - louvain: { label: 'Louvain', short: 'Louvain' }, - leiden: { label: 'Leiden', short: 'Leiden' }, - lpa: { label: 'Label propagation', short: 'label propagation' }, - walktrap: { label: 'Walktrap', short: 'Walktrap' }, - mcl: { label: 'Markov clustering (MCL)', short: 'MCL' }, -} - -export const WALKTRAP_NODE_LIMIT = 2000 - -// Renumbers communities 0..k-1 in order of first appearance and scores the -// partition by (resolution-free) modularity on the original graph. -export function partitionStats(g, raw) { - const n = g.n - const remap = new Map() - const membership = new Int32Array(n) - for (let i = 0; i < n; i++) { - if (!remap.has(raw[i])) remap.set(raw[i], remap.size) - membership[i] = remap.get(raw[i]) - } - const count = remap.size - const sizes = new Array(count).fill(0) - const inW = new Float64Array(count), - totW = new Float64Array(count) - const twoM = 2 * g.m - for (let u = 0; u < n; u++) { - const cu = membership[u] - sizes[cu]++ - totW[cu] += g.adj[u].length - const a = g.adj[u] - for (let k = 0; k < a.length; k++) if (membership[a[k]] === cu) inW[cu] += 1 - } - let q = 0 - if (twoM) for (let c = 0; c < count; c++) q += inW[c] / twoM - Math.pow(totW[c] / twoM, 2) - return { membership, count, sizes, modularity: twoM ? q : NaN } -} - -// Splits communities that fall apart into connected pieces. -export function splitDisconnected(g, raw) { - const n = g.n - const out = new Int32Array(n).fill(-1) - let next = 0 - for (let s0 = 0; s0 < n; s0++) { - if (out[s0] >= 0) continue - const c = raw[s0] - out[s0] = next - const stack = [s0] - while (stack.length) { - const v = stack.pop() - const a = g.adj[v] - for (let k = 0; k < a.length; k++) { - const u = a[k] - if (out[u] < 0 && raw[u] === c) { - out[u] = next - stack.push(u) - } - } - } - next++ - } - return out -} - -// page wiring, run by main.ts in the original order -export function init() { - cy.on('add remove', () => requestAnimationFrame(updateEmptyState)) - - document.getElementById('btnEmptyExample').addEventListener('click', openWelcomeExample) - - document.getElementById('btnEmpty2Example').addEventListener('click', openWelcomeExample) - - document.getElementById('btnEmpty2Db').addEventListener('click', () => switchSideTab('db')) - - document.getElementById('btnEmpty2Upload').addEventListener('click', () => { - switchSideTab('data') - document.getElementById('btnNormaAdd').click() - }) - - document.getElementById('btnEmpty2Welcome').addEventListener('click', () => switchTab('welcome')) - - document.getElementById('btnEmptyString').addEventListener('click', () => { - switchSideTab('db') - document.getElementById('stringSection').classList.remove('collapsed') - document.querySelector('#stringSection > h3').setAttribute('aria-expanded', 'true') - const q = document.getElementById('stringQuery') - q.scrollIntoView({ block: 'center', behavior: 'smooth' }) - q.focus() - }) - - document.getElementById('btnEmptyUpload').addEventListener('click', () => { - switchSideTab('data') - document.getElementById('btnNormaAdd').click() - }) - - document - .getElementById('btnEmptyHelp') - .addEventListener('click', () => switchTab('help', 'help-start')) -} diff --git a/frontend/src/wiring.ts b/frontend/src/wiring.ts deleted file mode 100644 index 864f915..0000000 --- a/frontend/src/wiring.ts +++ /dev/null @@ -1,1154 +0,0 @@ -// @ts-nocheck -// ponytail: split verbatim from the former single-file script; typed one file at a time (remove @ts-nocheck as it lands) -import { S } from './state' -import { - activeView, - nameForSelection, - openInNewView, - renderProfilerNetworkList, - renderViewBar, - selectionFromKey, - uniqueViewName, -} from './profiler' -import { buildModuleNetwork, generateDirectedMultiNetwork, mulberry32 } from './sample_data' -import { convertArena3dNetwork } from './arena3d' -import { cy } from './cy' -import { demoToNormaTexts } from './demo_downloads' -import { drawGroupHulls, resizeHullCanvas } from './hulls' -import { exportNormaAnnotation, exportNormaColors, exportNormaNetwork } from './export_norma' -import { handleNormaUploads } from './uploads' -import { libSelection, plural, setStatus } from './layouts/controls' -import { net3d } from './view3d/state' -import { refreshEnrichmentChoices } from './enrichment' -import { refreshLibraryView } from './library' -import { renderCompareList } from './label_colors' -import { setMode3d } from './view3d/tab' - -// Example files from NORMA (github.com/PavlopoulosLab/NORMA, MIT license), -// www/Examples/, embedded verbatim so they work offline. -export const NORMA_EXAMPLE_SETS = { - 'string-tp53': { - title: 'STRING: TP53 interactors', - source: 'STRING database export', - link: null, - files: [ - { - kind: 'network', - name: 'STRING TP53', - fileName: 'string_interactions.txt', - text: 'Source\tTarget\tWeight\nCDKN1A\tTP53\t70\nTP53\tMDM2\t2\nMDM4\tTP53\t3\nBCL2L1\tTP53\t4\nCHEK2\tATM\t5\nTP53\tEP300\t20\nATM\tTP53\t7\nTP53\tCREBB\t1\nMDM4\tMDM2\t1\nCHEK2\tTP53\t1\nTP53BP2\tTP53\t1\nCDKN2A\tTP53\t1\nCDKN2A\tMDM2\t1\nATM\tMDM2\t2\nEP300\tCREBBP\t1\nMDM4\tATM\t2\nCHEK2\tMDM4\t1\nCHEK2\tMDM2\t1\nCDKN2A\tCDKN1A\t1\nCDKN2A\tMDM4\t1\nEP300\tMDM2\t1\nCDKN1A\tEP300\t1\nCREBBP\tMDM2\t1\nCDKN1A\tMDM2\t1\nCDKN2A\tBCL2L1\t1\nBCL2L1\tMDM2\t2\nCDKN2A\tATM\t1\nCDKN1A\tMDM4\t1\nCDKN1A\tCHEK2\t1\nCDKN1A\tATM\t2\nCDKN1A\tCREBBP\t1\nCDKN1A\tBCL2L1\t1\nCDKN2A\tCHEK2\t1\nATM\tEP300\t1\nTP53BP2\tBCL2L1\t3\nCDKN2A\tEP300\t3\nBCL2L1\tATM\t3\nMDM4\tEP300\t1\nATM\tCREBBP\t1\nCHEK2\tEP300\t1\nTP53BP2\tMDM2\t1\nCHEK2\tBCL2L1\t1\nCDKN2A\tCREBBP\t1\nTP53BP2\tEP300\t1\nBCL2L1\tEP300\t1\nMDM4\tBCL2L1\t1\n', - }, - { - kind: 'annotation', - name: 'STRING TP53 groups', - fileName: 'string_interactions_groups_comma_duplicate.txt', - text: 'Group-1\tBCL2L1,MDM4,MDM2,CHEK2\nGroup-2\tCDKN2A,ATM,TP53BP2,MDM2\nGroup-3\tCHEK2,MDM2\nGroup-4\tTP53,EP300\nGroup-5\tMDM4,MDM2\n', - }, - { - kind: 'colors', - name: 'STRING TP53 expression', - fileName: 'string_expression_colors.txt', - text: 'CDKN1A\tblue\nTP53\tblue\nMDM4\tblue\nBCL2L1\tred\nCHEK2\tred\nATM\tred\nTP53BP2\tred\nCDKN2A\tblue\nEP300\tred\nCREBBP\tred\n', - }, - ], - }, - 'string-bcar3': { - title: 'STRING: BCAR3 interactors', - source: 'STRING database export', - link: null, - files: [ - { - kind: 'network', - name: 'STRING BCAR3', - fileName: 'BCAR3.txt', - text: 'Source\tTarget\nVCP\tNPLOC4\nBCAR1\tPXN\nVCP\tDERL2\nNSFL1C\tVCP\nCDC42\tWASL\nVCP\tDERL1\nNPLOC4\tUFD1L\nSYVN1\tVCP\nVCP\tUFD1L\nCDC42\tWAS\nCDC42\tPAK1\nCDC42\tBAIAP2\nVCP\tFAF2\nCDC42\tARHGAP1\nVIMP\tVCP\nCDC42\tPARD6A\nCDC42\tTNK2\nCDC42\tITSN1\nCDC42\tARHGDIA\nCDC42\tPARD6B\nSYVN1\tDERL2\nCDC42\tPAK2\nVIMP\tDERL1\nDERL1\tDERL2\nWAS\tBAIAP2\nCDC42\tPXN\nFAF2\tDERL2\nSYVN1\tDERL1\nUFD1L\tFAF2\nNPLOC4\tFAF2\nBCAR1\tBCAR3\nITSN1\tWASL\nBAIAP2\tWASL\nBCAR1\tTNK2\nBCAR1\tCDC42\nPAK1\tPXN\nPAK1\tARHGDIA\nFAF2\tDERL1\nNEDD9\tPXN\nPAK2\tPAK1\nVIMP\tDERL2\nSYVN1\tFAF2\nVIMP\tSYVN1\nCDC42\tBCAR3\nUFD1L\tDERL1\nPAK2\tPXN\nNSFL1C\tUFD1L\nNEDD9\tBCAR3\nPARD6B\tPARD6A\nBAIAP2\tPXN\nNSFL1C\tNPLOC4\nBCAR1\tBAIAP2\nPAK2\tPARD6A\nSYVN1\tUFD1L\nWAS\tWASL\nTULP4\tSPSB1\nSYVN1\tNPLOC4\nVIMP\tFAF2\nUFD1L\tDERL2\nNPLOC4\tDERL1\nNPLOC4\tDERL2\nITSN1\tWAS\nVIMP\tNPLOC4\nTNK2\tWAS\nNSFL1C\tFAF2\nTULP4\tKCNE4\nBCAR3\tTULP4\nBCAR3\tKCNE4\nNSFL1C\tDERL2\nSAT1\tBCAR3\nNSFL1C\tDERL1\nBCAR3\tVCP\nPAK2\tARHGDIA\nKCNE4\tTULP1\nBCAR3\tTULP1\nPXN\tWASL\nVIMP\tUFD1L\nWAS\tARHGAP1\nWAS\tPXN\nWAS\tPAK1\nBCAR3\tSPSB1\nARHGAP1\tARHGDIA\nBCAR1\tPARD6A\nPAK1\tWASL\nBCAR1\tWASL\nARHGDIA\tPXN\nBCAR1\tWAS\nPAK2\tWASL\nWAS\tPAK2\nARHGAP1\tPXN\nARHGDIA\tWASL', - }, - { - kind: 'annotation', - name: 'BCAR3 KEGG pathways', - fileName: 'BCAR3_KEGG.txt', - text: 'Protein processing in endoplasmic reticulum\tDERL1,DERL2,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP\nRegulation of actin cytoskeleton\tBAIAP2,BCAR1,CDC42,PAK1,PAK2,PXN,WAS,WASL\nChemokine signaling pathway\tBCAR1,CDC42,PAK1,PXN,WAS,WASL\nBacterial invasion of epithelial cells\tBCAR1,CDC42,PXN,WAS,WASL\nTight junction\tCDC42,PARD6A,PARD6B,WAS,WASL\nAxon guidance\tCDC42,PAK1,PAK2,PARD6A,PARD6B\nFocal adhesion\tBCAR1,CDC42,PAK1,PAK2,PXN\nEndocytosis\tCDC42,PARD6A,PARD6B,WAS,WASL\nAdherens junction\tBAIAP2,CDC42,WAS,WASL\nFc gamma R-mediated phagocytosis\tCDC42,PAK1,WAS,WASL\nRap1 signaling pathway\tBCAR1,CDC42,PARD6A,PARD6B\nHuman papillomavirus infection\tCDC42,PARD6A,PARD6B,PXN\nPathogenic Escherichia coli infection\tCDC42,WAS,WASL\nShigellosis\tCDC42,WAS,WASL\nRenal cell carcinoma\tCDC42,PAK1,PAK2\nSalmonella infection\tCDC42,WAS,WASL\nT cell receptor signaling pathway\tCDC42,PAK1,PAK2\nLeukocyte transendothelial migration\tBCAR1,CDC42,PXN\nProteoglycans in cancer\tCDC42,PAK1,PXN\nRas signaling pathway\tCDC42,PAK1,PAK2\nMAPK signaling pathway\tCDC42,PAK1,PAK2\nVEGF signaling pathway\tCDC42,PXN\nEpithelial cell signaling in Helicobacter pylori infection\tCDC42,PAK1\nErbB signaling pathway\tPAK1,PAK2\nCholine metabolism in cancer\tWAS,WASL\nNeurotrophin signaling pathway\tARHGDIA,CDC42\nHippo signaling pathway\tPARD6A,PARD6B', - }, - { - kind: 'annotation', - name: 'BCAR3 GO molecular function', - fileName: 'BCAR3_GO_MF.txt', - text: 'protein binding\tARHGAP1,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\nenzyme binding\tARHGAP1,BCAR1,BCAR3,CDC42,DERL1,FAF2,ITSN1,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS\nmolecular function regulator\tARHGAP1,ARHGDIA,BCAR3,FAF2,ITSN1,NSFL1C,PAK2,TNK2,VCP,WAS,WASL\nenzyme regulator activity\tARHGAP1,ARHGDIA,FAF2,ITSN1,NSFL1C,PAK2,TNK2,VCP,WAS,WASL\nidentical protein binding\tBAIAP2,CDC42,PAK1,PAK2,SAT1,TNK2,VCP,WAS\nprotein kinase binding\tBCAR1,CDC42,PAK1,PAK2,PARD6A,PXN,WAS\nGTPase binding\tARHGAP1,BCAR3,ITSN1,PAK1,PAK2,PARD6A,WAS\nprotein domain specific binding\tARHGAP1,BAIAP2,BCAR1,CDC42,TNK2,VCP,WAS\nubiquitin protein ligase binding\tDERL1,FAF2,NPLOC4,PXN,TNK2,VCP\nRas GTPase binding\tARHGAP1,ITSN1,PAK1,PAK2,PARD6A,WAS\nATPase binding\tDERL1,NSFL1C,SYVN1,UFD1L,VIMP\nRho GTPase binding\tITSN1,PAK1,PAK2,PARD6A,WAS\nGTPase regulator activity\tARHGAP1,ARHGDIA,TNK2,WAS,WASL\nenzyme activator activity\tARHGAP1,ARHGDIA,ITSN1,PAK2,VCP\nubiquitin-specific protease binding\tDERL1,SYVN1,VCP,VIMP\nprotein serine/threonine kinase activity\tCDC42,PAK1,PAK2,TNK2\nRac GTPase binding\tPAK1,PAK2,WAS\nubiquitin binding\tFAF2,NPLOC4,NSFL1C\nSH3 domain binding\tARHGAP1,BCAR1,WAS\nprotein binding, bridging\tARHGAP1,BAIAP2,BCAR3\n', - }, - { - kind: 'annotation', - name: 'BCAR3 GO biological process', - fileName: 'BCAR3_GO_BP.txt', - text: 'cellular process\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SPSB1,SYVN1,TNK2,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL\nbiological regulation\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\nregulation of biological process\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\nregulation of cellular process\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\nresponse to stimulus\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,NEDD9,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\nlocalization\tARHGAP1,BAIAP2,BCAR1,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,PAK2,PXN,SYVN1,TNK2,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL\ncellular response to stimulus\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,NEDD9,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS,WASL\nsignal transduction\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NEDD9,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,VCP,VIMP,WAS,WASL\nestablishment of localization\tARHGAP1,BAIAP2,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\npositive regulation of biological process\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NSFL1C,PAK1,PAK2,PARD6A,PXN,TNK2,TULP1,VCP,WAS,WASL\nregulation of response to stimulus\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL2,ITSN1,NPLOC4,PAK1,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP,WAS,WASL\ntransport\tARHGAP1,BAIAP2,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,PAK1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL\npositive regulation of cellular process\tARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NSFL1C,PAK1,PAK2,PXN,TNK2,TULP1,VCP,WAS,WASL\ncellular component organization\tBAIAP2,BCAR1,CDC42,DERL1,FAF2,ITSN1,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,TULP1,VCP,WAS,WASL\nresponse to chemical\tBAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS\ncellular localization\tARHGAP1,CDC42,DERL1,DERL2,FAF2,ITSN1,NPLOC4,NSFL1C,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL\nresponse to organic substance\tBAIAP2,BCAR1,CDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS\nnegative regulation of biological process\tARHGAP1,ARHGDIA,CDC42,DERL2,ITSN1,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,SYVN1,TNK2,UFD1L,VIMP,WAS,WASL\norganonitrogen compound metabolic process\tCDC42,DERL1,DERL2,FAF2,NPLOC4,NSFL1C,PAK1,PAK2,SAT1,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP\ncellular protein metabolic process\tCDC42,DERL1,DERL2,FAF2,NPLOC4,NSFL1C,PAK1,PAK2,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP\nestablishment of localization in cell\tARHGAP1,CDC42,DERL1,DERL2,FAF2,ITSN1,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP,WAS,WASL\ncellular response to organic substance\tBAIAP2,BCAR1,CDC42,DERL1,DERL2,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS\nregulation of signal transduction\tARHGAP1,ARHGDIA,BAIAP2,BCAR3,CDC42,ITSN1,NPLOC4,PAK1,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP\nresponse to stress\tCDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS\nnegative regulation of cellular process\tARHGAP1,ARHGDIA,CDC42,DERL2,ITSN1,NPLOC4,PAK1,PAK2,PARD6A,SYVN1,UFD1L,VIMP,WAS,WASL\nprotein localization\tARHGAP1,BAIAP2,DERL1,DERL2,FAF2,NPLOC4,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WASL\ncell surface receptor signaling pathway\tARHGDIA,BAIAP2,BCAR1,CDC42,ITSN1,NEDD9,PAK1,PAK2,PARD6A,PXN,TNK2,WAS,WASL\nregulation of cellular component organization\tARHGDIA,BAIAP2,BCAR1,CDC42,DERL2,NSFL1C,PAK1,PXN,TNK2,TULP1,VCP,WAS,WASL\nregulation of localization\tARHGAP1,BCAR1,DERL2,KCNE4,NSFL1C,PAK1,PARD6A,PARD6B,TNK2,TULP1,VIMP,WAS,WASL\nregulation of molecular function\tARHGAP1,ARHGDIA,BCAR3,DERL1,FAF2,ITSN1,NSFL1C,PAK1,PAK2,TNK2,VCP,WAS,WASL\nresponse to organonitrogen compound\tBAIAP2,BCAR1,DERL1,DERL2,FAF2,NPLOC4,PAK1,PXN,SYVN1,UFD1L,VCP,VIMP\nintracellular transport\tARHGAP1,CDC42,DERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP,WAS,WASL\ncellular protein modification process\tCDC42,DERL1,DERL2,PAK1,PAK2,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP\norganelle organization\tBAIAP2,BCAR1,CDC42,FAF2,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,WAS,WASL\ncellular protein localization\tDERL1,DERL2,FAF2,NPLOC4,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WASL\nvesicle-mediated transport\tARHGAP1,BAIAP2,CDC42,FAF2,ITSN1,PAK1,TNK2,TULP1,VCP,WAS,WASL\npositive regulation of response to stimulus\tARHGAP1,BAIAP2,BCAR1,BCAR3,CDC42,ITSN1,PAK1,PAK2,VCP,WAS,WASL\nregulation of catalytic activity\tARHGAP1,ARHGDIA,FAF2,ITSN1,NSFL1C,PAK1,PAK2,TNK2,VCP,WAS,WASL\ncellular component assembly\tBAIAP2,CDC42,DERL1,NEDD9,NSFL1C,PARD6A,PARD6B,PXN,VCP,WAS,WASL\nenzyme linked receptor protein signaling pathway\tBAIAP2,BCAR1,CDC42,ITSN1,PAK1,PAK2,PARD6A,PXN,TNK2,WASL\nregulation of immune response\tBAIAP2,BCAR1,CDC42,NPLOC4,PAK1,PAK2,UFD1L,VIMP,WAS,WASL\ncytoskeleton organization\tBAIAP2,BCAR1,CDC42,NEDD9,NSFL1C,PAK1,PAK2,PARD6A,WAS,WASL\norganonitrogen compound catabolic process\tDERL1,DERL2,FAF2,NPLOC4,NSFL1C,SAT1,SYVN1,UFD1L,VCP,VIMP\npositive regulation of cellular component organization\tARHGDIA,BAIAP2,CDC42,NSFL1C,PAK1,PXN,TULP1,VCP,WAS,WASL\ncellular response to stress\tDERL1,DERL2,FAF2,NPLOC4,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP\ncellular catabolic process\tDERL1,DERL2,FAF2,NPLOC4,NSFL1C,SAT1,SYVN1,UFD1L,VCP,VIMP\nregulation of intracellular signal transduction\tARHGAP1,ARHGDIA,CDC42,ITSN1,NPLOC4,PAK1,PAK2,SYVN1,UFD1L,VIMP\nmulti-organism process\tBAIAP2,CDC42,DERL1,DERL2,ITSN1,PAK2,PARD6A,VCP,VIMP,WASL\nimmune system process\tBAIAP2,BCAR1,CDC42,FAF2,PAK1,PAK2,TNK2,VCP,WAS,WASL\nproteasome-mediated ubiquitin-dependent protein catabolic process\tDERL1,DERL2,FAF2,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP\ntransmembrane receptor protein tyrosine kinase signaling pathway\tBAIAP2,BCAR1,CDC42,ITSN1,PAK1,PAK2,PXN,TNK2,WASL\nimport into cell\tARHGAP1,BAIAP2,CDC42,ITSN1,PAK1,TNK2,TULP1,WAS,WASL\nprotein transport\tARHGAP1,DERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP\nregulation of cellular protein metabolic process\tBCAR3,DERL1,DERL2,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP\nretrograde protein transport, ER to cytosol\tDERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP\nsmall GTPase mediated signal transduction\tARHGAP1,BCAR3,CDC42,ITSN1,PAK1,PAK2,TNK2,WAS\nactin cytoskeleton organization\tBAIAP2,BCAR1,CDC42,NEDD9,PAK1,PAK2,WAS,WASL\nregulation of cytoskeleton organization\tARHGDIA,BAIAP2,CDC42,NSFL1C,PAK1,PXN,WAS,WASL\nendocytosis\tBAIAP2,CDC42,ITSN1,PAK1,TNK2,TULP1,WAS,WASL\nprotein modification by small protein conjugation or removal\tCDC42,DERL1,SPSB1,SYVN1,TULP4,UFD1L,VCP,VIMP\ngeneration of neurons\tARHGDIA,BAIAP2,CDC42,PAK1,PAK2,PARD6B,TULP1,WASL\nregulation of apoptotic process\tARHGDIA,BCAR1,ITSN1,PAK1,PAK2,SYVN1,VCP,VIMP\nregulation of phosphate metabolic process\tBCAR3,ITSN1,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP\nregulation of transport\tARHGAP1,DERL2,KCNE4,PAK1,TNK2,TULP1,VIMP,WASL\nregulation of protein modification process\tBCAR3,DERL1,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP\nanatomical structure morphogenesis\tBAIAP2,BCAR3,CDC42,PAK1,PARD6B,SAT1,TULP1,WASL\nubiquitin-dependent ERAD pathway\tDERL1,DERL2,FAF2,NPLOC4,SYVN1,VCP,VIMP\nresponse to topologically incorrect protein\tDERL1,DERL2,FAF2,SYVN1,UFD1L,VCP,VIMP\npositive regulation of cytoskeleton organization\tBAIAP2,CDC42,NSFL1C,PAK1,PXN,WAS,WASL\nimmune response-activating cell surface receptor signaling pathway\tBAIAP2,BCAR1,CDC42,PAK1,PAK2,WAS,WASL\nregulation of actin cytoskeleton organization\tARHGDIA,BAIAP2,CDC42,PAK1,PXN,WAS,WASL\npositive regulation of cellular component biogenesis\tBAIAP2,CDC42,PAK1,PXN,VCP,WAS,WASL\nneuron projection development\tBAIAP2,CDC42,PAK1,PAK2,PARD6B,TULP1,WASL\nprotein ubiquitination\tCDC42,DERL1,SPSB1,SYVN1,TULP4,VCP,VIMP\nimmune effector process\tBAIAP2,CDC42,FAF2,PAK1,VCP,WAS,WASL\nregulation of response to stress\tDERL2,NPLOC4,PAK1,PAK2,SYVN1,UFD1L,VIMP\nmovement of cell or subcellular component\tBCAR1,CDC42,PAK2,PXN,TNK2,WAS,WASL\nregulation of phosphorylation\tBCAR3,ITSN1,PAK1,PAK2,PARD6A,TNK2,VCP\npositive regulation of signal transduction\tARHGAP1,BAIAP2,BCAR3,ITSN1,PAK1,PAK2,VCP\nFc receptor signaling pathway\tBAIAP2,CDC42,PAK1,PAK2,WAS,WASL\ncellular response to topologically incorrect protein\tDERL1,DERL2,SYVN1,UFD1L,VCP,VIMP\nresponse to unfolded protein\tDERL1,DERL2,FAF2,SYVN1,VCP,VIMP\npositive regulation of supramolecular fiber organization\tBAIAP2,CDC42,PAK1,PXN,WAS,WASL\nphagocytosis\tBAIAP2,CDC42,PAK1,TULP1,WAS,WASL\nactin filament organization\tBAIAP2,BCAR1,CDC42,NEDD9,WAS,WASL\nregulation of actin filament organization\tBAIAP2,CDC42,PAK1,PXN,WAS,WASL\nregulation of protein complex assembly\tBAIAP2,CDC42,PAK1,VCP,WAS,WASL\nviral process\tCDC42,DERL1,ITSN1,PAK2,PARD6A,VCP\nregulation of plasma membrane bounded cell projection organization\tARHGDIA,BAIAP2,CDC42,PAK1,WAS,WASL\nregulation of growth\tBCAR1,CDC42,DERL2,NEDD9,PAK1,PAK2\npositive regulation of phosphorylation\tBCAR3,ITSN1,PAK1,PAK2,TNK2,VCP\ncellular response to endogenous stimulus\tBAIAP2,BCAR1,PAK1,PARD6A,PXN,VIMP\npositive regulation of protein modification process\tBCAR3,DERL1,PAK1,PAK2,TNK2,VCP\nregulation of hydrolase activity\tARHGAP1,ARHGDIA,NSFL1C,PAK2,TNK2,VCP\nvascular endothelial growth factor receptor signaling pathway\tBAIAP2,BCAR1,CDC42,PAK2,PXN\nFc-gamma receptor signaling pathway involved in phagocytosis\tBAIAP2,CDC42,PAK1,WAS,WASL\ndendrite development\tBAIAP2,CDC42,PAK2,TULP1,WASL\nendoplasmic reticulum unfolded protein response\tDERL1,DERL2,SYVN1,VCP,VIMP\nestablishment or maintenance of cell polarity\tCDC42,NSFL1C,PAK1,PARD6A,PARD6B\n', - }, - ], - }, - tau: { - title: 'Drosophila Tau network', - source: 'PMID 31488613, PMCID PMC6794924', - link: 'https://doi.org/10.1523/JNEUROSCI.0391-19.2019', - files: [ - { - kind: 'network', - name: 'Drosophila Tau', - fileName: 'TAU_network_DEGs_NORMA.txt', - text: "Source\tTarget\nCG6178\tyip2\nshep\tScpX\nnito\tRef1\nbol\tHrb27C\nLar\tAbl\nLar\tCadN\nCG31715\tcpb\nMapmodulin\tAats-ile\nCG5590\tCG17597\nSc2\tOst48\nfrj\tCG10932\nKap-alpha3\temb\nUch\tUsp5\nPrx2540-1\tPrx2540-2\nCG2082\tCG2091\nlig\tCapr\nlig\trin\nclu\tDp1\nclu\teIF3-S10\nclu\trin\nclu\teIF3-S8\nclu\teIF3-S9\nCG9090\tCyt-c-p\nCG9090\tCOX5A\nAce\tCat\nDbp80\temb\nSsadh\tCG33791\nSsadh\tCG32026\nSsadh\tMdh2\nCG4572\tCG10576\nCG4572\tHsc70-3\nPlexA\talpha-Cat\nl(1)G0156\tCG11876\nl(1)G0156\tSsadh\nl(1)G0156\tCG33791\nPlc21C\tGbeta76C\nPlc21C\tPkc53E\nHrb27C\tpoe\nHrb27C\theph\nHrb27C\tPyK\nHrb27C\tbel\nHrb27C\tme31B\nHrb27C\tpAbp\nHrb27C\tTER94\nKhc\tTAU\nCG32026\tCG33791\nbel\tRef1\nbel\tpoe\nbel\temb\nbel\tvig\nbel\tme31B\nDp1\tCG2918\nSgt\tTrap1\nSgt\tTcp-1zeta\nMen-b\tCG11876\nMen-b\tPyK\nMen-b\tMdh2\nCG11899\tCG3011\nMgstl\tCG3529\nMgstl\tOst48\nCG14715\tCG2852\nLpin\tPkc53E\ngammaSnap2\tzetaCOP\ngammaSnap2\tbetaCOP\ngammaSnap2\tepsilonCOP\ngammaSnap2\tbeta'COP\ngammaSnap2\tdeltaCOP\nCG10359\tGp93\nCG10359\tCG1354\nCG10359\tCaBP1\nCG10359\tPdi\nKP78a\tCG32683\nKP78a\tArr2\nCG9257\tpoe\nCG9257\tCG3529\nEct3\tRpn5\nEct3\tCG6891\nEct3\tCat\nEct3\tCalpB\nCG4882\tbonsai\nCalpB\tCG6891\nCalpB\tCat\npar-1\tCG32683\npar-1\tKP78a\npar-1\tArr2\nPrx5\tPrx2540-2\nPrx5\tCat\nPkc53E\tzip\nCG31064\tRab5\nNlp\tmago\nNlp\teEF1delta\nNlp\tsmt3\nCG10186\tbetaCOP\nCG10186\tcpb\nCG10186\tcpa\nCG10186\tbeta'COP\nCG10186\tbai\nCG10186\tepsilonCOP\nCG10186\tDlic\nCG10186\tp115\nCG10186\tzetaCOP\nCG10186\tdeltaCOP\nrin\tCapr\nLpR1\tArr2\nLpR1\tArr1\nLpR1\tUbqn\nLpR1\tCG32683\nLpR1\tlqf\nLpR1\tHrs\nLpR1\tLpR2\nLpR1\tRab5\nCG16935\tCG12262\nCG16935\tyip2\npoe\tbetaCOP\npoe\tCG3529\nCG43367\tbetaCOP\nCG43367\tpoe\nRab39\tRab5\nRab39\tGdi\nemb\tRae1\nemb\talphaCOP\nemb\tCG10576\nAGBE\tEno\nAGBE\tPyK\nLpR2\tUbqn\nLpR2\tCG32683\nLpR2\tArr2\nLpR2\tHrs\nLpR2\tlqf\nLpR2\tRab5\nCG3529\tOst48\nOst48\tCG2918\nHsp60C\tHop\nHsp60C\tHsc70-3\nPrx2540-2\tCat\nEfTuM\tRpS14a\nEfTuM\tHsc70-5\nEfTuM\tRpL10Ab\nEfTuM\tRpS13\nEfTuM\tRpS16\nEfTuM\tRpS23\nEfTuM\tRpS15Aa\nEfTuM\tRpL4\nEfTuM\tCG11876\nEfTuM\tbonsai\nEfTuM\tmRpS10\nEfTuM\tRpL12\nEfTuM\tsta\nEfTuM\tRpL3\nEfTuM\tHsp60\nEfTuM\tRpS18\nEfTuM\tRpS2\nEfTuM\tmRpS9\nEfTuM\tRpS3\nEfTuM\tmRpL12\nEfTuM\tRpL11\nfne\tTango4\nfne\tSrp54\nfne\tCG10777\nfne\tPrp8\nfne\tmago\nfne\tCG16941\nfne\typs\nfne\tCG10077\nfne\tRbp9\nfne\tU2A\nfne\tPkc53E\nfne\tHrb98DE\nfne\tLS2\nfne\tSF2\nfne\tmub\nfne\theph\nKlp10A\tdeltaCOP\nMtl\tzip\nMtl\ttsr\nMtl\tRhoGDI\nHsp60\tCG7033\nRanBPM\tCG6617\nyps\tTango4\nyps\tLS2\nyps\tmago\nyps\tCG10777\nArt4\tCG10777\nArt4\tCG10077\nArt4\tpAbp\nImpL3\tCat\nImpL3\tMen-b\nImpL3\tPfk\nImpL3\tMen\nImpL3\tEno\nImpL3\tTpi\nImpL3\tPyK\nImpL3\tCG11876\nCG10077\tCG10777\nCG10077\tLS2\nCG10077\tTango4\nCG10077\typs\nCG10077\tmago\nLam\tRab5\nLamC\tLam\nUbqn\tHDAC6\nUbqn\tCG32683\nmub\tCG3800\nmub\tmago\nmub\tU2A\nmub\tSrp54\nmub\tTango4\nmub\tPrp8\nmub\tCG10777\nmub\tCG16941\nmub\typs\nmub\tCG10077\nmub\tLS2\nmub\tHrb98DE\nmub\tRbp9\nmub\tSF2\nAGO1\tRbp9\nAGO1\tpAbp\nAGO1\tRae1\nAGO1\tme31B\nglo\tHrb98DE\nSap-r\tCG3529\nSap-r\tMgstl\nSap-r\tpins\nSap-r\tRab5\nSap-r\tOst48\nNdg\tCg25C\nNdg\tLanB2\ntrol\tCp1\ntrol\tLanB2\ntrol\tCg25C\ntrol\tNdg\nHsp70Ab\tPdi\nHsp70Ab\tDnaJ-H\nHsp70Ab\tHop\nHsp70Ab\tHsp27\nbai\tSrp19\nbai\tCG2852\nbai\tgammaSnap2\nbai\tKlp10A\nbai\tKlc\nbai\tDlic\nbai\tcpa\nbai\tp115\nbai\tcpb\nbai\tdeltaCOP\nbai\tbetaCOP\nbai\tepsilonCOP\nbai\tbeta'COP\nbai\tzetaCOP\nNc73EF\tSsadh\nNc73EF\tCG32026\nNc73EF\tl(1)G0156\nNc73EF\tCG33791\nNc73EF\tCG11876\nSrp54\typs\nSrp54\tCG10777\nSrp54\tmago\nSrp54\tLS2\nSrp54\tCG10077\nSrp54\tTango4\nUgt\tCG2918\nRbp9\tCG10777\nRbp9\tTango4\nRbp9\tSrp54\nRbp9\tPrp8\nRbp9\tCG10077\nRbp9\tCG16941\nRbp9\tU2A\nRbp9\tPkc53E\nRbp9\tLS2\nRbp9\tHrb98DE\nRbp9\typs\nRbp9\tmago\nAct79B\tcpb\nAct79B\tflr\nAct79B\tzip\nRbp2\tpAbp\nRbp2\tRpL10Ab\nRbp2\tRpS4\nRbp2\tRpL18A\nRbp2\tRpS3\nRbp2\tRpS11\nRbp2\teIF3-S9\nRbp2\tRpL11\nRbp2\teIF3-S10\nRbp2\tRpL13\nRbp2\teIF3-S8\nRbp2\tRpS10b\nRbp2\tRpS10a\nRbp2\tRpS27\nRbp2\tRpL3\nRbp2\tRpS12\nRbp2\tRpS7\nRbp2\tRpL12\nRbp2\tTango7\nRbp2\tRpS23\nRbp2\tRpS13\nRbp2\tsta\nRbp2\tRpS18\nRbp2\tRpS16\nRbp2\tRpL4\nRbp2\tRpS6\nRbp2\tTrip1\nRbp2\tRpS2\nRbp2\teIF-3p66\nRbp2\tCG5642\nRbp2\tCG9769\nIswi\tHis1\nIswi\tBap55\nIswi\tsmt3\nIswi\tmor\nVps60\tHrs\neEF1delta\teIF3-S10\neEF1delta\tRpS10b\neEF1delta\tRpS13\neEF1delta\tRpS6\neEF1delta\tRpS16\neEF1delta\tRpL12\neEF1delta\tsta\neEF1delta\tRpS2\neEF1delta\tCG5642\neEF1delta\tRpL13\neEF1delta\tTrip1\nCG10576\tRpS6\nCG10576\tRpS4\nCG10576\tCapr\nCG10576\tsta\nCG10576\tRpS16\nCG10576\tTcp-1zeta\nCG10576\twuho\nCG10576\tRpL12\nCG10576\tCG7033\nAbl\tDscam1\nAbl\tsyd\nAbl\talpha-Cat\nAbl\tarm\nRae1\tmago\nMtor\tmago\nMtor\tAGO1\nMtor\tHsc70-3\nMtor\tHsc70-5\nMtor\tsmt3\nMtor\tRae1\nRef1\tLS2\nRef1\tmago\nTrap1\tHsp70Ab\nTrap1\tHop\nTrap1\tHsp60C\nTrap1\tHsp60\nTrap1\tCG2918\nTrap1\tHsc70-3\nTrap1\tCG1416\nCG17597\tCat\nCG17597\tScpX\nCG17597\tMfe2\nlqf\tCG32683\nlqf\tUbqn\nlqf\tArr2\nlqf\tRab5\nlqf\tHrs\nTim17b\tTim9a\nCG3902\tCG10932\nCat\tCG6891\np115\tdeltaCOP\nSec24AB\tdeltaCOP\nSec24AB\tbetaCOP\nSec24AB\tepsilonCOP\nSec24AB\tzetaCOP\nSec24AB\talphaCOP\nSec24AB\tbeta'COP\nSec24AB\tp115\nninaC\tAct79B\nninaC\tzip\nninaC\tArr2\nmago\tsta\nmago\tRpS6\nmago\tCG10777\nCG11876\tMen\nCG11876\tMdh2\nCG11876\tPyK\nCG11876\tND-PDSW\nCG11876\tAcCoAS\nCG11876\tEno\nCG11876\tCG33791\nPdk\tade3\nPdk\tCG11876\nB52\tRpS3\nB52\tHrb27C\nB52\tglo\nB52\tCG10777\nB52\tfne\nB52\tCG10077\nB52\typs\nB52\tRbp9\nB52\tCG16941\nB52\tTango4\nB52\tU2A\nB52\tmago\nB52\tSrp54\nB52\tmub\nB52\theph\nB52\tPrp8\nB52\tLS2\nB52\tHrb98DE\nB52\tU2af50\nB52\tSF2\nVha100-1\tsun\nVha100-1\tVha100-2\nVha100-1\tVha68-2\nrobl\tDlic\nHsc70-5\tCG7033\nHsc70-5\tT-cp1\nHsc70-5\tTpi\nHsc70-5\tDnaJ-H\nHsc70-5\tTcp-1eta\nHsc70-5\tHop\nHsc70-5\tRae1\nHsc70-5\tTrap1\nHsc70-5\tHsp60C\nHsc70-5\tHsc70-3\nHsc70-5\tHsp60\nHsc70-5\tGp93\ntsr\tsqh\ntsr\tcpa\ntsr\tRpS6\nflr\tcpa\nflr\ttsr\nAcCoAS\tCG12262\nAcCoAS\tCG10932\nACC\tEno\nACC\tCG10932\nACC\tAcCoAS\nLS2\tmago\nLS2\tCG10777\nFKBP59\tninaC\nFKBP59\tDlic\nFKBP59\tcpb\nFKBP59\tcpa\nFKBP59\tHop\nU2af50\tRef1\nU2af50\tLS2\nU2af50\typs\nU2af50\tCG10777\nU2af50\tCG10077\nU2af50\tRbp9\nU2af50\tfne\nU2af50\tSrp54\nU2af50\tmago\nU2af50\tmub\nU2af50\tTango4\nU2af50\theph\nU2af50\tCG16941\nU2af50\tHrb98DE\nU2af50\tPrp8\nU2af50\tSF2\nU2af50\tU2A\nFmr1\tdock\nFmr1\tPyK\nFmr1\tpoe\nFmr1\tbel\nFmr1\tpAbp\nFmr1\teIF4G\nFmr1\tDscam1\nFmr1\tRanBPM\nFmr1\tHrb98DE\nFmr1\tHrb27C\nFmr1\tCapr\nFmr1\tTER94\nFmr1\tRbp9\nFmr1\tme31B\nFmr1\tAGO1\nFmr1\tvig\nScpX\tCat\nScpX\tMfe2\nTop2\tRpS13\nTop2\tPkc53E\nTop2\tUba2\nTop2\tIswi\nTop2\tsmt3\nCG8209\tUfd1-like\nheph\tTango4\nheph\tSrp54\nheph\tCG16941\nheph\tCG10077\nheph\tCG10777\nheph\tU2A\nheph\tmago\nheph\typs\nheph\tPrp8\nheph\tmub\nheph\tLS2\nheph\tpAbp\nheph\tHrb98DE\nheph\tRbp9\nheph\tSF2\nHrs\tCG4572\nHrs\tCG32683\nHrs\tArr2\nHrs\tUbqn\nHrs\tRab5\nHrb98DE\tCG10777\nHrb98DE\tmago\nHrb98DE\tSrp54\nHrb98DE\tCG16941\nHrb98DE\tCG10077\nHrb98DE\typs\nHrb98DE\tLS2\nHrb98DE\tTango4\nbic\tRpS3\nNacalpha\tRpS13\nNacalpha\tRpS2\nNacalpha\tsta\nNacalpha\tRpS6\nNacalpha\tRpS16\nNacalpha\tRpS27\nNacalpha\tRpS4\nNacalpha\tRpS23\nNacalpha\tRpS18\nNacalpha\tRpL12\nNacalpha\tRpL18A\nNacalpha\tRpL11\nNacalpha\tRpS10b\nNacalpha\tRpS17\nNacalpha\tRpL13\nNacalpha\tRpS3\nNacalpha\tRpL4\nNacalpha\tbic\nAats-val\teEF1delta\nAats-val\tEf1beta\nAats-val\tAats-asp\nAats-val\tAats-ile\nAats-val\tCG33123\nAats-val\tAats-arg\nBicD\tLam\nBicD\tbic\nBicD\tKhc\nBicD\tcpb\nBicD\tcpa\nBicD\tDlic\nDnaJ-H\tCG2918\nDnaJ-H\tHsc70-3\nDnaJ-H\tHop\nmRpS34\tCG4882\nmRpS34\tbonsai\nCadN\talpha-Cat\nCadN\tarm\nvig\tRpS15Aa\nvig\tRpS13\nvig\tRpS16\nvig\tRpS17\nvig\tRpL12\nTudor-SN\tbel\nTudor-SN\tAGO1\nTudor-SN\tFmr1\nTudor-SN\tvig\nMdh2\tAldh\nMdh2\tND-51\nMdh2\tCOX4\nMdh2\tCyt-c-p\nMdh2\tCOX5A\nMdh2\tMen\nMdh2\tEno\nCyt-c-p\tCat\nCyt-c-p\tporin\nCyt-c-p\tND-PDSW\nCyt-c-p\tCOX6B\nAldh\tCat\nAldh\tEno\nAldh\tCG12262\nAldh\tAcCoAS\nCG15093\tCG12262\nCG15093\tAcCoAS\nCG15093\tCG3902\nCG15093\tAldh\nCG4679\tmRpS34\nCG4679\tbsf\nCG4679\tCG4882\nCG4679\tmRpL12\nCG4679\tmRpL44\nCG4679\tbonsai\nCG4679\tmRpS10\nCG4679\tmRpS23\nCG4679\tmRpS22\nCG4679\tmRpS9\nCG4679\tmRpS18B\nSF2\tCG10777\nSF2\tCG10077\nSF2\tU2A\nSF2\tTango4\nSF2\typs\nSF2\tPrp8\nSF2\tRbp9\nSF2\tCG16941\nSF2\tSrp54\nSF2\tRef1\nSF2\tLS2\nSF2\tmago\nSF2\tHrb98DE\np47\tCG8209\np47\tUfd1-like\np47\tRpt2\nRad23\tme31B\nRad23\tUch-L5\nRad23\tRpt1\nRad23\tHDAC6\nRad23\tUsp5\nRad23\tUba2\nRad23\tUfd1-like\nRad23\tSgt\nRad23\tRpt5\nRad23\tBap55\nRad23\tpont\nRad23\tCG8209\nRad23\tIswi\nRad23\tRpn13\nRad23\tUbqn\nRad23\tsmt3\nRad23\tRpt2\nRad23\tTER94\nme31B\tpoe\nme31B\tCG2091\nme31B\typs\nND-51L1\tND-51\nND-51L1\tNP15.6\nND-51L1\tND-PDSW\nND-51L1\tND-B16.6\nsyd\talpha-Cat\nsyd\tDlic\nsyd\tKlc\nArr2\tUbqn\nArr2\tCG32683\nArr1\tninaC\nArr1\tchp\nArr1\tKP78a\nArr1\tLpR2\nArr1\tUbqn\nArr1\tCG32683\nArr1\tpar-1\nArr1\tHrs\nArr1\tlqf\nArr1\tRab5\nArr1\tArr2\nND-51\tND-B16.6\nND-51\tND-PDSW\nCG16941\typs\nCG16941\tmago\nCG16941\tCG10777\nCG16941\tCG10077\nCG16941\tSrp54\nCG16941\tLS2\nCG16941\tTango4\nCnx99A\tOst48\nCnx99A\tHsp70Ab\nCnx99A\tCG2918\nCnx99A\tUgt\nCnx99A\tGp93\nCnx99A\tPdi\nCnx99A\tHsc70-3\nCnx99A\tERp60\nAats-his\tAats-asp\nAats-his\tCct5\nAats-his\tAats-arg\nAats-his\tAats-val\nAats-his\tCG33123\nAats-his\tAats-ile\nmRpS35\tRpS2\nmRpS35\tCG4882\nmRpS35\tmRpL12\nmRpS35\tmRpS34\nmRpS35\tmRpL44\nmRpS35\tCG4679\nmRpS35\tmRpS23\nmRpS35\tmRpS18B\nmRpS35\tmRpS10\nmRpS35\tmRpS9\nmRpS35\tmRpS22\nmRpS35\tbonsai\nmRpS35\tmRpS5\nVha68-2\tsun\nVha68-2\tEno\nVha100-2\tsun\nVha100-2\tVha68-2\nRac1\tsqh\nRac1\tFmr1\nRac1\ttsr\nRac1\tDscam2\nRac1\tGdi\nRac1\tAbl\nRac1\tdock\nRac1\tzip\nRac1\tRhoGDI\nmRpS7\tade3\nmRpS7\tCG9769\nmRpS7\tCG4882\nmRpS7\teIF3-S8\nmRpS7\tSrp19\nmRpS7\tEfTuM\nmRpS7\teIF3-S10\nmRpS7\teIF-2gamma\nmRpS7\tSrp54k\nmRpS7\teRF1\nmRpS7\tTango7\nmRpS7\tRpLP2\nmRpS7\teIF3-S9\nmRpS7\tRpL4\nmRpS7\tRpS10a\nmRpS7\tRpS12\nmRpS7\tRpS10b\nmRpS7\tCG7048\nmRpS7\tRpS15Ab\nmRpS7\tRpS15Aa\nmRpS7\tRpL10Ab\nmRpS7\tRpS14a\nmRpS7\tRpL18A\nmRpS7\tRack1\nmRpS7\tRpS4\nmRpS7\tRpS17\nmRpS7\tRpL13\nmRpS7\tRpS7\nmRpS7\tRpS6\nmRpS7\tRpS8\nmRpS7\tRpS16\nmRpS7\tsta\nmRpS7\tRpS13\nmRpS7\tRpL3\nmRpS7\tRpS23\nmRpS7\tRpS27\nmRpS7\tRpL12\nmRpS7\tRpS18\nmRpS7\tRpS3A\nmRpS7\tRpS2\nmRpS7\tRpS3\nmRpS7\tRpS11\nmRpS7\tRpL11\nmRpS7\tRpL30\nmRpS7\tmRpL44\nmRpS7\tmRpS22\nmRpS7\tmRpS34\nmRpS7\tCG4679\nmRpS7\tmRpS23\nmRpS7\tbonsai\nmRpS7\tmRpS10\nmRpS7\tmRpS35\nmRpS7\tmRpL12\nmRpS7\tmRpS18B\nmRpS7\tmRpS9\nmRpS7\tmRpS5\nPrp8\typs\nPrp8\tSrp54\nPrp8\tCG10777\nPrp8\tCG10077\nPrp8\tLS2\nPrp8\tmago\nPrp8\tHrb98DE\nPrp8\tCG16941\nPrp8\tTango4\npont\tTcp-1zeta\npont\tTcp-1eta\npont\tRpt6\npont\tUch-L5\nmRpL44\tmRpS34\nmRpL44\tmRpL12\nmRpL44\tbonsai\nmRpS22\tCG4882\nmRpS22\tmRpL12\nmRpS22\tmRpS10\nmRpS22\tmRpL44\nmRpS22\tmRpS34\nmRpS22\tmRpS23\nmRpS22\tbonsai\nmRpS22\tmRpS18B\nmRpS22\tmRpS9\nCp1\tEct3\nCp1\tPyK\nCp1\tRpn5\nCp1\tCG6891\nCp1\tCalpB\nCp1\tRpn6\nCp1\tCat\nCp1\tRpt5\ncathD\tCG4572\ncathD\tCG6891\ncathD\tPyK\ncathD\tCG8258\ncathD\tEct3\ncathD\tPfk\ncathD\tRpn5\ncathD\tCalpB\ncathD\tRpt5\ncathD\tRpn6\ncathD\tCat\ncathD\tSap-r\ncathD\tCp1\nTango4\tCG10777\nTango4\tLS2\nTango4\tmago\nU2A\typs\nU2A\tCG10077\nU2A\tCG10777\nU2A\tSrp54\nU2A\tHrb98DE\nU2A\tmago\nU2A\tLS2\nU2A\tCG16941\nU2A\tPrp8\nU2A\tTango4\nUba2\tUsp5\nUba2\tUbqn\nsmt3\ttsr\nsmt3\tmago\nsmt3\tUfd1-like\nsmt3\tCG10777\nsmt3\tCG10077\nsmt3\tRae1\nsmt3\tUba2\nzip\tsqh\nND-15\tCG11752\nND-15\tND-51L1\nND-15\tCOX5A\nND-15\tND-51\nND-15\tNP15.6\nND-15\tND-B16.6\nND-15\tND-PDSW\npAbp\trin\npAbp\tRpL3\npAbp\tRpL4\npAbp\temb\npAbp\tDp1\npAbp\tRpS6\npAbp\ttyf\npAbp\tbel\npAbp\typs\npAbp\tCG11505\npAbp\tme31B\neIF4G\tDp1\neIF4G\tkra\neIF4G\tbel\neIF4G\tPlexA\neIF4G\tme31B\neIF4G\tRpL18A\neIF4G\tRpL12\neIF4G\tRpS12\neIF4G\tHsp67Bc\neIF4G\tRpS4\neIF4G\tRpL11\neIF4G\tRpS23\neIF4G\tRpS7\neIF4G\tRpS27\neIF4G\tRpLP2\neIF4G\tRpS10b\neIF4G\tRpS10a\neIF4G\tsta\neIF4G\tRpS16\neIF4G\tRpL4\neIF4G\tRpL13\neIF4G\tRpS18\neIF4G\tRpS13\neIF4G\tRpL3\neIF4G\tRpL10Ab\neIF4G\tRpS11\neIF4G\tRpS8\neIF4G\tRpS2\neIF4G\tHsp27\neIF4G\teIF-3p66\neIF4G\tmago\neIF4G\tTango7\neIF4G\tRpS3\neIF4G\tCG5642\neIF4G\tCG9769\neIF4G\tTrip1\neIF4G\tRbp2\neIF4G\tRpS6\neIF4G\talpha-Cat\neIF4G\teIF3-S10\neIF4G\teIF3-S8\neIF4G\teIF3-S9\neIF4G\tpAbp\nPfk\tCG11876\nPfk\tCG6891\nPfk\tEct3\nPfk\tRpt5\nPfk\tCp1\nPfk\tRpn6\nPfk\tCalpB\nPfk\tRpn5\nPfk\tCat\nPfk\tEno\nPfk\tTpi\nPfk\tPyK\nbonsai\tRpS14a\nbonsai\tRpS16\nbonsai\tsta\nbonsai\tRpS6\nCG12262\tCG10932\nyip2\tUch\nyip2\tAcCoAS\nyip2\tCG10932\nyip2\tCG3902\nyip2\tCG12262\nRab5\tMgstl\nRab5\tCG32683\nRab5\tOst48\nRab5\tCG3529\nRab5\tArr2\nGdi\tCat\nGdi\tCG6891\nGdi\tPfk\nGdi\tRpt1\nGdi\tRpt5\nGdi\tRpn5\nGdi\tCG8258\nGdi\tRpn6\nGdi\tRpn3\nGdi\tPyK\nGdi\tTER94\nGdi\tRhoGDI\nGdi\tRab5\nmRpS10\tSrp54k\nmRpS10\tCG4882\nmRpS10\tRpL10Ab\nmRpS10\tRpS14a\nmRpS10\tRpS16\nmRpS10\tRpS10b\nmRpS10\tRpS13\nmRpS10\tRpS2\nmRpS10\tRpS15Aa\nmRpS10\tRpL3\nmRpS10\tRpL12\nmRpS10\tRpL13\nmRpS10\tRpS4\nmRpS10\tRpS17\nmRpS10\tRpS6\nmRpS10\tsta\nmRpS10\tmRpS23\nmRpS10\tmRpS18B\nmRpS10\tmRpS34\nmRpS10\tmRpL12\nmRpS10\tmRpL44\nmRpS10\tbonsai\nmRpS10\tmRpS9\nSrp54k\tbonsai\nSrp54k\tRpS14a\nSrp54k\tRpS15Aa\nSrp54k\tRpS17\nSrp54k\tRpL10Ab\nSrp54k\tRpS10b\nSrp54k\tRpL12\nSrp54k\tRpS4\nSrp54k\tRpS6\nSrp54k\tRpS16\nSrp54k\tRpS12\nSrp54k\tsta\nSrp54k\tSrp19\nmRpL12\tRpS14a\nmRpL12\tRpL10Ab\nmRpL12\tRpS16\nmRpL12\tsta\nmRpL12\tmRpS34\nmRpL12\tRpS15Aa\nmRpL12\tRpL12\nmRpL12\tbonsai\nTctp\tRpS10b\nTctp\teEF1delta\nTctp\tRpL10Ab\nTctp\tRpS14a\nTctp\tRpS15Aa\nTctp\tRpS4\nTctp\tRpS17\nTctp\tsta\nTctp\tRpS6\nTctp\tRpS16\nTctp\tRpL12\nTctp\tRpS13\nTctp\tRpS12\nTctp\tRpS2\nTctp\tRpL13\neIF-2gamma\tTcp-1zeta\neIF-2gamma\tTcp-1eta\neIF-2gamma\tCG10576\neIF-2gamma\tCctgamma\neIF-2gamma\tCG7033\neIF-2gamma\tpAbp\neIF-2gamma\tCct5\neIF-2gamma\tRpS14a\neIF-2gamma\tCG8258\neIF-2gamma\tCG33123\neIF-2gamma\tCG1354\neIF-2gamma\tRpS15Ab\neIF-2gamma\tRpS15Aa\neIF-2gamma\tT-cp1\neIF-2gamma\tRpS17\neIF-2gamma\tRpL10Ab\neIF-2gamma\tRpLP2\neIF-2gamma\tRpL18A\neIF-2gamma\tRbp2\neIF-2gamma\tCG5642\neIF-2gamma\tRpL30\neIF-2gamma\teIF-3p66\neIF-2gamma\teIF4G\neIF-2gamma\tRpS3A\neIF-2gamma\tCG9769\neIF-2gamma\tRpL12\neIF-2gamma\tRpL11\neIF-2gamma\tRpS8\neIF-2gamma\tRpS10a\neIF-2gamma\tRpS10b\neIF-2gamma\tRpL13\neIF-2gamma\tRpS27\neIF-2gamma\tRpS23\neIF-2gamma\tRpS18\neIF-2gamma\tRpS11\neIF-2gamma\tRpS7\neIF-2gamma\tRpL3\neIF-2gamma\tRpS12\neIF-2gamma\tRpL4\neIF-2gamma\tRpS4\neIF-2gamma\tRpS13\neIF-2gamma\tTrip1\neIF-2gamma\tRpS2\neIF-2gamma\teIF3-S10\neIF-2gamma\tRpS16\neIF-2gamma\tRpS6\neIF-2gamma\tTango7\neIF-2gamma\tsta\neIF-2gamma\tRpS3\neIF-2gamma\teIF3-S8\neIF-2gamma\teIF3-S9\nUch-L5\tUsp5\nUch-L5\tRpt4R\nUch-L5\tRpn12\nRpt4R\tRpn12\nProsalpha1\tRpt4R\nProsalpha1\tRpn2\nProsalpha1\tRpn12\nProsalpha1\tRpn6\nProsalpha1\tRpt6R\nProsalpha1\tRpn5\nProsalpha1\tRpt6\nProsalpha1\tRpt5\nProsalpha1\tProsalpha6\nProsalpha1\tRpt2\nProsalpha1\tProsbeta7\nmRpS23\tCG4882\nmRpS23\tmRpL44\nmRpS23\tmRpL12\nmRpS23\tbonsai\nmRpS23\tmRpS34\nmRpS18B\tCG4882\nmRpS18B\tmRpL12\nmRpS18B\tbonsai\nmRpS18B\tmRpS34\nmRpS18B\tmRpL44\nmRpS18B\tmRpS23\nmor\tpont\nBap55\tUch-L5\nBap55\tpont\nBap55\tmor\nade5\tCG3011\nCg25C\tLanA\nvkg\tGlt\nvkg\tLanA\nvkg\tNdg\nvkg\ttrol\nvkg\tCg25C\nUfd1-like\tUbqn\nUfd1-like\tUsp5\nTER94\tpoe\nTER94\tbel\nTER94\tCnx99A\nTER94\tERp60\nTER94\tPdi\nTER94\tsmt3\nTER94\tHop\nTER94\tHsc70-3\nTER94\tCalpB\nTER94\tPfk\nTER94\tCG6891\nTER94\tEct3\nTER94\tPyK\nTER94\tRpn2\nTER94\tCat\nTER94\tCp1\nTER94\tme31B\nTER94\tCG8258\nTER94\tcathD\nTER94\tUbqn\nTER94\tRpn5\nTER94\tRpn6\nTER94\tRpt1\nTER94\tRpt5\nTER94\tCG8209\nTER94\tRpt2\nTER94\tp47\nTER94\tUfd1-like\nade3\tRpS15Aa\nade3\tRpL12\nade3\tRpS14a\nade3\tRpL10Ab\nade3\tsu(r)\nade3\tRpS6\nade3\tRpS4\nade3\tade5\nade3\tCG3011\nmRpS9\tCG4882\nmRpS9\tade3\nmRpS9\tRpS14a\nmRpS9\tRpL10Ab\nmRpS9\tRpS16\nmRpS9\tsta\nmRpS9\tRpS13\nmRpS9\tRpS15Aa\nmRpS9\tRpL12\nmRpS9\tRpS2\nmRpS9\tmRpS34\nmRpS9\tmRpL44\nmRpS9\tmRpS18B\nmRpS9\tmRpS23\nmRpS9\tbonsai\nmRpS9\tmRpL12\nmRpS5\tCG11876\nmRpS5\tRpS16\nmRpS5\tRpS14a\nmRpS5\tRpL10Ab\nmRpS5\tRpLP2\nmRpS5\tCG4882\nmRpS5\tSrp54k\nmRpS5\tRpS10b\nmRpS5\tRpL4\nmRpS5\tRpS10a\nmRpS5\tRpS8\nmRpS5\tRpS13\nmRpS5\tsta\nmRpS5\tRpL3\nmRpS5\tRpL12\nmRpS5\tRpS15Aa\nmRpS5\tRpS23\nmRpS5\tRpS15Ab\nmRpS5\tRpS27\nmRpS5\tRpS7\nmRpS5\tEfTuM\nmRpS5\tRpL18A\nmRpS5\tRpL13\nmRpS5\tRpS4\nmRpS5\tRpS17\nmRpS5\tRpS6\nmRpS5\tRpS18\nmRpS5\tRpS11\nmRpS5\tRpL11\nmRpS5\tRpS3\nmRpS5\tmRpS34\nmRpS5\tmRpS18B\nmRpS5\tmRpS23\nmRpS5\tmRpS22\nmRpS5\tCG4679\nmRpS5\tmRpL44\nmRpS5\tmRpL12\nmRpS5\tmRpS10\nmRpS5\tbonsai\nmRpS5\tmRpS9\nNot1\tIswi\nNot1\teIF4G\nNot1\tpAbp\nNot1\tPrp8\nNot1\tme31B\nNot3\tfrj\nNot3\tme31B\nNot3\tNot1\nHsc70-3\tCG2852\nHsc70-3\tHsp60\nHsc70-3\tOst48\nHsc70-3\tRae1\nHsc70-3\tCG2918\nPyK\tbel\nPyK\tpoe\nPyK\tme31B\nPyK\tMen\nPyK\tAldh\nPyK\tEct3\nPyK\tCG6891\nPyK\tCalpB\nPyK\tND-51\nPyK\tRpn5\nPyK\tVha68-2\nPyK\tCat\nPyK\tMdh2\nPyK\tEno\nLanA\tLanB2\nalphaCOP\tdeltaCOP\nSrp19\tRpS14a\nSrp19\tRpS15Aa\nSrp19\tRpS17\nSrp19\tRpL10Ab\nSrp19\tRpS10b\nSrp19\tRpS6\nSrp19\tRpS4\nSrp19\tsta\nSrp19\tRpS12\nSrp19\tRpL12\nSrp19\tRpS16\nSrp72\tRpS12\nSrp72\tRpS10b\nSrp72\tRpS6\nSrp72\tRpS13\nSrp72\tRpL10Ab\nSrp72\tRpL13\nSrp72\tsta\nSrp72\tRpS4\nSrp72\tRpS2\nSrp72\tRpL3\nSrp72\tRpS16\nSrp72\tRpL12\nSrp72\tSrp54k\nSrp72\tSrp19\nHsp83\tTcp-1zeta\nHsp83\tRack1\nHsp83\tzip\nHsp83\tERp60\nHsp83\tpont\nHsp83\tPdi\nHsp83\tTpi\nHsp83\tHDAC6\nHsp83\tbai\nHsp83\tCctgamma\nHsp83\tCnx99A\nHsp83\tCG2918\nHsp83\tCct5\nHsp83\tCG7033\nHsp83\tSgt\nHsp83\tHsp27\nHsp83\tT-cp1\nHsp83\tCalpB\nHsp83\tEct3\nHsp83\tCG6891\nHsp83\tDlic\nHsp83\tcpa\nHsp83\tCp1\nHsp83\tDhc64C\nHsp83\tHsp60C\nHsp83\tcpb\nHsp83\tcathD\nHsp83\tPfk\nHsp83\tGdi\nHsp83\tRac1\nHsp83\tPyK\nHsp83\tRpn6\nHsp83\tHsp60\nHsp83\tRpn5\nHsp83\tRpt1\nHsp83\tRpn3\nHsp83\tRpt5\nHsp83\tHsp70Ab\nHsp83\tCat\nHsp83\tCG8258\nHsp83\tHsc70-5\nHsp83\tFKBP59\nHsp83\tTER94\nHsp83\tDnaJ-H\nHsp83\tHsc70-3\nHsp83\tCG1416\nHsp83\tHop\nCG11089\tade5\nCG11089\tade3\nCG11089\tCG3011\nERp60\tPdi\nERp60\tOst48\nERp60\tCG2852\nERp60\tCG2918\nERp60\tCaBP1\nERp60\tHsc70-3\nERp60\tGp93\nzetaCOP\tKlp10A\nzetaCOP\tDlic\nzetaCOP\tp115\nzetaCOP\tcpa\nzetaCOP\talphaCOP\nzetaCOP\tbetaCOP\nzetaCOP\tdeltaCOP\nCOX6B\tND-B16.6\nCG7048\tcpa\nCG7048\tSrp19\nCG7048\tNlp\nCG7048\tmRpS23\nCG7048\tCG8498\nCG7048\trobl\nCG7048\tTim9a\nCG7048\teEF1delta\nCG7770\tRpS18\nCG7770\tpont\nCG7770\tCG7048\narm\tHsc70-3\narm\tProsalpha6\narm\tUch-L5\narm\tRpn5\narm\tProsbeta7\narm\tRpn12\narm\tRpt6R\narm\tRpt6\narm\tsyd\narm\tRpn2\narm\tRpn6\narm\tRpt1\narm\tRpt2\narm\tRpt5\narm\temb\narm\tRpt4R\narm\tpont\narm\talpha-Cat\neIF-3p66\tTcp-1eta\neIF-3p66\tCct5\neIF-3p66\tCctgamma\neIF-3p66\tRpS12\neIF-3p66\tRpS16\neIF-3p66\tRpS11\neIF-3p66\tRpS27\neIF-3p66\tRpS23\neIF-3p66\tRpS10a\neIF-3p66\tRpS10b\neIF-3p66\tRpS13\neIF-3p66\tRpL10Ab\neIF-3p66\tRpL13\neIF-3p66\tRpS7\neIF-3p66\tRpL12\neIF-3p66\tsta\neIF-3p66\tRpS4\neIF-3p66\tRpL11\neIF-3p66\tRpS18\neIF-3p66\tRpS2\neIF-3p66\tRpL3\neIF-3p66\tRpL18A\neIF-3p66\tRpS3\neIF-3p66\tRpL4\neIF-3p66\tRpS6\neIF-3p66\tCG9769\neIF-3p66\teIF3-S10\neIF-3p66\tTango7\neIF-3p66\teIF3-S8\neIF-3p66\teIF3-S9\neIF-3p66\tTrip1\neIF-3p66\tCG5642\nCG11752\tND-B16.6\nCOX4\tCG11752\nCOX4\tporin\nCOX4\tND-B16.6\nCOX4\tND-PDSW\nCOX4\tCyt-c-p\nCOX4\tCOX6B\nCOX5A\tsun\nCOX5A\tND-51\nCOX5A\tCG7603\nCOX5A\tCG11752\nCOX5A\tND-B16.6\nCOX5A\tCyt-c-p\nCOX5A\tND-PDSW\nCOX5A\tCOX6B\nCOX5A\tCOX4\nEno\tHsc70-3\nEno\tsta\nTpi\tCat\nTpi\tCG11899\nTpi\tRpS13\nTpi\tCG11876\nTpi\tade3\nTpi\tCG3011\nTpi\tHsc70-3\nTpi\tMdh2\nTpi\tPyK\nTpi\tEno\nCctgamma\tTrip1\nCctgamma\tCG10576\nCctgamma\tCG3800\nCctgamma\tHop\nCctgamma\tTcp-1zeta\nCctgamma\tTcp-1eta\nCctgamma\tCG7033\nRpt1\tUch\nRpt1\tRpS15Aa\nRpt1\tRpS15Ab\nRpt1\tRpS6\nRpt1\tUbqn\nRpt1\tRpL3\nRpt1\tRpS11\nRpt1\tRpS18\nRpt1\tUfd1-like\nRpt1\tCct5\nRpt1\tCalpB\nRpt1\tEct3\nRpt1\tCG6891\nRpt1\tPyK\nRpt1\tCp1\nRpt1\tCat\nRpt1\tPfk\nRpt1\tCG8258\nRpt1\tcathD\nRpt1\tProsalpha1\nRpt1\tUch-L5\nRpt1\tRpt6R\nRpt1\tRpt4R\nRpt1\tProsalpha6\nRpt1\tRpn12\nRpt1\tRpt5\nRpt1\tRpn5\nRpt1\tProsbeta7\nRpt1\tRpn2\nRpt1\tRpt6\nRpt1\tRpt2\nRpt1\tRpn6\nDlic\tp115\nDlic\tdeltaCOP\nDlic\tcpa\nDhc64C\tCLIP-190\nDhc64C\tbetaTub56D\nDhc64C\tKlc\nDhc64C\tpoe\nDhc64C\tbai\nDhc64C\tFKBP59\nDhc64C\tbetaCOP\nDhc64C\tdeltaCOP\nDhc64C\tepsilonCOP\nDhc64C\tbeta'COP\nDhc64C\tCG10186\nDhc64C\tzetaCOP\nDhc64C\tp115\nDhc64C\tcpa\nDhc64C\tcpb\nDhc64C\trobl\nDhc64C\tKhc\nDhc64C\tBicD\nDhc64C\tDlic\nEf1beta\tFeCH\nEf1beta\tCG7048\nEf1beta\tAats-asp\nEf1beta\tRpS15Aa\nEf1beta\tRpS10a\nEf1beta\tRpS15Ab\nEf1beta\tCG5642\nEf1beta\tRpL3\nEf1beta\tRpS3A\nEf1beta\tRpS12\nEf1beta\tRpS13\nEf1beta\tRpS10b\nEf1beta\tRpS16\nEf1beta\tRpS2\nEf1beta\tRpS6\nEf1beta\tRpL12\nEf1beta\tRpL30\nEf1beta\tRpS23\nEf1beta\tRpS11\nEf1beta\teEF1delta\nEf1beta\tRpL13\nEf1beta\tsta\nEf1beta\tRpS7\nEf1beta\tTctp\nEf1beta\tTrip1\nEf1beta\tRpS8\nEf1beta\tRpS18\nEf1beta\tRack1\nEf1beta\tRpS17\nEf1beta\tRpL10Ab\nEf1beta\tRpL18A\nEf1beta\tRpS3\nEf1beta\tRpS4\nEf1beta\tRpL11\nEf1beta\tRpLP2\nEf1beta\tRpL4\nRpn13\tTER94\nRpn13\tUfd1-like\nRpn13\tUbqn\nRpn13\tProsalpha6\nRpn13\tProsbeta7\nRpn13\tRpt4R\nRpn13\tProsalpha7\nRpn13\tRpn12\nRpn13\tRpt6R\nRpn13\tRpn5\nRpn13\tRpt6\nRpn13\tRpn6\nRpn13\tUch-L5\nRpn13\tRpt2\nRpn13\tRpt5\nRpn13\tRpt1\nRpn13\tRpn2\nTango7\tCG7033\nTango7\tTcp-1zeta\nTango7\tmRpS10\nTango7\tRpn5\nTango7\tbonsai\nTango7\tRpn2\nTango7\tRpn6\nTango7\tRpS14a\nTango7\tRpS15Aa\nTango7\tRpS17\nTango7\tRpL10Ab\nTango7\tRpL12\nTango7\tRpL13\nTango7\tRpL18A\nTango7\tRpL3\nTango7\tRpS10a\nTango7\tRpL11\nTango7\tRpS10b\nTango7\tRpS2\nTango7\tRpS27\nTango7\tRpS23\nTango7\tRpS18\nTango7\tRpS12\nTango7\tRpS13\nTango7\tRpS4\nTango7\tRpS6\nTango7\tRpS16\nTango7\tsta\nTango7\teIF3-S10\nTango7\tRpS3\nTango7\tTrip1\nTango7\tRpL4\nTango7\tCG9769\nTango7\teIF3-S9\nTango7\tCG5642\nTango7\teIF3-S8\nRpt2\tUbqn\nRpt2\tUfd1-like\nRpt2\tCG8209\nRpt2\tUch-L5\nRpt2\tRpt4R\nRpt2\tRpn12\nRpt2\tRpt5\nRpt2\tRpt6\nRpt2\tProsalpha6\nRpt2\tRpn5\nRpt2\tProsbeta7\nRpn5\tUbqn\nRpn5\tUfd1-like\nRpn5\tCat\nRpn5\tCalpB\nRpn5\tCG6891\nRpn5\tUch-L5\nRpn5\tRpt4R\nRpn5\tRpt6\nRpn5\tRpn12\nbetaCOP\tcpa\nbetaCOP\tDlic\nbetaCOP\tKlp10A\nbetaCOP\tp115\nbetaCOP\talphaCOP\nbetaCOP\tdeltaCOP\nRpt6R\tRpS4\nRpt6R\tpont\nRpt6R\tRpt6\nRpt6R\tUch-L5\nRpt6R\tRpt4R\nRpt6R\tProsalpha6\nRpt6R\tRpt2\nRpt6R\tRpt5\nRpt6R\tProsbeta7\nRpt6R\tRpn12\nRpt6R\tRpn2\nRpt6R\tRpn5\nRpn2\tTrip1\nRpn2\tRpS2\nRpn2\tUsp5\nRpn2\tUch-L5\nRpn2\tProsalpha6\nRpn2\tRpt4R\nRpn2\tProsbeta7\nRpn2\tRpn12\nRpn2\tRpn5\nRpn2\tRpt6\nRpn2\tRpt2\nRpn2\tRpt5\nRpL10Ab\tbonsai\nRpL10Ab\tmago\nRpL10Ab\tCG5642\nRpL10Ab\tRpS15Aa\nRpL10Ab\tRpS14a\nRpL10Ab\tRpL12\nRpL10Ab\tRpS16\nRpL10Ab\tRpS12\nRpL10Ab\tRpS10b\nRpL10Ab\tRpS4\nRpL10Ab\tsta\nRpL10Ab\tRpS6\nTrip1\tRpS14a\nTrip1\tTcp-1zeta\nTrip1\tCG10576\nTrip1\tRpS17\nTrip1\tCG7033\nTrip1\tRpL10Ab\nTrip1\tRpS10b\nTrip1\tRpL12\nTrip1\tRpS12\nTrip1\tRpS4\nTrip1\tRpS6\nTrip1\tRpS16\nTrip1\tsta\nTrip1\tCG9769\nTrip1\teIF3-S10\nTrip1\tCG5642\ncpa\tp115\ncpa\tdeltaCOP\ncpb\tCG6891\ncpb\tflr\ncpb\tp115\ncpb\tdeltaCOP\ncpb\ttsr\ncpb\tbetaCOP\ncpb\tzetaCOP\ncpb\tDlic\ncpb\tcpa\neIF3-S10\tCG33123\neIF3-S10\tAats-ile\neIF3-S10\tRpS14a\neIF3-S10\tRpS17\neIF3-S10\tRpS15Aa\neIF3-S10\tRpL10Ab\neIF3-S10\tRpL12\neIF3-S10\tRpS12\neIF3-S10\tRpS10b\neIF3-S10\tRpS6\neIF3-S10\tRpS16\neIF3-S10\tRpS4\neIF3-S10\tsta\neIF3-S10\tCG5642\neIF3-S10\tCG9769\neIF3-S9\tCctgamma\neIF3-S9\trin\neIF3-S9\tRpS14a\neIF3-S9\tRpS15Aa\neIF3-S9\tRpS17\neIF3-S9\tTcp-1eta\neIF3-S9\tCG7033\neIF3-S9\tT-cp1\neIF3-S9\tCG10576\neIF3-S9\tpAbp\neIF3-S9\tRpL10Ab\neIF3-S9\tRpL12\neIF3-S9\tRpS12\neIF3-S9\tRpS10a\neIF3-S9\tRpS27\neIF3-S9\tRpS10b\neIF3-S9\tRpS18\neIF3-S9\tRpL11\neIF3-S9\tRpL13\neIF3-S9\tRpL3\neIF3-S9\tRpL4\neIF3-S9\tRpS16\neIF3-S9\tsta\neIF3-S9\tRpS2\neIF3-S9\tRpS13\neIF3-S9\tRpS4\neIF3-S9\tRpS6\neIF3-S9\tRpS3\neIF3-S9\tCG5642\neIF3-S9\teIF3-S8\neIF3-S9\tCG9769\neIF3-S9\tTrip1\neIF3-S9\teIF3-S10\nPdi\tUgt\nPdi\tOst48\nPdi\tCG2852\nPdi\tCG2918\nPdi\tHsc70-3\nepsilonCOP\tDlic\nepsilonCOP\tKlc\nepsilonCOP\tKlp10A\nepsilonCOP\tcpb\nepsilonCOP\tcpa\nepsilonCOP\tp115\nepsilonCOP\tdeltaCOP\nepsilonCOP\tzetaCOP\nepsilonCOP\talphaCOP\nepsilonCOP\tbetaCOP\nbeta'COP\tcpb\nbeta'COP\tcpa\nbeta'COP\tDlic\nbeta'COP\tKlp10A\nbeta'COP\tKlc\nbeta'COP\tp115\nbeta'COP\tzetaCOP\nbeta'COP\talphaCOP\nbeta'COP\tdeltaCOP\nbeta'COP\tbetaCOP\nbeta'COP\tepsilonCOP\nCaBP1\tUgt\nCaBP1\tOst48\nCaBP1\tCG2918\nCaBP1\tHsc70-3\nCaBP1\tPdi\nGp93\tCctgamma\nGp93\tbai\nGp93\tHsp27\nGp93\tHop\nGp93\tHsp70Ab\nGp93\tCG11577\nGp93\tPdi\nGp93\tHsc70-3\nGp93\tCG2918\nGp93\tCaBP1\nTcp-1eta\tRpL12\nTcp-1eta\tRpS6\nTcp-1eta\tRpL13\nTcp-1eta\tRpL10Ab\nTcp-1eta\tsta\nTcp-1eta\tRpS2\nTcp-1eta\tRpS12\nTcp-1eta\tRpS13\nTcp-1eta\tCG10576\nTcp-1eta\tRpS4\nTcp-1eta\tRpS16\nTcp-1eta\tTrip1\nTcp-1eta\tHop\nTcp-1eta\tade3\nTcp-1eta\tTcp-1zeta\nTcp-1eta\tCG7033\nRpL13\tmRpL12\nRpL13\tCG7033\nRpL13\tade3\nRpL13\tbonsai\nRpL13\tSrp19\nRpL13\tmago\nRpL13\tCG5642\nRpL13\teIF3-S10\nRpL13\tCG10576\nRpL13\tSrp54k\nRpL13\tCG9769\nRpL13\tTrip1\nRpL13\tRpS15Aa\nRpL13\tRpL12\nRpL13\tRpS12\nRpL13\tRpS13\nRpL13\tRpS6\nRpL13\tRpS14a\nRpL13\tRpL10Ab\nRpL13\tRpS4\nRpL13\tsta\nRpL13\tRpS17\nRpL13\tRpS10b\nRpL13\tRpS16\nRpt4\tProsalpha1\nRpt4\tarm\nRpt4\tRpt4R\nRpt4\tUch-L5\nRpt4\tRpt6R\nRpt4\tRpn13\nRpt4\tRpt1\nRpt4\tRpt6\nRpt4\tProsalpha6\nRpt4\tProsbeta7\nRpt4\tRpn6\nRpt4\tRpt2\nRpt4\tRpn3\nRpt4\tProsalpha7\nRpt4\tRpn2\nRpt4\tRpn12\nRpt4\tRpn5\nRpt4\tRpt5\nRpS11\tvig\nRpS11\tbic\nRpS11\tCG7033\nRpS11\tTctp\nRpS11\tTcp-1eta\nRpS11\teEF1delta\nRpS11\tmago\nRpS11\tEfTuM\nRpS11\tSrp72\nRpS11\tbonsai\nRpS11\tmRpS10\nRpS11\tCG5642\nRpS11\tmRpS9\nRpS11\tmRpL12\nRpS11\teIF3-S8\nRpS11\tCG9769\nRpS11\teIF3-S10\nRpS11\tSrp19\nRpS11\tade3\nRpS11\teIF3-S9\nRpS11\tSrp54k\nRpS11\tTango7\nRpS11\tTrip1\nRpS11\tRpL10Ab\nRpS11\tRpS23\nRpS11\tRpL3\nRpS11\tRpL12\nRpS11\tRpL11\nRpS11\tRpS14a\nRpS11\tRpL4\nRpS11\tRpS18\nRpS11\tRpS6\nRpS11\tsta\nRpS11\tRpS10a\nRpS11\tRpS17\nRpS11\tRpS2\nRpS11\tRpL18A\nRpS11\tRpS12\nRpS11\tRpS3\nRpS11\tRpS13\nRpS11\tRpS27\nRpS11\tRpS10b\nRpS11\tRpL13\nRpS11\tRpS16\nRpS11\tRpS15Aa\nRpS11\tRpS4\neIF3-S8\tCG33123\neIF3-S8\tTcp-1zeta\neIF3-S8\teEF1delta\neIF3-S8\tpAbp\neIF3-S8\tRpS14a\neIF3-S8\tRpS17\neIF3-S8\tRpS15Aa\neIF3-S8\tCG10576\neIF3-S8\tRpL12\neIF3-S8\tRpL10Ab\neIF3-S8\tRpL11\neIF3-S8\tRpL13\neIF3-S8\tRpL3\neIF3-S8\tRpS12\neIF3-S8\tsta\neIF3-S8\tRpS2\neIF3-S8\tRpS13\neIF3-S8\tRpS4\neIF3-S8\tRpS10b\neIF3-S8\tRpS27\neIF3-S8\tRpS10a\neIF3-S8\tRpS16\neIF3-S8\tRpL4\neIF3-S8\tRpS18\neIF3-S8\tRpS6\neIF3-S8\tRpS3\neIF3-S8\teIF3-S10\neIF3-S8\tTrip1\neIF3-S8\tCG9769\neIF3-S8\tCG5642\nHop\tHsp60\nHop\tHsc70-3\nHop\tUbqn\nCG1416\tCG7033\nCG1416\tHsp60\nCG1416\tGp93\nCG1416\tCctgamma\nCG1416\tHop\nRpS6\tRpS14a\nRpS6\tsta\nRpS2\tCG10576\nRpS2\tade3\nRpS2\tbonsai\nRpS2\tCG7033\nRpS2\tmago\nRpS2\tmRpL12\nRpS2\tCG5642\nRpS2\teIF3-S10\nRpS2\tSrp19\nRpS2\tCG9769\nRpS2\tSrp54k\nRpS2\tTrip1\nRpS2\tRpS6\nRpS2\tRpS15Aa\nRpS2\tRpL12\nRpS2\tRpS14a\nRpS2\tRpS10b\nRpS2\tRpS16\nRpS2\tRpL10Ab\nRpS2\tRpS17\nRpS2\tsta\nRpS2\tRpS4\nRpS2\tRpS13\nRpS2\tRpL13\nRpS2\tRpS12\nProsalpha6\tTrip1\nProsalpha6\tTcp-1zeta\nProsalpha6\tUch-L5\nProsalpha6\tRpn5\nProsalpha6\tRpt4R\nProsalpha6\tRpt6\nProsalpha6\tProsbeta7\nProsalpha6\tRpn12\nND-PDSW\tCOX6B\nND-PDSW\tCG7603\nND-PDSW\tND-B16.6\nND-PDSW\tCG11752\nNP15.6\tCG7603\nNP15.6\tCOX4\nNP15.6\tCG11752\nNP15.6\tND-51\nNP15.6\tCOX5A\nNP15.6\tND-B16.6\nNP15.6\tND-PDSW\nKlc\tDlic\nKlc\tbetaCOP\nKlc\tdeltaCOP\nKlc\tzetaCOP\nKlc\tKlp10A\nKhc\tDlic\nKhc\tzetaCOP\nKhc\tepsilonCOP\nKhc\tbai\nKhc\tdeltaCOP\nKhc\tbeta'COP\nKhc\tsyd\nKhc\tbetaCOP\nKhc\tKlp10A\nKhc\tbetaTub56D\nKhc\tKlc\nRpL12\tbonsai\nRpL12\tmago\nRpL12\tRpS16\nRpL12\tsta\nRpL12\tRpS14a\nRpL12\tRpS6\nRpt5\tUbqn\nRpt5\tCat\nRpt5\tCalpB\nRpt5\tCG6891\nRpt5\tEct3\nRpt5\tPyK\nRpt5\tTcp-1eta\nRpt5\tUfd1-like\nRpt5\tUch-L5\nRpt5\tRpt4R\nRpt5\tProsalpha6\nRpt5\tProsbeta7\nRpt5\tRpn5\nRpt5\tRpt6\nRpt5\tRpn12\nProsalpha7\tRpS16\nProsalpha7\tCct5\nProsalpha7\tTcp-1eta\nProsalpha7\tCG7033\nProsalpha7\tTrip1\nProsalpha7\tCG8258\nProsalpha7\tarm\nProsalpha7\tUch-L5\nProsalpha7\tRpt4R\nProsalpha7\tProsalpha1\nProsalpha7\tRpt6R\nProsalpha7\tRpn12\nProsalpha7\tRpt6\nProsalpha7\tRpn6\nProsalpha7\tRpt2\nProsalpha7\tRpt1\nProsalpha7\tProsbeta7\nProsalpha7\tProsalpha6\nProsalpha7\tRpn5\nProsalpha7\tRpn2\nProsalpha7\tRpt5\nRpn6\tTrip1\nRpn6\tRpS2\nRpn6\tUbqn\nRpn6\tCalpB\nRpn6\tCat\nRpn6\tPyK\nRpn6\tEct3\nRpn6\tCG6891\nRpn6\tUch-L5\nRpn6\tProsalpha6\nRpn6\tRpt4R\nRpn6\tProsbeta7\nRpn6\tRpn2\nRpn6\tRpt5\nRpn6\tRpn12\nRpn6\tRpn5\nRpn6\tRpt2\nRpn6\tRpt6R\nRpn6\tRpt6\nATPsynbeta\tRack1\nATPsynbeta\tCOX4\nATPsynbeta\tCOX6B\nATPsynbeta\tsta\nATPsynbeta\tHsc70-5\nATPsynbeta\tEno\nATPsynbeta\tRpL4\nATPsynbeta\tRpS3A\nATPsynbeta\tCG11876\nATPsynbeta\tMpcp\nATPsynbeta\tporin\nATPsynbeta\tVha100-1\nATPsynbeta\tRpS2\nATPsynbeta\tVha100-2\nATPsynbeta\tHsp83\nATPsynbeta\tND-51\nATPsynbeta\tVha68-2\nATPsynbeta\tEfTuM\nATPsynbeta\tMdh2\nATPsynbeta\tCG9090\nATPsynbeta\tCOX5A\nATPsynbeta\tsun\nATPsynCF6\tVha100-1\nATPsynCF6\tVha100-2\nATPsynCF6\tNP15.6\nATPsynCF6\tVha68-2\nATPsynCF6\tND-B16.6\nATPsynCF6\tCOX6B\nATPsynCF6\tCG11752\nATPsynCF6\tCyt-c-p\nATPsynCF6\tCOX4\nATPsynCF6\tND-PDSW\nATPsynCF6\tCOX5A\nATPsynCF6\tsun\nATPsynCF6\tATPsynbeta\nCG8258\tRpS13\nCG8258\tRpL11\nCG8258\teIF3-S9\nCG8258\tTango7\nCG8258\tHsp60C\nCG8258\tRpn2\nCG8258\tRpS3\nCG8258\tRpL12\nCG8258\tRpL3\nCG8258\tHsc70-5\nCG8258\tRpL4\nCG8258\tRpS4\nCG8258\tRpS2\nCG8258\tRpS16\nCG8258\tHsp60\nCG8258\tCG10576\nCG8258\tCat\nCG8258\tCG6891\nCG8258\tCalpB\nCG8258\tEct3\nCG8258\tCp1\nCG8258\tPfk\nCG8258\tPyK\nCG8258\tade3\nCG8258\tTrip1\nCG8258\tRpt5\nCG8258\tRpn6\nCG8258\tRpn5\nCG8258\tHop\nCG8258\tCct5\nCG8258\tCctgamma\nCG8258\tTcp-1zeta\nCG8258\tCG7033\nCG8258\tTcp-1eta\nCG8258\tT-cp1\nRpS7\tRpt6R\nRpS7\tRpt6\nRpS7\tvig\nRpS7\teEF1delta\nRpS7\tRpt1\nRpS7\tRpt2\nRpS7\tade3\nRpS7\tmRpS10\nRpS7\tSrp72\nRpS7\tmago\nRpS7\tbonsai\nRpS7\tTctp\nRpS7\tCG5642\nRpS7\tCG9769\nRpS7\teIF3-S10\nRpS7\tSrp54k\nRpS7\tSrp19\nRpS7\tTrip1\nRpS7\teIF3-S9\nRpS7\teIF3-S8\nRpS7\tTango7\nRpS7\tRpS15Ab\nRpS7\tRpS10a\nRpS7\tRpS15Aa\nRpS7\tRpS14a\nRpS7\tRpL4\nRpS7\tRpL13\nRpS7\tsta\nRpS7\tRpS2\nRpS7\tRpS23\nRpS7\tRpS16\nRpS7\tRpL10Ab\nRpS7\tRpS6\nRpS7\tRpS18\nRpS7\tRpL11\nRpS7\tRpS12\nRpS7\tRpS4\nRpS7\tRpS3\nRpS7\tRpS13\nRpS7\tRpS27\nRpS7\tRpL3\nRpS7\tRpS10b\nRpS7\tRpS17\nRpS7\tRpL18A\nRpS7\tRpS11\nRpS7\tRpL12\nRpL10\tmRpS10\nRpL10\tade3\nRpL10\tCct5\nRpL10\tmRpS5\nRpL10\teEF1delta\nRpL10\tmRpL12\nRpL10\tmRpS9\nRpL10\tEf1beta\nRpL10\teIF4G\nRpL10\tSrp72\nRpL10\tmago\nRpL10\tRbp2\nRpL10\teIF3-S10\nRpL10\teIF3-S8\nRpL10\tCG5642\nRpL10\tCG10576\nRpL10\tTango7\nRpL10\tTrip1\nRpL10\tSrp19\nRpL10\tmRpS7\nRpL10\teIF-2gamma\nRpL10\tElf\nRpL10\teIF-3p66\nRpL10\tCG9769\nRpL10\teIF3-S9\nRpL10\tSrp54k\nRpL10\teRF1\nRpL10\tRpS15Ab\nRpL10\tTctp\nRpL10\tRpS14a\nRpL10\tRpS15Aa\nRpL10\tRpS10b\nRpL10\tRpS17\nRpL10\tRpL13\nRpL10\tRpS8\nRpL10\tRpS16\nRpL10\tsta\nRpL10\tRpL12\nRpL10\tRpS3\nRpL10\tRpL4\nRpL10\tRpS27\nRpL10\tRpL3\nRpL10\tRpS12\nRpL10\tRpL11\nRpL10\tRpLP2\nRpL10\tRpL18A\nRpL10\tRpL26\nRpL10\tRpS4\nRpL10\tRack1\nRpL10\tRpS3A\nRpL10\tRpS11\nRpL10\tRpS13\nRpL10\tRpS10a\nRpL10\tRpL30\nRpL10\tRpS2\nRpL10\tRpS18\nRpL10\tRpS23\nRpL10\tRpL10Ab\nRpL10\tRpS6\nRpL10\tRpS7\nAats-asp\tCG5642\nAats-asp\tCG31739\nAats-asp\tCG33123\nAats-asp\tAats-arg\nAats-asp\tAats-ile\nCG33123\tAats-arg\nAats-ile\tCG33123\nAats-ile\tAats-arg\nRpS8\tT-cp1\nRpS8\tvig\nRpS8\tRpn2\nRpS8\tCct5\nRpS8\teEF1delta\nRpS8\tmRpS10\nRpS8\tCG8258\nRpS8\tTcp-1eta\nRpS8\tbonsai\nRpS8\tCG7033\nRpS8\tade3\nRpS8\tCG10576\nRpS8\tSrp72\nRpS8\tmago\nRpS8\tRbp2\nRpS8\tNacalpha\nRpS8\tCG5642\nRpS8\teIF-3p66\nRpS8\tSrp54k\nRpS8\tSrp19\nRpS8\teIF3-S10\nRpS8\tTctp\nRpS8\tTrip1\nRpS8\tTango7\nRpS8\teIF3-S8\nRpS8\tCG9769\nRpS8\teIF3-S9\nRpS8\tRpL13\nRpS8\tRpS17\nRpS8\tRpL11\nRpS8\tRpS12\nRpS8\tRpL4\nRpS8\tRpS3\nRpS8\tRpS27\nRpS8\tRpS18\nRpS8\tRpS2\nRpS8\tRpS13\nRpS8\tRpS7\nRpS8\tRpS6\nRpS8\tsta\nRpS8\tRpL10Ab\nRpS8\tRpS10b\nRpS8\tRpS10a\nRpS8\tRpS14a\nRpS8\tRpS4\nRpS8\tRpL12\nRpS8\tRpS11\nRpS8\tRpL18A\nRpS8\tRpLP2\nRpS8\tRpL3\nRpS8\tRpS23\nRpS8\tRpS15Ab\nRpS8\tRpS15Aa\nRpS8\tRpS16\nRpt6\tUbqn\nRpt6\tRpS4\nRpt6\tUch-L5\nRpt6\tRpt4R\nRpt6\tRpn12\nRpn3\tbel\nRpn3\tUbqn\nRpn3\tCalpB\nRpn3\tEct3\nRpn3\tPfk\nRpn3\tCG6891\nRpn3\tCat\nRpn3\tCp1\nRpn3\tPyK\nRpn3\tarm\nRpn3\tcathD\nRpn3\tCG8258\nRpn3\tProsalpha1\nRpn3\tUch-L5\nRpn3\tTER94\nRpn3\tProsalpha6\nRpn3\tProsbeta7\nRpn3\tProsalpha7\nRpn3\tRpt4R\nRpn3\tRpn2\nRpn3\tRpt6R\nRpn3\tRpt2\nRpn3\tRpn5\nRpn3\tRpt1\nRpn3\tRpt5\nRpn3\tRpn6\nRpn3\tRpn13\nRpn3\tRpn12\nRpn3\tRpt6\nRpL30\tvig\nRpL30\tNacalpha\nRpL30\tCG10576\nRpL30\tSrp72\nRpL30\teIF4G\nRpL30\tbonsai\nRpL30\teIF3-S8\nRpL30\tmago\nRpL30\teIF3-S10\nRpL30\tCG5642\nRpL30\tmRpS5\nRpL30\tRbp2\nRpL30\tmRpS10\nRpL30\teIF-3p66\nRpL30\tTctp\nRpL30\teIF3-S9\nRpL30\tTango7\nRpL30\tCG9769\nRpL30\tTrip1\nRpL30\tSrp54k\nRpL30\tSrp19\nRpL30\tRpS15Aa\nRpL30\tRpS15Ab\nRpL30\tRpS10a\nRpL30\tRpL18A\nRpL30\tRpS27\nRpL30\tRpL12\nRpL30\tRpS12\nRpL30\tRpS18\nRpL30\tRpS17\nRpL30\tRpS2\nRpL30\tRpL11\nRpL30\tRpS7\nRpL30\tRpS14a\nRpL30\tRpS4\nRpL30\tRpS8\nRpL30\tRpS10b\nRpL30\tRpLP2\nRpL30\tRpL13\nRpL30\tRpS16\nRpL30\tRpS3\nRpL30\tRpS13\nRpL30\tRpS11\nRpL30\tRpS6\nRpL30\tRpL3\nRpL30\tRpL10Ab\nRpL30\tRpS23\nRpL30\tRpL4\nRpL30\tsta\nRpS10a\tTctp\nRpS10a\teEF1delta\nRpS10a\tbonsai\nRpS10a\tmRpS10\nRpS10a\tRpS10b\nRpS10a\tSrp72\nRpS10a\tmago\nRpS10a\tCG5642\nRpS10a\tCG9769\nRpS10a\teIF3-S10\nRpS10a\tTrip1\nRpS10a\tSrp19\nRpS10a\tSrp54k\nRpS10a\tRpS15Aa\nRpS10a\tsta\nRpS10a\tRpS2\nRpS10a\tRpS3\nRpS10a\tRpL10Ab\nRpS10a\tRpS4\nRpS10a\tRpS13\nRpS10a\tRpS6\nRpS10a\tRpS12\nRpS10a\tRpS16\nRpS10a\tRpS14a\nRpS10a\tRpS27\nRpS10a\tRpL13\nRpS10a\tRpL12\nRpS10a\tRpS17\nRpS10a\tRpL3\nRpS17\tCG7033\nRpS17\tCG5642\nRpS17\tbonsai\nRpS17\tRpS6\nRpS17\tRpL12\nRpS17\tRpL10Ab\nRpS17\tRpS15Aa\nRpS17\tRpS12\nRpS17\tsta\nRpS17\tRpS14a\nRpS17\tRpS4\nRpS17\tRpS16\nRpS17\tRpS10b\nRpLP0\tATPsynbeta\nRpLP0\tTcp-1zeta\nRpLP0\tT-cp1\nRpLP0\tCG7033\nRpLP0\tEfTuM\nRpLP0\tNacalpha\nRpLP0\tbonsai\nRpLP0\tTcp-1eta\nRpLP0\tmRpS10\nRpLP0\tade3\nRpLP0\tCct5\nRpLP0\tmRpS5\nRpLP0\teEF1delta\nRpLP0\tSrp72\nRpLP0\teIF4G\nRpLP0\tmRpS9\nRpLP0\tRbp2\nRpLP0\tmago\nRpLP0\tCG5642\nRpLP0\teIF3-S8\nRpLP0\tElf\nRpLP0\teIF3-S10\nRpLP0\tCG10576\nRpLP0\tmRpL12\nRpLP0\tmRpS7\nRpLP0\tTctp\nRpLP0\tSrp19\nRpLP0\tTango7\nRpLP0\teIF-2gamma\nRpLP0\teIF-3p66\nRpLP0\tCG9769\nRpLP0\teIF3-S9\nRpLP0\tRpS15Aa\nRpLP0\tSrp54k\nRpLP0\tTrip1\nRpLP0\tRpS15Ab\nRpLP0\teRF1\nRpLP0\tRpS14a\nRpLP0\tRpS10a\nRpLP0\tEf1beta\nRpLP0\tRpS6\nRpLP0\tRpS13\nRpLP0\tRpS23\nRpLP0\tsta\nRpLP0\tRpS11\nRpLP0\tRpS16\nRpLP0\tRpS7\nRpLP0\tRpS27\nRpLP0\tRpL10\nRpLP0\tRpS3\nRpLP0\tRpS10b\nRpLP0\tRpS8\nRpLP0\tRpLP2\nRpLP0\tRpS12\nRpLP0\tRpL3\nRpLP0\tRack1\nRpLP0\tRpL13\nRpLP0\tRpL26\nRpLP0\tRpL30\nRpLP0\tRpL18A\nRpLP0\tRpL4\nRpLP0\tRpL10Ab\nRpLP0\tRpS18\nRpLP0\tRpS3A\nRpLP0\tRpL12\nRpLP0\tRpL11\nRpLP0\tRpS2\nRpLP0\tRpS4\nRpLP0\tRpS17\neRF1\tbonsai\neRF1\tT-cp1\neRF1\teIF3-S8\neRF1\tCG8635\neRF1\tCG7033\neRF1\tCctgamma\neRF1\tmRpS5\neRF1\tmRpS10\neRF1\twuho\neRF1\tRpS15Ab\neRF1\tRpS14a\neRF1\tRpS15Aa\neRF1\teIF3-S10\neRF1\tpAbp\neRF1\tCG10576\neRF1\tRpS17\neRF1\teIF-2gamma\neRF1\tTrip1\neRF1\tmago\neRF1\tRpLP2\neRF1\teIF3-S9\neRF1\teIF4G\neRF1\tRack1\neRF1\tRpS10a\neRF1\tRpS10b\neRF1\tRpL3\neRF1\tRpL10Ab\neRF1\tRpS27\neRF1\tRpS12\neRF1\tRpS7\neRF1\tRpS18\neRF1\tRpL12\neRF1\tRpS23\neRF1\tRpL18A\neRF1\tRpS16\neRF1\tRpL30\neRF1\tRpS11\neRF1\tRpS3\neRF1\tRpS3A\neRF1\tsta\neRF1\tRpS13\neRF1\tRpS6\neRF1\tRpS8\neRF1\tRpL4\neRF1\tRpS4\neRF1\tRpL11\neRF1\tRpS2\neRF1\tRpL13\nElf\tade5\nElf\tmRpS7\nElf\tRpS15Aa\nElf\tRpS15Ab\nElf\tT-cp1\nElf\teIF3-S9\nElf\tCctgamma\nElf\tRpS17\nElf\tCG33123\nElf\tCct5\nElf\tRpS14a\nElf\teIF3-S8\nElf\teIF3-S10\nElf\teIF-2gamma\nElf\tRpL10Ab\nElf\tRpLP2\nElf\tmago\nElf\tRpL13\nElf\tRpL3\nElf\tRpL30\nElf\tRpL18A\nElf\tRpL12\nElf\tRpL11\nElf\tRack1\nElf\tRpL4\nElf\tRpS4\nElf\tRpS10b\nElf\tRpS10a\nElf\tRpS12\nElf\teIF4G\nElf\tRpS11\nElf\tRpS13\nElf\tRpS27\nElf\tRpS8\nElf\tRpS16\nElf\tRpS3\nElf\tsta\nElf\tRpS23\nElf\tRpS18\nElf\tRpS7\nElf\tRpS6\nElf\tRpS3A\nElf\tRpS2\nElf\tpAbp\nElf\teRF1\nRpS15Aa\tbonsai\nRpS15Aa\tRpL12\nRpS15Aa\tsta\nRpS15Aa\tRpS6\nRpS15Aa\tRpS16\nRpS15Aa\tRpS14a\nRpS27\tvig\nRpS27\tSrp72\nRpS27\tmRpS10\nRpS27\tbonsai\nRpS27\tmago\nRpS27\tTctp\nRpS27\tCG5642\nRpS27\tCG9769\nRpS27\tTrip1\nRpS27\teIF3-S10\nRpS27\tSrp19\nRpS27\tSrp54k\nRpS27\tRpS13\nRpS27\tRpS17\nRpS27\tRpS4\nRpS27\tRpS2\nRpS27\tRpL13\nRpS27\tRpL12\nRpS27\tRpS6\nRpS27\tRpS12\nRpS27\tRpS10b\nRpS27\tsta\nRpS27\tRpS16\nRpS27\tRpS14a\nRpS27\tRpL10Ab\nRpS27\tRpL3\nRpS27\tRpS3\nRpS27\tRpS15Aa\nRpS18\tRpt6R\nRpS18\tRpt6\nRpS18\tTcp-1eta\nRpS18\tRpt2\nRpS18\tvig\nRpS18\tade3\nRpS18\tSrp72\nRpS18\tmago\nRpS18\tbonsai\nRpS18\tTctp\nRpS18\tCG5642\nRpS18\tmRpL12\nRpS18\tmRpS10\nRpS18\tmRpS9\nRpS18\teIF3-S10\nRpS18\tCG9769\nRpS18\tSrp19\nRpS18\tSrp54k\nRpS18\tTrip1\nRpS18\tRpS12\nRpS18\tRpS2\nRpS18\tRpS16\nRpS18\tsta\nRpS18\tRpL10Ab\nRpS18\tRpL4\nRpS18\tRpS14a\nRpS18\tRpS17\nRpS18\tRpS3\nRpS18\tRpS4\nRpS18\tRpL3\nRpS18\tRpS10a\nRpS18\tRpS13\nRpS18\tRpS10b\nRpS18\tRpL13\nRpS18\tRpL12\nRpS18\tRpS15Aa\nRpS18\tRpS6\nRpS18\tRpS27\nRpS10b\tbonsai\nRpS10b\tmago\nRpS10b\tRpS15Aa\nRpS10b\tRpS16\nRpS10b\tRpS14a\nRpS10b\tRpL12\nRpS10b\tsta\nRpS10b\tRpS6\nRpS3A\tCG7048\nRpS3A\tNacalpha\nRpS3A\tvig\nRpS3A\teEF1delta\nRpS3A\tCct5\nRpS3A\tCG8258\nRpS3A\tCG7033\nRpS3A\tTcp-1eta\nRpS3A\tSrp72\nRpS3A\tmRpS5\nRpS3A\tRbp2\nRpS3A\tmRpS10\nRpS3A\teIF4G\nRpS3A\tbonsai\nRpS3A\tmago\nRpS3A\tCG5642\nRpS3A\teIF-3p66\nRpS3A\teIF3-S8\nRpS3A\tSrp54k\nRpS3A\teIF3-S10\nRpS3A\tSrp19\nRpS3A\tCG9769\nRpS3A\tade3\nRpS3A\tTrip1\nRpS3A\tTango7\nRpS3A\teIF3-S9\nRpS3A\tTctp\nRpS3A\tRpS12\nRpS3A\tRpS13\nRpS3A\tRpS2\nRpS3A\tRpS17\nRpS3A\tRpS15Ab\nRpS3A\tRpL13\nRpS3A\tRpS15Aa\nRpS3A\tRpS8\nRpS3A\tRpS10a\nRpS3A\tRpS4\nRpS3A\tRpL11\nRpS3A\tRpS23\nRpS3A\tRpS7\nRpS3A\tRpLP2\nRpS3A\tsta\nRpS3A\tRpS6\nRpS3A\tRpL12\nRpS3A\tRpS11\nRpS3A\tRpL30\nRpS3A\tRpL3\nRpS3A\tRpS3\nRpS3A\tRpS14a\nRpS3A\tRpL10Ab\nRpS3A\tRpL18A\nRpS3A\tRpS27\nRpS3A\tRpL4\nRpS3A\tRpS16\nRpS3A\tRpS18\nRpS3A\tRpS10b\nRpS16\tmago\nRpS16\tRpS14a\nRpS16\tRpS6\nRpS16\tsta\nRpS15Ab\tvig\nRpS15Ab\tCG9769\nRpS15Ab\tade3\nRpS15Ab\tEfTuM\nRpS15Ab\tSrp19\nRpS15Ab\teIF3-S9\nRpS15Ab\teIF3-S10\nRpS15Ab\teIF3-S8\nRpS15Ab\tbonsai\nRpS15Ab\tmRpS10\nRpS15Ab\tSrp54k\nRpS15Ab\tTango7\nRpS15Ab\tTctp\nRpS15Ab\tmRpS9\nRpS15Ab\tmRpL12\nRpS15Ab\tRpS15Aa\nRpS15Ab\tRpS12\nRpS15Ab\tRpL10Ab\nRpS15Ab\tRpL13\nRpS15Ab\tRpL18A\nRpS15Ab\tRpL12\nRpS15Ab\tRpL3\nRpS15Ab\tRpL4\nRpS15Ab\tRpS10a\nRpS15Ab\tRpL11\nRpS15Ab\tRpS6\nRpS15Ab\tRpS2\nRpS15Ab\tRpS13\nRpS15Ab\tRpS23\nRpS15Ab\tRpS18\nRpS15Ab\tRpS3\nRpS15Ab\tRpS10b\nRpS15Ab\tRpS17\nRpS15Ab\tRpS14a\nRpS15Ab\tsta\nRpS15Ab\tRpS11\nRpS15Ab\tRpS27\nRpS15Ab\tRpS4\nRpS15Ab\tRpS16\nRpL3\teEF1delta\nRpL3\tbonsai\nRpL3\tTcp-1zeta\nRpL3\tTcp-1eta\nRpL3\tCG7033\nRpL3\tTctp\nRpL3\tmago\nRpL3\tSrp19\nRpL3\tmRpL12\nRpL3\tmRpS9\nRpL3\teIF3-S10\nRpL3\tCG10576\nRpL3\tCG5642\nRpL3\tCG9769\nRpL3\tSrp54k\nRpL3\tTrip1\nRpL3\tade3\nRpL3\tRpS14a\nRpL3\tRpS10b\nRpL3\tRpS15Aa\nRpL3\tRpS13\nRpL3\tRpS16\nRpL3\tRpS2\nRpL3\tRpS4\nRpL3\tRpS17\nRpL3\tsta\nRpL3\tRpL12\nRpL3\tRpL13\nRpL3\tRpL10Ab\nRpL3\tRpS12\nRpL3\tRpS6\nTcp-1zeta\tTim9a\nTcp-1zeta\tRpL12\nTcp-1zeta\tHsp60\nTcp-1zeta\tsta\nTcp-1zeta\tCG7033\nT-cp1\tRpS13\nT-cp1\tRpS2\nT-cp1\tRpL13\nT-cp1\tHop\nT-cp1\tsta\nT-cp1\tRpL12\nT-cp1\tAats-ile\nT-cp1\tRpS4\nT-cp1\tRpL3\nT-cp1\tCG10576\nT-cp1\tCG7033\nT-cp1\tCctgamma\nT-cp1\tTcp-1eta\nT-cp1\tTcp-1zeta\nRpS3\tTcp-1zeta\nRpS3\tvig\nRpS3\tCG7033\nRpS3\teEF1delta\nRpS3\tade3\nRpS3\tTcp-1eta\nRpS3\tSrp72\nRpS3\tmago\nRpS3\tbonsai\nRpS3\tmRpS10\nRpS3\tmRpS9\nRpS3\tmRpL12\nRpS3\tSrp19\nRpS3\tCG5642\nRpS3\tSrp54k\nRpS3\tCG9769\nRpS3\tTctp\nRpS3\teIF3-S10\nRpS3\tRpS15Aa\nRpS3\tRpL13\nRpS3\tRpS6\nRpS3\tRpS2\nRpS3\tRpL12\nRpS3\tRpL10Ab\nRpS3\tRpS14a\nRpS3\tRpS12\nRpS3\tRpS13\nRpS3\tRpS16\nRpS3\tRpS10b\nRpS3\tsta\nRpS3\tRpS4\nRpS3\tRpL3\nRpS3\tRpS17\nRpS3\tTrip1\nRpL26\tCG10576\nRpL26\teEF1delta\nRpL26\tEfTuM\nRpL26\tNacalpha\nRpL26\tbonsai\nRpL26\tade3\nRpL26\tmRpS10\nRpL26\tmRpS5\nRpL26\tTctp\nRpL26\teIF4G\nRpL26\tCG5642\nRpL26\tmago\nRpL26\teIF3-S10\nRpL26\tRbp2\nRpL26\tmRpS9\nRpL26\teIF3-S9\nRpL26\tmRpL12\nRpL26\teIF-3p66\nRpL26\teIF3-S8\nRpL26\tSrp72\nRpL26\teIF-2gamma\nRpL26\tmRpS7\nRpL26\tCG9769\nRpL26\tElf\nRpL26\tTango7\nRpL26\tTrip1\nRpL26\tEf1beta\nRpL26\tSrp19\nRpL26\teRF1\nRpL26\tSrp54k\nRpL26\tRpS15Aa\nRpL26\tRpS15Ab\nRpL26\tRpL10Ab\nRpL26\tRpL11\nRpL26\tRpL3\nRpL26\tRpS13\nRpL26\tRpS2\nRpL26\tRpS6\nRpL26\tRpS4\nRpL26\tRpS27\nRpL26\tRpL30\nRpL26\tRpL18A\nRpL26\tRpS12\nRpL26\tRpS10a\nRpL26\tRpS18\nRpL26\tRpS3A\nRpL26\tRpS8\nRpL26\tRpS17\nRpL26\tsta\nRpL26\tRpS14a\nRpL26\tRpS11\nRpL26\tRpS23\nRpL26\tRpL13\nRpL26\tRpL12\nRpL26\tRpS10b\nRpL26\tRack1\nRpL26\tRpL4\nRpL26\tRpS3\nRpL26\tRpS7\nRpL26\tRpS16\nRpL26\tRpLP2\nRpS12\tmago\nRpS12\tRpS14a\nRpS12\tRpS15Aa\nRpS12\tRpS10b\nRpS12\tRpS6\nRpS12\tRpS16\nRpS12\tRpL12\nRpS12\tsta\nCG5642\tRpL12\nCG5642\tRpS6\nCG5642\tsta\nCG5642\tRpS10b\nCG5642\tRpS12\nCG5642\tRpS16\nCG5642\tRpS4\nCG9769\tRpS15Aa\nCG9769\tRpS17\nCG9769\tRpS12\nCG9769\tRpL12\nCG9769\tRpS10b\nCG9769\tRpL10Ab\nCG9769\tRpS4\nCG9769\tsta\nCG9769\tRpS6\nCG9769\tRpS16\nCG9769\tCG5642\nCG7033\tRpS14a\nCG7033\tsta\nCct5\tRpS13\nCct5\tade3\nCct5\tProsalpha6\nCct5\tHsc70-5\nCct5\tRpt6\nCct5\tRpS3\nCct5\tRef1\nCct5\tRpS16\nCct5\tRpL12\nCct5\tRpL13\nCct5\tCG5642\nCct5\tRpL18A\nCct5\tRpS6\nCct5\tsta\nCct5\tRpL11\nCct5\tAats-ile\nCct5\tpont\nCct5\tTrip1\nCct5\tRpS4\nCct5\tTango7\nCct5\teIF3-S9\nCct5\tRpL3\nCct5\tRpS2\nCct5\tCG10576\nCct5\tRpL4\nCct5\tHop\nCct5\tCctgamma\nCct5\tTcp-1eta\nCct5\tT-cp1\nCct5\tTcp-1zeta\nCct5\tCG7033\nRpLP2\tCG10576\nRpLP2\tbonsai\nRpLP2\tmRpS10\nRpLP2\tSrp72\nRpLP2\tSrp54k\nRpLP2\tSrp19\nRpLP2\tmago\nRpLP2\teIF3-S8\nRpLP2\teIF3-S10\nRpLP2\tCG5642\nRpLP2\tRbp2\nRpLP2\tTango7\nRpLP2\teIF3-S9\nRpLP2\teIF-3p66\nRpLP2\tNacalpha\nRpLP2\tRpS15Aa\nRpLP2\tCG9769\nRpLP2\tTrip1\nRpLP2\tTctp\nRpLP2\tRpS10a\nRpLP2\tRpS10b\nRpLP2\tRpS27\nRpLP2\tRpS3\nRpLP2\tRpS13\nRpLP2\tRpS18\nRpLP2\tRpL4\nRpLP2\tRpS7\nRpLP2\tRpL13\nRpLP2\tRpS23\nRpLP2\tRpL10Ab\nRpLP2\tRpS15Ab\nRpLP2\tRpS6\nRpLP2\tRpS14a\nRpLP2\tRpS2\nRpLP2\tRpL3\nRpLP2\tRpL12\nRpLP2\tRpL18A\nRpLP2\tRpL11\nRpLP2\tRpS11\nRpLP2\tsta\nRpLP2\tRpS16\nRpLP2\tRpS17\nRpLP2\tRpS12\nRpLP2\tRpS4\nRpS23\tCG7033\nRpS23\tvig\nRpS23\teEF1delta\nRpS23\tbonsai\nRpS23\tmRpS10\nRpS23\tSrp72\nRpS23\tmago\nRpS23\tCG5642\nRpS23\tmRpL12\nRpS23\tmRpS9\nRpS23\tTctp\nRpS23\teIF3-S9\nRpS23\teIF3-S8\nRpS23\teIF3-S10\nRpS23\tCG9769\nRpS23\tTrip1\nRpS23\tSrp19\nRpS23\tSrp54k\nRpS23\tRpS16\nRpS23\tRpS27\nRpS23\tRpL18A\nRpS23\tRpS10b\nRpS23\tRpS15Aa\nRpS23\tRpS12\nRpS23\tRpS3\nRpS23\tRpS18\nRpS23\tRpS14a\nRpS23\tRpL11\nRpS23\tRpL3\nRpS23\tRpS2\nRpS23\tsta\nRpS23\tRpS6\nRpS23\tRpL10Ab\nRpS23\tRpL12\nRpS23\tRpL13\nRpS23\tRpS17\nRpS23\tRpL4\nRpS23\tRpS4\nRpS23\tRpS10a\nRpS23\tRpS13\nRpS14a\tsta\nRack1\tvig\nRack1\tbel\nRack1\tProsalpha7\nRack1\tRpn6\nRack1\tRpn2\nRack1\teEF1delta\nRack1\tCctgamma\nRack1\tEno\nRack1\tpAbp\nRack1\tNacalpha\nRack1\teIF3-S8\nRack1\tCG10576\nRack1\tCG9769\nRack1\tade3\nRack1\tDp1\nRack1\tT-cp1\nRack1\tTcp-1zeta\nRack1\tSrp54k\nRack1\tTcp-1eta\nRack1\tSrp19\nRack1\tCG8258\nRack1\tCG7033\nRack1\tCct5\nRack1\tmRpS5\nRack1\tbonsai\nRack1\tmRpS10\nRack1\teIF3-S10\nRack1\tTctp\nRack1\teIF3-S9\nRack1\teIF-2gamma\nRack1\tTrip1\nRack1\tRpS15Ab\nRack1\tTango7\nRack1\tRpS15Aa\nRack1\tRpS10a\nRack1\tRpS14a\nRack1\tRpS10b\nRack1\tRpL10Ab\nRack1\tRpS8\nRack1\tRpL3\nRack1\tRpS11\nRack1\tRpS17\nRack1\tRpS2\nRack1\tRpS16\nRack1\tRpS13\nRack1\tRpS12\nRack1\tRpS4\nRack1\tRpS3A\nRack1\tRpS3\nRack1\tRpS18\nRack1\tRpS23\nRack1\tRpS6\nRack1\tRpL12\nRack1\tRpL13\nRack1\tRpS27\nRack1\tRpL11\nRack1\tRpL18A\nRack1\tRpL30\nRack1\tRpS7\nRack1\tRpLP2\nRack1\tRpL4\nRack1\tsta\nRpL11\tCG7033\nRpL11\tvig\nRpL11\teEF1delta\nRpL11\tTcp-1eta\nRpL11\tmago\nRpL11\teIF3-S10\nRpL11\tCG10576\nRpL11\tCG5642\nRpL11\tbonsai\nRpL11\tmRpS10\nRpL11\tSrp72\nRpL11\tmRpS9\nRpL11\tTctp\nRpL11\tmRpL12\nRpL11\tCG9769\nRpL11\tSrp19\nRpL11\tSrp54k\nRpL11\tTrip1\nRpL11\tRpS15Aa\nRpL11\tRpS13\nRpL11\tRpS18\nRpL11\tRpL3\nRpL11\tRpL12\nRpL11\tRpS10b\nRpL11\tRpS17\nRpL11\tRpL10Ab\nRpL11\tRpS10a\nRpL11\tRpL13\nRpL11\tRpS4\nRpL11\tRpS16\nRpL11\tRpS12\nRpL11\tRpS27\nRpL11\tRpS2\nRpL11\tRpS3\nRpL11\tRpS6\nRpL11\tRpS14a\nRpL11\tRpL4\nRpL11\tsta\nRpn12\tUbqn\nProsbeta7\tUba2\nProsbeta7\tCG7033\nProsbeta7\tUch-L5\nProsbeta7\tRpt4R\nProsbeta7\tRpn5\nProsbeta7\tRpt6\nProsbeta7\tRpn12\nRpS13\tCG10576\nRpS13\tCG7033\nRpS13\tmRpL12\nRpS13\tade3\nRpS13\tmago\nRpS13\tCG5642\nRpS13\teIF3-S10\nRpS13\tCG9769\nRpS13\tSrp19\nRpS13\tSrp54k\nRpS13\tTrip1\nRpS13\tRpL10Ab\nRpS13\tRpS15Aa\nRpS13\tRpS12\nRpS13\tRpS17\nRpS13\tRpL12\nRpS13\tRpS10b\nRpS13\tsta\nRpS13\tRpS16\nRpS13\tRpS4\nRpS13\tRpS6\nRpS13\tRpS14a\nRpL18A\tTcp-1zeta\nRpL18A\tvig\nRpL18A\tCG7033\nRpL18A\teEF1delta\nRpL18A\tTcp-1eta\nRpL18A\tade3\nRpL18A\tmRpS10\nRpL18A\tmago\nRpL18A\tbonsai\nRpL18A\tCG10576\nRpL18A\tCG5642\nRpL18A\teIF3-S8\nRpL18A\teIF3-S10\nRpL18A\tSrp72\nRpL18A\teIF3-S9\nRpL18A\tTctp\nRpL18A\tSrp19\nRpL18A\tCG9769\nRpL18A\tSrp54k\nRpL18A\tTrip1\nRpL18A\tRpS15Aa\nRpL18A\tRpS14a\nRpL18A\tsta\nRpL18A\tRpL10Ab\nRpL18A\tRpS2\nRpL18A\tRpL3\nRpL18A\tRpS17\nRpL18A\tRpL4\nRpL18A\tRpL13\nRpL18A\tRpS4\nRpL18A\tRpL11\nRpL18A\tRpS10a\nRpL18A\tRpS6\nRpL18A\tRpL12\nRpL18A\tRpS18\nRpL18A\tRpS16\nRpL18A\tRpS12\nRpL18A\tRpS27\nRpL18A\tRpS10b\nRpL18A\tRpS3\nRpL18A\tRpS13\nRpS4\tTcp-1zeta\nRpS4\tCG7033\nRpS4\tmago\nRpS4\tbonsai\nRpS4\tRpS15Aa\nRpS4\tRpL12\nRpS4\tsta\nRpS4\tRpS10b\nRpS4\tRpS14a\nRpS4\tRpS16\nRpS4\tRpS6\nRpS4\tRpS12\nRpL4\tvig\nRpL4\tbonsai\nRpL4\tCctgamma\nRpL4\tmRpS10\nRpL4\tTcp-1eta\nRpL4\tTctp\nRpL4\tT-cp1\nRpL4\tCG7033\nRpL4\tTcp-1zeta\nRpL4\tade3\nRpL4\tmRpS9\nRpL4\tmRpL12\nRpL4\tSrp72\nRpL4\tmago\nRpL4\tSrp19\nRpL4\tCG5642\nRpL4\tCG10576\nRpL4\tSrp54k\nRpL4\tCG9769\nRpL4\teIF3-S10\nRpL4\tRpS15Aa\nRpL4\tRpS10a\nRpL4\tRpL10Ab\nRpL4\tTrip1\nRpL4\tRpS16\nRpL4\tRpS6\nRpL4\tRpS12\nRpL4\tRpS14a\nRpL4\tRpS2\nRpL4\tRpL12\nRpL4\tRpS27\nRpL4\tsta\nRpL4\tRpS13\nRpL4\tRpS10b\nRpL4\tRpS3\nRpL4\tRpS17\nRpL4\tRpL13\nRpL4\tRpL3\nRpL4\tRpS4", - }, - { - kind: 'annotation', - name: 'Tau Louvain communities', - fileName: 'TAU_Louvain.txt', - text: "Group-1\tMgstl,KP78a,CG9257,par-1,CG31064,LpR1,Rab39,LpR2,CG3529,Lam,LamC,Sap-r,Vps60,lqf,ninaC,Hrs,Arr2,Arr1,Rab5,CG32683,pins,chp\nGroup-2\tSc2,Uch,Prx2540-1,Ace,CG4572,Sgt,CG14715,CG10359,Ect3,CalpB,Prx5,Ost48,Hsp60C,Prx2540-2,Hsp60,Ubqn,Hsp70Ab,Ugt,Iswi,Trap1,Cat,Hsc70-5,CG8209,DnaJ-H,p47,Rad23,Cnx99A,pont,Cp1,cathD,Uba2,Pfk,Gdi,Uch-L5,Rpt4R,Prosalpha1,mor,Bap55,Ufd1-like,TER94,Hsc70-3,PyK,Hsp83,ERp60,arm,Rpt1,Rpn13,Rpt2,Rpn5,Rpt6R,Rpn2,Pdi,CaBP1,Gp93,Rpt4,Hop,CG1416,Prosalpha6,Rpt5,Prosalpha7,Rpn6,CG8258,Rpt6,Rpn3,Rpn12,Prosbeta7,Usp5,CG2918,CG2852,CG1354,CG6891,HDAC6,Hsp27,His1,CG11577\nGroup-3\tshep,CG5590,CG17597,ScpX,Mfe2\nGroup-4\tCG4882,EfTuM,Rbp2,eEF1delta,CG10576,Nacalpha,mRpS34,vig,CG4679,mRpS35,mRpS7,mRpL44,mRpS22,eIF4G,bonsai,mRpS10,Srp54k,mRpL12,Tctp,eIF-2gamma,mRpS23,mRpS18B,ade3,mRpS9,mRpS5,Srp19,Srp72,eIF-3p66,Cctgamma,Ef1beta,Tango7,RpL10Ab,Trip1,eIF3-S10,eIF3-S9,Tcp-1eta,RpL13,RpS11,eIF3-S8,RpS6,RpS2,RpL12,RpS7,RpL10,RpS8,RpL30,RpS10a,RpS17,RpLP0,eRF1,Elf,RpS15Aa,RpS27,RpS18,RpS10b,RpS3A,RpS16,RpS15Ab,RpL3,Tcp-1zeta,T-cp1,RpS3,RpL26,RpS12,CG5642,CG9769,CG7033,Cct5,RpLP2,RpS23,RpS14a,Rack1,RpL11,RpS13,RpL18A,RpS4,RpL4,sta,wuho,bsf,kra,Hsp67Bc,su(r),FeCH,CG8635\nGroup-5\tnito,bol,Kap-alpha3,CG2082,lig,clu,Dbp80,Plc21C,Hrb27C,bel,Dp1,Lpin,Pkc53E,Nlp,rin,poe,CG43367,emb,fne,RanBPM,yps,Art4,CG10077,mub,AGO1,glo,Srp54,Rbp9,Rae1,Mtor,Ref1,mago,B52,LS2,U2af50,Fmr1,Top2,heph,Hrb98DE,Tudor-SN,SF2,me31B,CG16941,Prp8,Tango4,U2A,smt3,pAbp,Not1,Not3,CG2091,Capr,Gbeta76C,CG10777,CG6617,CG3800,tyf,CG11505\nGroup-6\tCG6178,frj,CG9090,Ssadh,l(1)G0156,CG32026,Men-b,CG11899,CG16935,AGBE,ImpL3,Nc73EF,CG3902,CG11876,Pdk,Vha100-1,AcCoAS,ACC,Mdh2,Cyt-c-p,Aldh,CG15093,ND-51L1,ND-51,Vha68-2,Vha100-2,ND-15,CG12262,yip2,ade5,CG11089,COX6B,CG11752,COX4,COX5A,Eno,Tpi,ND-PDSW,NP15.6,ATPsynbeta,ATPsynCF6,CG10932,CG33791,CG3011,Men,sun,porin,ND-B16.6,CG7603,Mpcp\nGroup-7\tNdg,trol,Cg25C,vkg,LanA,LanB2,Glt\nGroup-8\tLar,CG31715,PlexA,gammaSnap2,CG10186,Klp10A,Mtl,bai,Act79B,Abl,Tim17b,p115,Sec24AB,robl,tsr,flr,FKBP59,bic,BicD,CadN,syd,Rac1,zip,alphaCOP,zetaCOP,CG7048,CG7770,Dlic,Dhc64C,betaCOP,cpa,cpb,epsilonCOP,beta'COP,Klc,Khc,alpha-Cat,deltaCOP,RhoGDI,Dscam1,Tim9a,sqh,dock,Dscam2,CG8498,CLIP-190,betaTub56D\nGroup-9\tMapmodulin,Aats-val,Aats-his,Aats-asp,CG33123,Aats-ile,Aats-arg,CG31739\n", - }, - { - kind: 'annotation', - name: 'Tau KEGG pathways', - fileName: 'TAU_KEGG_Annotation_NORMA.txt', - text: 'Ribosome\tRpL10,RpL10Ab,RpL11,RpL12,RpL13,RpL18A,RpL26,RpL3,RpL30,RpL4,RpLP0,RpLP2,RpS10a,RpS10b,RpS11,RpS12,RpS13,RpS15Aa,RpS15Ab,RpS16,RpS17,RpS18,RpS2,RpS23,RpS27,RpS3,RpS3A,RpS4,RpS6,RpS7,RpS8,bonsai,mRpL12,mRpS10,mRpS7,mRpS9,sta\nProteasome\tProsalpha6,Prosalpha7,Prosbeta7,Rpn12,Rpn13,Rpn2,Rpn3,Rpn5,Rpn6,Rpt1,Rpt2,Rpt4,Rpt4R,Rpt5,Rpt6,Rpt6R\nSpliceosome\tB52,CG10077,CG10777,CG16941,Hrb98DE,LS2,Prp8,Ref1,SF2,Tango4,U2A,U2af50,mago\nPhagosome\tAct79B,Cnx99A,Cp1,Dhc64C,Dlic,Hrs,Mtl,Rab5,Rac1,Vha100-1,Vha100-2,Vha68-2,betaTub56D\nCarbon metabolism\tAcCoAS,CG10932,CG11876,CG11899,CG12262,CG3011,CG32026,CG33791,Cat,Eno,Mdh2,Men,Men-b,Nc73EF,Pfk,PyK,Tpi,l(1)G0156\nOxidative phosphorylation\tATPsynCF6,ATPsynbeta,COX4,COX5A,COX6B,ND-15,ND-51,ND-51L1,ND-B16.6,ND-PDSW,NP15.6,Vha100-1,Vha100-2,Vha68-2,sun\nRNA degradation\tCG2091,Eno,Hsc70-5,Hsp60,Hsp60C,Not1,Not3,Pfk,me31B,pAbp\nRNA transport\tCG9769,Fmr1,Mtor,Rae1,Ref1,Trip1,eIF-3p66,eIF3-S10,eIF3-S8,eIF3-S9,eIF4G,emb,mago,pAbp,smt3\nGlycolysis\tAcCoAS,Aldh,CG11876,Eno,ImpL3,Pfk,PyK,Tpi\nPyruvate metabolism\tACC,AcCoAS,Aldh,CG10932,CG11876,ImpL3,Mdh2,Men,Men-b,PyK\n', - }, - { - kind: 'colors', - name: 'Tau expression', - fileName: 'TAU_expressions.txt', - text: "CG7603\tred\nHis1\tred\nAct79B\tred\nGbeta76C\tred\nCG11876\tred\nKap-alpha3\tred\nCG2082\tred\nLanA\tred\nVha68-2\tred\nCalpB\tred\nSsadh\tred\nCG43367\tred\nl(1)G0156\tred\nCG9090\tred\nPrx5\tred\nCG1354\tred\nporin\tred\nGstD9\tred\nCG32026\tred\nAldh\tred\nGlt\tred\nTpi\tred\nPyK\tred\nATPsynbeta\tred\nEno\tred\nMtl\tred\nArr1\tred\nMpcp\tred\nArr2\tred\nNdg\tred\nFeCH\tred\nNc73EF\tred\nCG10932\tred\nCG33791\tred\nbetaTub56D\tred\nCyt-c-p\tred\nCG16935\tred\nPrx2540-2\tred\nGstE4\tred\npins\tred\nchp\tred\nMen-b\tred\nGstD3\tred\nHDAC6\tred\nCG10359\tred\nmago\tred\nLamC\tred\nAce\tred\nMdh2\tred\nninaC\tred\nLanB2\tred\nCg25C\tred\nsun\tred\nvkg\tred\nPfk\tred\nPrx2540-1\tred\nEfTuM\tred\nVha100-1\tred\nVha100-2\tred\nAGBE\tred\nfrj\tred\nDnaJ-H\tred\ntrol\tred\nMen\tgreen\np47\tgreen\nmRpS10\tgreen\nNP15.6\tgreen\nRpS15Ab\tgreen\nNot3\tgreen\nTim17b\tgreen\neIF4G\tgreen\ncathD\tgreen\nCG12262\tgreen\nProsalpha7\tgreen\nUsp5\tgreen\nyip2\tgreen\nDscam1\tgreen\nCG2918\tgreen\ncpa\tgreen\nRpS4\tgreen\nade3\tgreen\nCG8209\tgreen\nCG11505\tgreen\nFKBP59\tgreen\nRpS11\tgreen\nRab39\tgreen\nRpL13\tgreen\nCG1371\tgreen\nPrp8\tgreen\nProsalpha6\tgreen\nRpn12\tgreen\nRpn2\tgreen\nRpn5\tgreen\nNacalpha\tgreen\nHsp60\tgreen\nTudor-SN\tgreen\nRpt5\tgreen\nRpt4\tgreen\nlqf\tgreen\nRpt1\tgreen\nTop2\tgreen\nRpn6\tgreen\nND-PDSW\tgreen\nsta\tgreen\nsqh\tgreen\nCG6178\tgreen\nCG32683\tgreen\nAats-asp\tgreen\nLpR2\tgreen\neIF-2gamma\tgreen\nRpL11\tgreen\nElf\tgreen\nERp60\tgreen\nSec24AB\tgreen\nCG11089\tgreen\nRae1\tgreen\nCG3011\tgreen\nAbl\tgreen\nRpt6R\tgreen\nUch\tgreen\nTrap1\tgreen\nCG7048\tgreen\nRpt6\tgreen\nCG10186\tgreen\nwuho\tgreen\nAats-arg\tgreen\nLam\tgreen\nHsp60C\tgreen\nTrip1\tgreen\nRpS3\tgreen\nTctp\tgreen\nCG31064\tgreen\nDscam2\tgreen\nemb\tgreen\nUba2\tgreen\npAbp\tgreen\nCapr\tgreen\nCG4972\tgreen\nAats-his\tgreen\nAats-ile\tgreen\nPlexA\tgreen\nCctgamma\tgreen\nRab5\tgreen\nzip\tgreen\nMi-2\tgreen\nAcCoAS\tgreen\nsu(r)\tgreen\nmRpS23\tgreen\narm\tgreen\nCG10077\tgreen\nRef1\tgreen\nRpS7\tgreen\nCG10777\tgreen\nAats-val\tgreen\nImpL3\tgreen\nPdi\tgreen\nSgt\tgreen\nbeta'COP\tgreen\nalphaCOP\tgreen\nFmr1\tgreen\nCG31715\tgreen\nmRpS7\tgreen\nEf1beta\tgreen\nND-B16.6\tgreen\nScpX\tgreen\nCat\tgreen\nlig\tgreen\nCG10576\tgreen\np115\tgreen\nade5\tgreen\nBap55\tgreen\nHsp83\tgreen\nRpL4\tgreen\nProsbeta7\tgreen\nRpLP2\tgreen\nCG4572\tgreen\nCG31739\tgreen\nTim9a\tgreen\npont\tgreen\nHsc70-5\tgreen\nHsp27\tgreen\nbic\tgreen\nHsp67Bc\tgreen\nKP78a\tgreen\nCG3529\tgreen\nBicD\tgreen\nRpS12\tgreen\nbsf\tgreen\nHsc70-3\tgreen\nHrb98DE\tgreen\nRpS8\tgreen\nCG8498\tgreen\nDp1\tgreen\nOst48\tgreen\nSrp54k\tgreen\nRad23\tgreen\nTango4\tgreen\nkra\tgreen\nHsp70Ab\tgreen\nRpS15Aa\tgreen\ntyf\tgreen\nLpin\tgreen\nCnx99A\tgreen\nmRpS18B\tgreen\neEF1delta\tgreen\nCG3800\tgreen\nRpL18A\tgreen\npar-1\tgreen\nme31B\tgreen\nCOX4\tgreen\neIF3-S10\tgreen\nCG11752\tgreen\nCG14715\tgreen\nMtor\tgreen\nU2af50\tgreen\nmRpS5\tgreen\nKhc\tgreen\nCct5\tgreen\nRpL12\tgreen\nyps\tgreen\nB52\tgreen\nCG1416\tgreen\nbonsai\tgreen\nTER94\tgreen\nLar\tgreen\nalpha-Cat\tgreen\nglo\tgreen\nCadN\tgreen\nCG7033\tgreen\nCG2091\tgreen\nRpS17\tgreen\nArt4\tgreen\nsyd\tgreen\nRpL3\tgreen\nTcp-1eta\tgreen\nCG4679\tgreen\nSrp54\tgreen\nmRpS34\tgreen\nvig\tgreen\nSrp72\tgreen\nND-15\tgreen\nMapmodulin\tgreen\nRack1\tgreen\nCp1\tgreen\nDlic\tgreen\nRpS18\tgreen\nfne\tgreen\nRpS14a\tgreen\neIF3-S8\tgreen\nCG5642\tgreen\nRpLP0\tgreen\ngammaSnap2\tgreen\nrobl\tgreen\nRpS16\tgreen\nUgt\tgreen\nHrs\tgreen\nKlc\tgreen\neIF-3p66\tgreen\nND-51L1\tgreen\nbetaCOP\tgreen\nzetaCOP\tgreen\nUbqn\tgreen\nrin\tgreen\neRF1\tgreen\nLS2\tgreen\nDbp80\tgreen\nUfd1-like\tgreen\neIF3-S9\tgreen\nND-51\tgreen\nCOX6B\tgreen\nmRpS9\tgreen\nCG33123\tgreen\nCG9769\tgreen\nbel\tgreen\nSap-r\tgreen\ndeltaCOP\tgreen\nRbp9\tgreen\nRpS13\tgreen\nCG9257\tgreen\nmRpL12\tgreen\nMfe2\tgreen\nCaBP1\tgreen\nAGO1\tgreen\nMgstl\tgreen\nCG17597\tgreen\nCG6617\tgreen\nmub\tgreen\nATPsynCF6\tgreen\nRbp2\tgreen\nUch-L5\tgreen\ndock\tgreen\nLpR1\tgreen\nCG6891\tgreen\nRpL10\tgreen\nheph\tgreen\nHop\tgreen\nRpL10Ab\tgreen\nCOX5A\tgreen\nRpL30\tgreen\nmRpS22\tgreen\nTcp-1zeta\tgreen\nNlp\tgreen\nRpt4R\tgreen\nEct3\tgreen\npoe\tgreen\nVps60\tgreen\ntsr\tgreen\nRanBPM\tgreen\nPlc21C\tgreen\nmor\tgreen\nepsilonCOP\tgreen\nRpt2\tgreen\nACC\tgreen\nRpn3\tgreen\nbol\tgreen\nRpS10a\tgreen\nCG4882\tgreen\nCG8258\tgreen\nbai\tgreen\nshep\tgreen\nSrp19\tgreen\nCG2852\tgreen\nCG11899\tgreen\nclu\tgreen\nIswi\tgreen\nDhc64C\tgreen\nRpS6\tgreen\nRpS10b\tgreen\nCG5590\tgreen\nKlp10A\tgreen\nGdi\tgreen\nRpS2\tgreen\nRpS27\tgreen\nNot1\tgreen\nflr\tgreen\nPkc53E\tgreen\nCG11577\tgreen\nCG15093\tgreen\nCLIP-190\tgreen\nsmt3\tgreen\nRac1\tgreen\ncpb\tgreen\nCG16941\tgreen\nRhoGDI\tgreen\nProsalpha1\tgreen\nmRpL44\tgreen\nGp93\tgreen\nPdk\tgreen\nRpS23\tgreen\nT-cp1\tgreen\nRpL26\tgreen\nCG3902\tgreen\nCG7770\tgreen\nmRpS35\tgreen\nCG8635\tgreen\nRpS3A\tgreen\nTango7\tgreen\nHrb27C\tgreen\nnito\tgreen\nSc2\tgreen\nU2A\tgreen\nRpn13\tgreen\nSF2\tgreen\n", - }, - ], - }, - coexpr: { - title: 'Human gene co-expression', - source: 'PMID 19081792, PMCID PMC2597745', - link: 'https://doi.org/10.1371/journal.pone.0003911', - files: [ - { - kind: 'network', - name: 'Human co-expression', - fileName: 'NORMA_Human_coexpression_NETWORK.txt', - text: 'Source\tTarget\nRPL24\tRPL41\nRPL24\tRPL26\nRPL24\tRPL30\nRPL24\tTPT1\nRPL24\tRPL9\nRPL24\tRPS25\nRPL24\tRPS23\nRPL24\tRPL31\nRPL24\tRPS27A\nRPL24\tU16\nRPL24\tRPS4X\nRPL24\tRPL7\nRPL24\tRPL4\nRPL24\tRPL5\nRPL24\tRPS3A\nRPL24\tRPS7\nRPL24\tRPL34\nRPL24\tRPL35A\nRPL24\tRPS24\nRPL24\tRPL6\nRPL41\tRPL5\nRPL41\tRPS3A\nRPL41\tTPT1\nRPL41\tRPS23\nRPL41\tRPS4X\nHNRPA1\tRPS25\nHNRPA1\tRPS24\nHNRPA1\tRPS4X\nHNRPA1\tRPS6\nHNRPA1\tRPL22\nHNRPA1\tNCL\nHNRPA1\tRPL24\nHNRPA1\tNPM1\nHNRPA1\tRPL4\nHNRPA1\tMDS1\nHNRPA1\tU16\nHNRPA1\tRPS23\nHNRPA1\tRPS7\nHNRPA1\tSF3B1\nHNRPA1\tNAP1L1\nHNRPA1\tRPL17\nHNRPA1\tRPL11\nHNRPA1\tRPL6\nHNRPA1\tRPL34\nHNRPA1\tLOC440055\nHNRPA1\tRPS3A\nHNRPA1\tRPL7\nRPS25\tRPS4X\nRPS25\tRPS3A\nRPS25\tRPS6\nRPS25\tU16\nRPS25\tRPS7\nRPS25\tRPS27A\nH3F3A\tRPA1\nH3F3A\tRAP1B\nH3F3A\tRPLP2\nH3F3A\tRPS20\nH3F3A\tRPL27\nH3F3A\tRPS11\nH3F3A\tRPL27A\nCOX8A\tCYC1\nCOX8A\tNDUFS8\nCOX8A\tSLC25A11\nCOX8A\tSLC25A3\nCOX8A\tNDUFAB1\nCOX8A\tNDUFB11\nCOX8A\tMRPS12\nCYC1\tETFB\nCYC1\tUQCRC1\nATP8B1\tPDE4C\nATP8B1\tFLJ42393\nATP8B1\tDKFZP566N034\nATP8B1\tCEP27\nATP8B1\tKNS2\nATP8B1\tRIOK3\nATP8B1\tDBT\nATP8B1\tPGF\nATP8B1\tZNF611\nATP8B1\tSLC35E1\nATP8B1\tLOC152719\nATP8B1\tFBXW12\nATP8B1\tZNF160\nPDE4C\tSPG21\nPDE4C\tUBE2D2\nPDE4C\tPRKAR2A\nPDE4C\tZNF611\nPDE4C\tPRR11\nPDE4C\tPGF\nPDE4C\tZNF160\nPDE4C\tRIOK3\nPDE4C\tSLC35E1\nLOC58486\tSUB1\nLOC58486\tMGEA5\nLOC58486\tRNPC2\nLOC58486\tPFAAP5\nLOC58486\tWAC\nLOC58486\tMATR3\nLOC58486\tPNRC2\nLOC58486\tPAPOLA\nLOC58486\tZNF638\nLOC58486\tSF3B1\nLOC58486\tUSP34\nLOC402057\tRPS7\nLOC402057\tRPS15A\nLOC402057\tRPL34\nLOC402057\tRPS4X\nLOC402057\tRPS6\nLOC402057\tRPL31\nLOC402057\tRPL35A\nLOC402057\tRPL37\nLOC402057\tRPL22\nLOC402057\tRPL4\nLOC402057\tRPL9\nLOC402057\tRPS23\nLOC402057\tRPL11\nLOC402057\tLOC440055\nLOC402057\tRPL30\nLOC402057\tRPL23\nLOC402057\tRPL36A\nLOC402057\tRPL39\nLOC402057\tMDS1\nLOC402057\tRPL32\nLOC402057\tRPS17\nRPS7\tSFRS5\nRPS7\tSF3B1\nRPS7\tU16\nMDS1\tRPL23\nMDS1\tRNPC2\nMDS1\tRPL17\nMDS1\tRPL5\nMDS1\tRPS3A\nMDS1\tRPL41\nMDS1\tRPL4\nMDS1\tTPT1\nMDS1\tNPM1\nMDS1\tRPS6\nMDS1\tRPL7\nMDS1\tRPL24\nMDS1\tRPL34\nMDS1\tRPL26\nMDS1\tRPL22\nMDS1\tRPS27A\nMDS1\tRPL6\nMDS1\tRPS25\nMDS1\tRPS15A\nMDS1\tU16\nMDS1\tRPS7\nMDS1\tRPS24\nMDS1\tRPL36A\nMDS1\tRPS4X\nMDS1\tRPL21\nMDS1\tRPL31\nMDS1\tRPL35A\nMDS1\tRPL30\nMDS1\tRPS17\nMDS1\tRPL9\nMDS1\tRPL11\nMDS1\tRPS23\nRPL23\tRPL7\nRPL23\tRPL26\nRPL23\tRPL5\nRPL23\tRPL24\nRPL23\tRPS27\nRPL23\tRPS15A\nRPL23\tRPS27A\nRPL23\tRPL6\nRPL23\tRPL36A\nRPL23\tU16\nRPL23\tRPS25\nRPL23\tRPS17\nRPL23\tRPS4X\nRPL23\tRPS7\nRPL23\tRPS3A\nRPL23\tRPS23\nRPL23\tRPL30\nRPL23\tRPS24\nRPL23\tRPL9\nRPL23\tRPL31\nRPL23\tRPS6\nRPL23\tRPL35A\nRPL23\tRPL4\nRPL23\tRPL34\nRPL5\tRPS6\nRPL5\tRPS23\nRPL5\tTPT1\nRPL5\tRPS25\nRPL5\tRPS3A\nRPL5\tRPS4X\nRPL5\tRPL9\nRPL5\tRPS27A\nRPL5\tRPL7\nRPL5\tRPS7\nRPL5\tU16\nRPL5\tRPS24\nRPL5\tRPL6\nATP5G3\tCOX8A\nATP5G3\tNDUFB11\nATP5G3\tSLC25A3\nATP5G3\tUQCRC1\nATP5G3\tUQCRQ\nATP5G3\tCOX5A\nATP5G3\tCOX4I1\nATP5G3\tNDUFAB1\nATP5G3\tUQCRFS1\nATP5G3\tCOX5B\nATP5G3\tMDH2\nCOPB\tTRAM1\nCOPB\tLOC58486\nCOPB\tRNPC2\nCOPB\tHSP90B1\nCOPB\tYME1L1\nCOPB\tPAPOLA\nPRKACB\tSEPT7\nSEPT7\tSH3BGRL\nSEPT7\tZFR\nUBE2D2\tZNF160\nRNPC2\tTAF7\nRNPC2\tTCEA1\nRNPC2\tZNF638\nRNPC2\tRPS3A\nRNPC2\tSF3B1\nRNPC2\tRPL5\nRNPC2\tRPL6\nRNPC2\tRPL17\nRNPC2\tSRP72\nIGHM\tIGKC\nIGHM\tIGLC2\nIGHM\tIGKV1-5\nIGHM\tLOC91316\nIGHM\tIGLV3-10\nIGHM\tIGL@\nIGHM\tIGHV1-69\nIGHM\tIGKV1D-13\nIGHM\tIGLV3-25\nIGHM\tIGLC1\nIGHM\tIGKV1OR2-108\nIGKC\tIGLJ3\nIGKC\tIGLC2\nIGKC\tIGKV1D-13\nIGKC\tIGLV3-25\nIGKC\tIGKV1OR2-108\nIGKC\tIGKV1-5\nIGKC\tIGLV3-10\nIGKC\tIGL@\nIGKC\tLOC91316\nIGKC\tIGLC1\nIGKC\tIGKV1OR15-118\nLOC342346\tYTHDC1\nLOC342346\tXPO1\nLOC342346\tSON\nLOC342346\tTAF7\nLOC342346\tPAPOLA\nLOC342346\tSH3BGRL\nEEF1A1\tRPL17\nEEF1A1\tMDS1\nEEF1A1\tRPS24\nEEF1A1\tRPS3A\nEEF1A1\tRPL22\nEEF1A1\tNPM1\nEEF1A1\tRPL11\nEEF1A1\tRPS7\nEEF1A1\tRPS27A\nEEF1A1\tTPT1\nEEF1A1\tRPS6\nEEF1A1\tRPL7\nEEF1A1\tRPS4X\nEEF1A1\tRPL23\nRPL17\tSFRS5\nRPL17\tRPS25\nRPL17\tRPL9\nRPL17\tTPT1\nRPL17\tRPL30\nRPL17\tRPL26\nRPL17\tRPL7\nRPL17\tRPL36A\nRPL17\tRPL22\nRPL17\tRPS6\nRPL17\tSF3B1\nRPL17\tRPS4X\nRPL17\tTINP1\nRPL17\tRPL35A\nRPL17\tRPS23\nRPL17\tU16\nRPL17\tRPS3A\nRPL17\tRPL24\nRPL17\tRPS7\nRPL17\tRPL23\nRPL17\tRPL31\nRPL17\tRPS27A\nRPL17\tRPL21\nRPL17\tRPL4\nRPL17\tRPL5\nRPL17\tRPL34\nRPL17\tRPL6\nRPL17\tRPS24\nRPL13A\tRPS9\nRPL13A\tRPS18\nRPL13A\tRPL18A\nRPL13A\tRPL27A\nRPL13A\tRPL14\nRPL13A\tRPS16\nRPL13A\tRPS14\nRPL13A\tRPL15\nRPL13A\tRPLP2\nRPL13A\tRPS21\nRPL13A\tRPL29\nRPL13A\tRPS19\nRPL13A\tRPL36\nRPL13A\tRPS5\nNDUFAB1\tNDUFB4\nNDUFAB1\tNDUFS3\nNDUFAB1\tUQCRC1\nNDUFAB1\tNDUFV1\nNDUFAB1\tUQCR\nNDUFAB1\tNDUFB2\nNDUFAB1\tUQCRQ\nNDUFAB1\tNDUFS7\nNDUFAB1\tNDUFB11\nNDUFAB1\tNDUFC1\nNDUFAB1\tPOLR2I\nNDUFAB1\tNDUFS8\nNDUFAB1\tUQCRFS1\nNDUFB4\tNDUFS3\nNDUFB4\tNDUFS8\nNDUFB4\tUQCRFS1\nNDUFB4\tUCRC\nAKR7A2\tCLPP\nAKR7A2\tTIMM13\nAKR7A2\tNDUFV1\nAKR7A2\tMRPL34\nAKR7A2\tC21orf33\nAKR7A2\tNDUFS8\nCLPP\tTUFM\nCLPP\tMRPL34\nhfl-B5\tHNRPA2B1\nhfl-B5\tHIATL1\nhfl-B5\tHNRPH1\nhfl-B5\tHNRPA1\nHNRPA2B1\tXPO1\nHNRPA2B1\tSRP46\nSET\tSFRS3\nSET\tSRP46\nSET\tSON\nSET\tSFRS10\nHLA-C\tISG20\nHLA-C\tMAN2B1\nHLA-C\tHLA-G\nHLA-C\tHLA-F\nHLA-C\tHLA-E\nHLA-C\tTAPBP\nHLA-C\tHLA-J\nISG20\tRAC2\nISG20\tPSMB9\nISG20\tUCP2\nRPS24\tTPT1\nRPS24\tTCEA1\nRPS24\tSF3B1\nRPS24\tRPS25\nRPS24\tRPS3A\nRPS24\tRPS6\nRPS24\tRPS4X\nRPS24\tU16\nRPS24\tRPS27A\nRPS24\tRPS7\nDDX1\tKTN1\nKTN1\tUSP47\nRPS4X\tU16\nRPS4X\tTPT1\nRPS4X\tRPS7\nRPS4X\tRPS6\nCCNL1\tRPL4\nCCNL1\tZNF638\nCCNL1\tNACA\nCCNL1\tRPL5\nCCNL1\tRPS27A\nCCNL1\tRPL17\nCCNL1\tRNPC2\nCCNL1\tRPL21\nCCNL1\tRPL6\nCCNL1\tEIF3S6\nRPL4\tTPT1\nRPL4\tRPL7\nRPL4\tSFRS5\nRPL4\tRPS15A\nRPL4\tRPL5\nRPL4\tRPS17\nRPL4\tRPS25\nRPL4\tRPL9\nRPL4\tRPS7\nRPL4\tRPS27A\nRPL4\tRPL6\nRPL4\tRPS23\nRPL4\tRPS4X\nRPL4\tRPS3A\nRPL4\tRPS24\nRPL4\tU16\nRPL4\tRPS6\nFLJ11021\tNAP1L1\nFLJ11021\tZNF403\nFLJ11021\tHIATL1\nFLJ11021\tKIAA0907\nFLJ11021\tWAC\nFLJ11021\tTUG1\nFLJ11021\tPAPOLA\nFLJ11021\tRNPC2\nFLJ11021\tXPO1\nFLJ11021\tLOC58486\nFLJ11021\tNPM1\nNAP1L1\tRPL17\nNAP1L1\tYTHDC1\nNAP1L1\tPAPOLA\nNAP1L1\tZNF9\nNAP1L1\tXPO1\nC21orf33\tETFB\nC21orf33\tCOX5B\nC21orf33\tNDUFS7\nC21orf33\tNDUFS8\nETFB\tNDUFS8\nETFB\tUQCRQ\nCOX5A\tNDUFS8\nCOX5A\tNDUFB11\nCOX5A\tNDUFS7\nCOX5A\tMDH2\nCOX5A\tCOX5B\nCOX5A\tNDUFAB1\nCOX5A\tUQCRFS1\nCOX5A\tUQCRC1\nNDUFS8\tSLC25A3\nNDUFS8\tPOLR2I\nNDUFS8\tPOLR2L\nNDUFS8\tTIMM13\nNDUFS8\tUQCRFS1\nNDUFS8\tTMEM93\nNDUFS8\tUQCRQ\nCROP\tIL6ST\nCROP\tZNF638\nCROP\tHNRPA3P1\nCROP\tRNPC2\nCROP\tMATR3\nCROP\tSFRS5\nCROP\tXPO1\nCROP\tSFPQ\nCROP\tLOC58486\nCROP\tHNRPDL\nCROP\tTUG1\nCROP\tSFRS11\nCROP\tZC3H11A\nCROP\tPFAAP5\nCROP\tNPM1\nCROP\tFLJ10154\nCROP\tGOLGA8A\nIL6ST\tSFRS11\nPOLR2E\tSIAHBP1\nRPL26\tRPS3A\nRPL26\tRPL4\nRPL26\tRPL7\nRPL26\tRPS7\nRPL26\tRPS6\nRPL26\tRPS27A\nRPL26\tRPL34\nRPL26\tRPS24\nRPL26\tRPS25\nRPL26\tRPL5\nRPL26\tRPL6\n216858_x_at\tCEP27\n216858_x_at\tZNF611\n216858_x_at\tFLJ14346\n216858_x_at\tLOC152719\nCEP27\tPRR11\nCEP27\tPGF\nCEP27\tPRKAR2A\nCEP27\tLOC152719\nCEP27\tPDE4C\nCEP27\tRIOK3\nCEP27\tZNF611\nCEP27\tKNS2\nCEP27\tFBXW12\nCEP27\tDBT\nCEP27\tZNF160\nCEP27\tSLC35E1\nCEP27\tFLJ12151\nRPL6\tRPL9\nRPL6\tSFRS5\nRPL6\tTPT1\nRPL6\tRPL7\nRPL6\tRPS23\nRPL6\tSF3B1\nRPL6\tRPS4X\nRPL6\tRPS25\nRPL6\tRPS3A\nRPL6\tRPS6\nRPL6\tU16\nRPL6\tRPS7\nRPL6\tRPS27A\nRPL6\tRPS24\nRPL9\tRPS27A\nRPL9\tRPS24\nRPL9\tU16\nRPL9\tRPS25\nRPL9\tRPS4X\nRPL9\tRPS23\nRPL9\tRPS7\nRPL9\tRPS3A\nRPL9\tRPS17\nRPL9\tRPS6\nRPL9\tRPS27\nRPL9\tRPS15A\nDDX3X\tXPO1\nDDX3X\tNAP1L1\nDDX3X\tPAPOLA\nDDX3X\tSON\nDDX3X\tHNRPR\nDDX3X\tTHRAP1\nDDX3X\tSH3BGRL\nDDX3X\tHNRPH1\nDDX3X\tMGEA5\nDDX3X\tHIATL1\nDDX3X\tDDX5\nDDX3X\tLOC342346\nDDX3X\tEIF4G2\nDDX3X\tSF3B1\nDDX3X\tYTHDC1\nCOPE\tSCAND1\nCOPE\tMRPS12\nCOPE\tNAPA\nCOPE\tFIBP\nSCAND1\tTHRAP5\nCOPS2\tRPL15\nCOPS2\tHSP90B1\nCOPS2\tHNRPH3\nCOPS2\tZNF638\nCOPS2\tKIDINS220\nCOPS2\tVDP\nCOPS2\tTLOC1\nCOPS2\tCSDE1\nRPL15\tRPL35\nRPL15\tRPS16\nITGB2\tRAC2\nITGB2\tRGS19\nRAC2\tTRA@\nRAC2\tTRB@\nDBT\tPRR11\nDBT\tDIP2A\nDBT\tFLJ42393\nDBT\tZNF611\nDBT\tFLJ12151\nDBT\tPGF\nDBT\tLOC152719\nDBT\tRIOK3\nDBT\tPDE4C\nDBT\tWDR45\nDBT\tZNF160\nDBT\tFBXW12\nDBT\tKNS2\nDBT\tSLC35E1\nPRR11\tZNF611\nPRR11\tSLC35E1\nPRR11\tZNF160\n207730_x_at\t216858_x_at\n207730_x_at\tUBE2D2\n207730_x_at\tFLJ42393\n207730_x_at\t208246_x_at\n207730_x_at\tPRKAR2A\n207730_x_at\t217679_x_at\n207730_x_at\tPDE4C\n207730_x_at\tPRR11\n207730_x_at\tPGF\n207730_x_at\tRIOK3\n207730_x_at\t208238_x_at\n207730_x_at\tKNS2\n207730_x_at\t216524_x_at\n207730_x_at\tATP8B1\n207730_x_at\tFLJ12151\n207730_x_at\tLOC152719\n207730_x_at\tDBT\n207730_x_at\tZNF611\n207730_x_at\tSLC35E1\n207730_x_at\tZNF160\n207730_x_at\tCEP27\n207730_x_at\tFBXW12\nEEF1D\tRPS9\nEEF1D\tGLTSCR2\nEEF1D\tRPL38\nPRDX2\tRIOK3\nRIOK3\tZNF611\nRIOK3\tZNF160\nRIOK3\tSLC35E1\nRPS23\tRPS27A\nRPS23\tU16\nRPS23\tRPS7\nRPS23\tRPS24\nRPS23\tRPS25\nRPS23\tRPS6\nRPS23\tRPS4X\nRPS23\tRPS3A\nRPS27A\tSFRS5\nRPS27A\tRPS4X\nRPS27A\tU16\nRPS27A\tTINP1\nRPS27A\tRPS3A\nRPS27A\tRPS7\nRPS27A\tRPS6\nRPL34\tTPT1\nRPL34\tSF3B1\nRPL34\tRPL7\nRPL34\tRPS25\nRPL34\tRPL36A\nRPL34\tRPL5\nRPL34\tRPS27A\nRPL34\tRPS4X\nRPL34\tRPS3A\nRPL34\tRPS6\nRPL34\tRPS7\nRPL34\tRPL4\nRPL34\tRPL9\nRPL34\tRPS23\nRPL34\tRPL6\nRPL34\tRPL35A\nRPL34\tRPS24\nHNRPDL\tMATR3\nHNRPDL\tSON\nHNRPDL\tNAP1L1\nHNRPDL\tNPM1\nHNRPDL\tSF3B1\nHNRPDL\tIL6ST\nHNRPDL\tSERBP1\nHNRPDL\tSFRS11\nMATR3\tWAC\nMATR3\tSERBP1\nMATR3\tNARS\nMATR3\tXPO1\nMATR3\tYTHDC1\nMATR3\tSFRS11\nMATR3\tSON\nMATR3\tMGEA5\nMATR3\tPAPOLA\nMATR3\tSH3BGRL\nMATR3\tSEPT7\nMATR3\tSEPT2\nMATR3\tTAF7\nMATR3\tNPM1\nMATR3\tPCNP\nMATR3\tRNPC2\nMATR3\tTLOC1\nEIF3S3\tMATR3\nEIF3S3\tHSP90AA1\nEIF3S3\tTTC3\nEIF3S3\tNARS\nEIF3S3\tFNTA\nEIF3S3\tSEPT2\nWAC\tZNF403\nRPL22\tRPL5\nRPL22\tTPT1\nRPL22\tRPS15A\nRPL22\tRPS23\nRPL22\tRPS27A\nRPL22\tRPS7\nRPL22\tRPL30\nRPL22\tRPS3A\nRPL22\tRPL6\nRPL22\tRPL34\nRPL22\tRPL26\nRPL22\tRPL9\nRPL22\tRPL24\nRPL22\tRPS25\nRPL22\tRPL35A\nRPL22\tRPS17\nRPL22\tRPS24\nRPL22\tRPL4\nRPL22\tRPL41\nRPL22\tRPL7\nRPL22\tRPS6\nRPL22\tRPL36A\nRPL22\tRPL23\nRPL22\tRPS4X\nRPL22\tRPL31\nSH3BGRL\tYTHDC1\nSH3BGRL\tTAF7\nSH3BGRL\tSON\nMRPL34\tNDUFS3\nMRPL34\tUQCRFS1\nMRPL34\tUQCRC1\nMRPL34\tNDUFV1\nMRPL34\tPOLR2I\nNDUFS3\tPOLR2I\nNDUFS3\tNDUFS8\nNDUFS3\tUQCRFS1\nFIBP\tSIAHBP1\nFIBP\tWIPI2\nFIBP\tPOLR2E\nAURKAIP1\tCOX5A\nAURKAIP1\tC12orf10\nAURKAIP1\tMRPL34\nAURKAIP1\tGPX4\nAURKAIP1\tNDUFS3\nAURKAIP1\tTUFM\nAURKAIP1\tNDUFS8\nAURKAIP1\tNDUFS7\nAURKAIP1\tNDUFA2\nAURKAIP1\tC3orf60\nAURKAIP1\tNDUFA3\nAURKAIP1\tUQCRFS1\nAURKAIP1\tTMEM93\nAURKAIP1\tUQCRC1\nAURKAIP1\tFIBP\nAURKAIP1\tEXOSC4\nDIP2A\tPGF\nCD52\tIL7R\nCD52\tTRBV3-1\nCD52\tFAIM3\nCD52\tLCK\nCD52\tISG20\nCD52\tTRA@\nCD52\tTRB@\nCD52\tRAC2\nCD52\tTRBC1\nCSNK1A1\tPAPOLA\nCSNK1A1\tEIF4G2\nPAPOLA\tTAF7\nPAPOLA\tPUM2\nPAPOLA\tSON\nPAPOLA\tZNF9\nPAPOLA\tSSR1\nPAPOLA\tSRP72\nPAPOLA\tTRAM1\nPAPOLA\tTHRAP1\nPAPOLA\tTMEM123\nPAPOLA\tUSP34\nPAPOLA\tRNPC2\nPAPOLA\tPNRC2\nPAPOLA\tYTHDC1\nPAPOLA\tSF3B1\nHLA-G\tMAN2B1\nHLA-G\tTGFB1\nHLA-G\tISG20\nHLA-G\tIFI30\nHLA-G\tHLA-J\nMAN2B1\tTGFB1\nATP5D\tCOX5B\nATP5D\tCYC1\nCOX5B\tFLJ14346\nCOX5B\tNDUFS3\nCOX5B\tNDUFB2\nCOX5B\tNDUFA3\nCOX5B\tMRPS12\nCOX5B\tNDUFS8\nCOX5B\tUQCRC1\nCOX5B\tCYC1\nCOX5B\tMSRB2\nCOX5B\tUQCRQ\nCOX5B\tNDUFB11\nCOX5B\tMDH2\nCOX5B\tNDUFV1\nCOX5B\tUQCRFS1\nCOX5B\tNDUFS7\nCOX5B\tNDUFAB1\nCOX5B\tCOX8A\nCOX5B\tUQCR\nIGLJ3\tIGLV3-25\nIGLJ3\tLOC91316\n215176_x_at\tIGHG3\n215176_x_at\t217258_x_at\n215176_x_at\tIGHV1-69\n215176_x_at\t217281_x_at\n215176_x_at\tIGLC2\n215176_x_at\tIGHA1\n215176_x_at\tIGKC\n215176_x_at\tIGHM\n215176_x_at\tIGLV3-25\n215176_x_at\tIGKV1OR2-108\n215176_x_at\tIGLJ3\n215176_x_at\tIGKV1-5\n215176_x_at\tIGKV1OR15-118\n215176_x_at\tIGLC1\n215176_x_at\tIGL@\n215176_x_at\tLOC91316\n215176_x_at\tIGKV1D-13\n215176_x_at\t221651_x_at\n215176_x_at\tCTA-246H3.1\nIGHG3\tIGHM\nIGHG3\tIGHV1-69\nIGHG3\tIGKV1-5\nIGHG3\tIGLJ3\nIGHG3\tIGKV1D-13\nIGHG3\tIGKC\nIGHG3\tIGKV1OR2-108\nIGHG3\tIGLC2\nIGHG3\tIGLC1\nIGHG3\tLOC91316\n217258_x_at\tIGLC2\n217258_x_at\tIGKC\n217258_x_at\tIGL@\n217258_x_at\tCTA-246H3.1\n217258_x_at\tIGLJ3\n217258_x_at\tIGKV1-5\n217258_x_at\tIGLV3-25\nRPL14\tRPL17\nRPL14\tRPS4X\nRPL14\tRPS3A\nRPL14\tRPL5\nRPL14\tRPS7\nRPL14\tU16\nRPL14\tRPL21\nRPL14\tRPL4\nRPL14\tRPL6\nRPL14\tRPL35A\nRPL14\tRPL39\nRPL14\tRPL23\nDYNLT3\tPAFAH1B1\nPAFAH1B1\tPRKACB\nPAFAH1B1\tPOPDC3\nPAFAH1B1\tREEP5\nPAFAH1B1\tZFR\nPAFAH1B1\tPCNP\nPAFAH1B1\tSEPT7\nRPS18\tRPS19\nSF3B1\tWAC\nSF3B1\tSON\nSF3B1\tTUG1\nSF3B1\tSFRS5\nSF3B1\tYTHDC1\nSF3B1\tTCEA1\nIQGAP1\tMBNL1\nIQGAP1\tROCK1\nMBNL1\tOSBPL8\nMBNL1\tROCK1\nMBNL1\tPNRC2\nMBNL1\tPUM2\nNPM1\tRPS3A\nNPM1\tSFRS5\nNPM1\tSFRS11\nNPM1\tRPS7\nNPM1\tRPS6\nNPM1\tRPL7\nNPM1\tRPL34\nNPM1\tTCEA1\nNPM1\tTUG1\nNPM1\tRPL5\nNPM1\tRPL17\nNPM1\tRPL24\nNPM1\tSF3B1\nNPM1\tTINP1\nNPM1\tRPL6\nNPM1\tRPS24\nNPM1\tRPL4\nNPM1\tRPS27A\nNPM1\tRNPC2\nRPS3A\tSF3B1\nRPS3A\tTPT1\nRPS3A\tU16\nRPS3A\tRPS4X\nRPS3A\tRPS7\nRPS3A\tRPS6\nSPN\tZNF552\nDDX50\tRNPC2\nEIF5\tPCNP\nEIF5\tPAPOLA\nEIF5\tTAF7\nEIF5\tMATR3\nEIF5\tHSP90AA1\nPCNP\tSEPT2\nPCNP\tTLOC1\nPCNP\tRNPC2\n217281_x_at\tIGHM\n217281_x_at\tIGHV1-69\n217281_x_at\tIGLJ3\n217281_x_at\tIGKC\n217281_x_at\tIGLC2\n217281_x_at\tIGHA1\n217281_x_at\tIGHG3\n217281_x_at\tIGKV1OR2-108\nCHCHD2\tMIF\nCHCHD2\tNDUFAB1\nCHCHD2\tCOX5B\nCHCHD2\tMRPS12\nCHCHD2\tMRP63\nCHCHD2\tCOX8A\nCHCHD2\tSLC25A3\nCHCHD2\tNDUFB11\nMIF\tTIMM13\nARPC5\tCAP1\nARPC5\tPRG1\nSFRS11\tTUG1\nACTR10\tSKP1A\nACTR10\tPRKAR1A\nSKP1A\tSUMO2\nACTR2\tARPC3\nACTR2\tACTR3\nACTR2\tCAPZA1\nACTR2\tPDCD10\nARPC3\tCAPZA1\nCAV1\tMMP2\nCAV1\tCOL6A1\nCAV1\tFBLN1\nCAV1\tCOL1A2\nCAV1\tPTRF\nCAV1\tLAMC1\nACTG2\tMYH11\nMYH11\tMYLK\nMYH11\tTPM1\nMYH11\tTAGLN\nRPL38\tRPS20\nRPL38\tRPLP2\nRPL38\tRPS19\nRPS20\tRPS27\nCSDE1\tFLJ11021\nCSDE1\tWAC\nCSDE1\tNFE2L2\nCOL1A2\tCOL3A1\nCOL3A1\tCOL6A3\nCD7\tLCK\nCD7\tHMHA1\nLCK\tRAC2\nLCK\tTRA@\nLCK\tTRB@\nLCK\tTRBC1\nLCK\tTRBV3-1\nNDUFB11\tNDUFS3\nNDUFB11\tUQCRQ\nNDUFB11\tUQCR\nNDUFB11\tNDUFB4\nNDUFB11\tUQCRFS1\nNDUFB11\tSLC25A3\nNDUFB11\tNDUFS8\nNDUFB11\tPOLR2L\nC3orf60\tEXOSC4\nC3orf60\tPOLR2I\nC3orf60\tMRPL34\nC3orf60\tGPX4\n211639_x_at\tIGKC\nRPL11\tRPS15A\nRPL11\tRPL5\nRPL11\tRPS24\nRPL11\tRPL17\nRPL11\tRPL7\nRPL11\tU16\nRPL11\tRPL24\nRPL11\tRPL21\nRPL11\tRPL35A\nRPL11\tRPS25\nRPL11\tRPL14\nRPL11\tRPS6\nRPL11\tRPL22\nRPL11\tRPL34\nRPL11\tRPL4\nRPL11\tRPS4X\nRPL11\tRPS3A\nRPL11\tRPS7\nRPL11\tRPL39\nRPL11\tRPL31\nRPL11\tRPL36A\nRPL11\tRPL23\nRPL11\tRPS17\nRPL11\tRPL30\nRPL11\tRPS23\nRPS15A\tRPS6\nRPS15A\tRPS17\nRPS15A\tRPS7\nRPS15A\tRPS3A\nRPS15A\tRPS20\nRPS15A\tRPS27\nRPS15A\tRPS23\nGNB2L1\tRPL13A\nGNB2L1\tRPS19\nGNB2L1\tRPL29\nGNB2L1\tRPS16\nRPL29\tRPS9\nRPL29\tRPS16\nRPL29\tRPL35\nRPL13\tRPL18A\nRPL13\tRPL28\nRPL13\tRPS2\nRPL13\tRPL18\nRPL13\tRPLP2\nRPL13\tRPS15\nRPL18A\tRPS2\nRPL18A\tRPS19\nRPL18A\tRPLP2\nRPL18A\tRPL28\nHLA-A\tHLA-F\nHLA-A\tMAN2B1\nHLA-A\tHLA-E\nHLA-A\tHLA-G\nHLA-A\tHLA-B\nHLA-A\tHLA-J\nHLA-A\tHLA-C\nHLA-F\tISG20\nHLA-F\tHLA-G\nHLA-F\tIFI30\nHLA-F\tMAN2B1\nHLA-F\tHLA-J\nBTF3\tMDS1\nBTF3\tRPS27\nBTF3\tRPS17\nBTF3\tRPS15A\nBTF3\tRPL30\nBTF3\tLOC402057\nBTF3\tRPL22\nNACA\tRPL35A\nNACA\tRPS3A\nNACA\tRPL5\nNACA\tNAP1L1\nNACA\tSFRS5\nNACA\tRPS7\nNACA\tRPL24\nNACA\tU16\nNACA\tRPL21\nNACA\tRPL9\nNACA\tNPM1\nNACA\tRPS23\nNACA\tRPL4\nNACA\tRPS27A\nNACA\tRPS24\nNACA\tRPL17\nNACA\tRPL6\nNACA\tTINP1\nRPL35A\tRPL7\nRPL35A\tRPS17\nRPL35A\tRPS27A\nRPL35A\tRPS20\nRPL35A\tRPS27\nRPL35A\tRPS15A\nRPL35A\tRPS3A\nRPL35A\tRPS25\nRPL35A\tRPL36A\nRPL35A\tRPL39\nRPL35A\tRPL6\nRPL35A\tRPS6\nRPL35A\tRPS4X\nRPL35A\tRPL4\nRPL35A\tRPL5\nRPL35A\tRPS23\nRPL35A\tRPS24\nRPL35A\tU16\nRPL35A\tRPL9\nRPL35A\tRPS7\nBIRC2\tDDX5\nBIRC2\tZNF403\nBIRC2\tLOC58486\nBIRC2\tTHRAP1\nBIRC2\tSERBP1\nBIRC2\tPAPOLA\nBIRC2\tUSP34\nBIRC2\tC1orf63\nDDX5\tSERBP1\nDDX5\tSRP46\nDDX5\thfl-B5\nDDX5\tTUG1\nDDX5\tNAP1L1\nDDX5\tHNRPH1\nDDX5\tNBPF1\nDDX5\tSFRS5\nDDX5\tLOC342346\nDDX5\tNBPF12\nDDX5\tMGEA5\nDDX5\tHNRPA2B1\nDDX5\tSON\nDDX5\tFLJ10154\nDDX5\tUSP34\nDDX5\tNPM1\nDDX5\tTARDBP\nDDX5\tEIF4G2\nDDX5\tPAPOLA\nDDX5\tHIATL1\nDDX5\tYTHDC1\nDDX5\tSF3B1\nRPL30\tU16\nRPL30\tRPS7\nRPL30\tRPS25\nRPL30\tRPS13\nRPL30\tRPS3A\nRPL30\tRPS4X\nRPL30\tRPL4\nRPL30\tRPS6\nRPL30\tRPL37\nRPL30\tRPL31\nRPL30\tRPL36A\nRPL30\tRPL35A\nRPL30\tRPS27\nRPL30\tRPS17\nRPL30\tRPS15A\nRPL30\tRPL9\nRPL30\tRPS20\nRPL30\tRPS23\nRPL30\tRPL32\nRPL30\tRPL39\nTMEM123\tTRAM1\nIGHA1\tIGHV1-69\nIGHA1\tIGKC\nIGHA1\tIGLJ3\nIGHA1\tIGLV3-25\nIGHA1\tIGHM\nIGHA1\tIGKV1OR15-118\nIGHA1\tIGLC2\nIGHA1\tIGL@\nIGHA1\tIGKV1-5\nIGHA1\tIGKV1D-13\nIGHA1\tIGHG3\nIGHA1\tIGKV1OR2-108\nIGHA1\tLOC91316\nIGHA1\tIGLC1\nIGHV1-69\tIGLC2\nIGHV1-69\tIGKC\nIGHV1-69\tIGLC1\nIGHV1-69\tIGKV1OR2-108\nNDUFS7\tPOLR2I\nNDUFS7\tNDUFV1\nNDUFS7\tNDUFS8\nPOLR2I\tUQCRFS1\n214836_x_at\t217258_x_at\n214836_x_at\t217281_x_at\n214836_x_at\tIGLC2\n214836_x_at\tIGLV3-25\n214836_x_at\tIGL@\n214836_x_at\tIGKV1OR2-108\n214836_x_at\tIGKV1OR15-118\n214836_x_at\tIGLJ3\n214836_x_at\tIGHG3\n214836_x_at\tIGKC\n214836_x_at\tLOC91316\n214836_x_at\tIGKV1-5\n214836_x_at\tIGKV1D-13\n214836_x_at\tIGLC1\n214836_x_at\tIGHA1\n214836_x_at\t221651_x_at\n214836_x_at\tCTA-246H3.1\n214836_x_at\t215176_x_at\nRPS6\tTPT1\nRPS6\tRPS7\nRPS6\tU16\nGTF2I\tSRP9\nGTF2I\tTMEM66\nSRP9\tSUMO1\nATP2A3\tTRA@\nATP2A3\tTRB@\nATP2A3\tTRBC1\nTRA@\tTRBC1\nTRA@\tTRB@\nIGLC2\tIGLJ3\nIGLC2\tLOC91316\nIGLC2\tIGLV3-25\nIGLC2\tIGLV3-10\nHSP90B1\tZNF638\nHSP90B1\tRNPC2\nHSP90B1\tVDP\nRPL7\tTPT1\nRPL7\tRPS3A\nRPL7\tRPS6\nRPL7\tRPS24\nRPL7\tTCEA1\nRPL7\tSF3B1\nRPL7\tRPS7\nRPL7\tRPS25\nRPL7\tRPL9\nRPL7\tRPS23\nRPL7\tRPS27A\nRPL7\tRPS4X\nMGEA5\tNAP1L1\nMGEA5\tPAPOLA\nMGEA5\tXPO1\nMGEA5\tSF3B1\nMGEA5\tRNPC2\nMGEA5\tYTHDC1\nMGEA5\tPNRC2\nBCLAF1\tPCM1\nBCLAF1\tRNPC2\nBCLAF1\tPUM2\nBCLAF1\tEIF3S6\nBCLAF1\tUSP47\nBCLAF1\tSF3B1\nBCLAF1\tSLC38A2\nBCLAF1\tFLJ11021\nBCLAF1\tCOPS2\nBCLAF1\tPCNP\nBCLAF1\tCSDE1\nBCLAF1\tMATR3\nBCLAF1\tWAC\nBCLAF1\tCROP\nBCLAF1\tSRP72\nBCLAF1\tLOC58486\nBCLAF1\tPHIP\nBCLAF1\tZNF638\nBCLAF1\tVDP\nBCLAF1\tNPM1\nBCLAF1\tHSP90B1\nBCLAF1\tTLOC1\nPCM1\tVDP\nCXorf9\tGPSM3\nCXorf9\tTRA@\nCXorf9\tIL2RG\nCXorf9\tIL16\n216342_x_at\tRPL7\n216342_x_at\tTPT1\n216342_x_at\tRPL14\n216342_x_at\tRPL9\n216342_x_at\tRPS27A\n216342_x_at\tRPL17\n216342_x_at\tRPS17\n216342_x_at\tRPL5\n216342_x_at\tU16\n216342_x_at\tRPL22\n216342_x_at\tRPL36A\n216342_x_at\tRPS25\n216342_x_at\tLOC402057\n216342_x_at\tRPS7\n216342_x_at\tRPL21\n216342_x_at\tRPL24\n216342_x_at\tRPL41\n216342_x_at\tRPS24\n216342_x_at\tMDS1\n216342_x_at\tRPL6\n216342_x_at\tLOC440055\n216342_x_at\tRPL4\n216342_x_at\tRPL34\n216342_x_at\tRPS3A\n216342_x_at\tRPL11\n216342_x_at\tRPS6\n216342_x_at\tRPL23\n216342_x_at\tRPL30\n216342_x_at\tRPL31\n216342_x_at\tRPS23\n216342_x_at\tRPL35A\n216342_x_at\tRPS4X\nNDUFA13\tNDUFA3\nNDUFA13\tNDUFB2\nNDUFA3\tNDUFAB1\nNDUFA3\tUCRC\nNDUFA3\tNDUFB2\nNDUFA3\tNDUFB11\nNDUFA3\tUQCR\nNDUFA3\tUQCRC1\nNDUFA3\tNDUFB4\nCDKN1B\tPPP2CA\nCDKN1B\tMARCKS\nHMGB1\tHNRPR\nHMGB1\tHNRPA2B1\nHNRPR\tSRP9\nHNRPR\tSON\nHNRPR\tSFRS10\nAP2S1\tATP6V0C\nAP2S1\tPOLR2E\nAP2S1\tGPX1\nAP2S1\tGUK1\nAP2S1\tNAPA\nAP2S1\tCOPE\nAP2S1\tATP6V0B\nAP2S1\tMRPS12\n211637_x_at\tIGKC\n211637_x_at\tIGHA1\n211637_x_at\tIGLC2\n211637_x_at\t211639_x_at\n211637_x_at\tIGHM\n211637_x_at\t211908_x_at\n211637_x_at\tIGHV1-69\n211637_x_at\tCTA-246H3.1\n211637_x_at\t211641_x_at\n211637_x_at\t215176_x_at\n211637_x_at\t211645_x_at\n211637_x_at\tIGHG3\n211637_x_at\t217281_x_at\n211637_x_at\tLOC91316\nCOL4A1\tCOL4A2\nCOL4A1\tFBLN1\nCOL4A2\tRBPMS\nFLJ42393\tPDE4C\nFLJ42393\tZNF160\nFLJ42393\tPOLR1B\nFLJ42393\tKNS2\nFLJ42393\tSLC35E1\nFLJ42393\tLOC152719\nMORF4L1\tPTGES3\nMORF4L1\tSKP1A\nPTGES3\tSUMO2\nSON\tXPO1\nSON\tSRP46\nSON\tYTHDC1\nIFI30\tTGFB1\nIFI30\tNCF1\nAASDHPPT\tPOPDC3\nAASDHPPT\tPAFAH1B1\nPOPDC3\tRAB6C\nPOPDC3\tPRKACB\nFXYD5\tITGB2\nFXYD5\tPSMB9\nCD79A\tIGKC\nCD79A\tIGHA1\nCD79A\tIGLC2\nCD79A\tIGLJ3\nFLJ14346\tZNF611\nFLJ14346\tGPAA1\nFLJ14346\tMRPS12\nFLJ14346\tLOC152719\nFLJ14346\tSLC35E1\nHLA-DPA1\tHLA-DQB1\nHLA-DPA1\tHLA-DQA1\nHLA-DPA1\tHLA-DRA\nHLA-DPA1\tHLA-DRB5\nHLA-DPA1\tHLA-DRB4\nHLA-DPA1\tHLA-DRB1\nHLA-DPA1\tHLA-DPB1\nHLA-DQB1\tHLA-DRB4\nHLA-DQB1\tHLA-DRB5\nHLA-DQB1\tHLA-DRB1\nIFNGR1\tROCK1\nC6orf12\tHLA-F\nC6orf12\tHLA-G\nC6orf12\tHLA-B\nC6orf12\tHLA-C\nC6orf12\tHLA-J\nC6orf12\tHLA-A\nNARS\tTLOC1\nNARS\tTTC3\nNARS\tSRP9\nRPL36A\tRPL6\nRPL36A\tRPS7\nRPL36A\tRPS24\nRPL36A\tRPS3A\nRPL36A\tRPL4\nRPL36A\tRPS4X\nRPL36A\tRPS25\nRPL36A\tRPS15A\nRPL36A\tRPS6\nRPL36A\tRPS17\nRPL36A\tRPS23\nRPL36A\tRPL39\n217718_s_at\tYWHAZ\n217718_s_at\tCAB39\nRPL23A\tRPS20\nTAF7\tUBE4A\nTAF7\tYTHDC1\nTAF7\tXPO1\nDHX15\tOGT\nDHX15\tKIAA0907\nDHX15\tNPM1\nCD2\tTRA@\nCD2\tCD52\nCD2\tIL7R\nDST\tPLEKHC1\nDST\tSPARCL1\nCOL6A1\tCOL6A3\nCOL6A1\tFBLN1\nCOL6A1\tPTRF\nFIS1\tNDUFA13\nEIF4G2\tSRP46\nEIF4G2\tHSP90AA1\nEIF4G2\tSERBP1\nEIF4G2\tSON\nEIF4G2\tEIF5\nEIF4G2\tNPM1\nEIF4G2\tPUM2\nEIF4G2\tSET\nEIF4G2\tPCNP\nEIF4G2\tLOC342346\nEIF4G2\tHNRPA2B1\nEIF4G2\tYTHDC1\nEIF4G2\tXPO1\nEIF4G2\tMATR3\nEIF4G2\tPAPOLA\nEIF4G2\tTHRAP1\nEIF4G2\tSF3B1\nEIF4G2\tHIATL1\nSRP46\tXPO1\nCOL1A1\tCOL6A3\nCOL1A1\tCOL1A2\nCOL1A1\tCOL3A1\nC12orf10\tFIBP\nC12orf10\tC3orf60\nLMOD1\tMYH11\n208246_x_at\tPOLR1B\n208246_x_at\tZNF611\n208246_x_at\tPGF\n208246_x_at\tATP8B1\n208246_x_at\tRIOK3\n208246_x_at\tKNS2\n208246_x_at\tDBT\n208246_x_at\tLOC152719\n208246_x_at\tFBXW12\n208246_x_at\tPDE4C\n208246_x_at\tSLC35E1\n208246_x_at\tZNF160\n208246_x_at\t216524_x_at\nHNRPH1\tSON\nHNRPH1\tSF3B1\nHNRPH1\tYTHDC1\nRPS14\tRPS16\nRPS14\tRPS5\nRPS14\tRPS9\nRPS16\tRPS19\nRPS16\tRPS9\nRPS16\tRPS5\nARHGDIB\tLAPTM5\nARHGDIB\tPRG1\nARHGDIB\tUCP2\nARHGDIB\tRAC2\nARHGDIB\tCXCR4\nARHGDIB\tCECR1\nLAPTM5\tRAC2\nLAPTM5\tPRG1\nATRX\tCOPS2\nATRX\tPCNP\nATRX\tPUM2\nATRX\tMATR3\nATRX\tSH3BGRL\nATRX\tSEPT7\nATRX\tBCLAF1\nATRX\tTLOC1\nHIATL1\tSFRS5\nHIATL1\tRPL6\nHIATL1\tRPL17\nHIATL1\tTUG1\nHIATL1\tHNRPA2B1\nHIATL1\tRAVER2\nHIATL1\tRNPC2\nHIATL1\tHNRPH1\nHIATL1\tYTHDC1\nHIATL1\tTCEA1\nHIATL1\tNPM1\nHIATL1\tLOC58486\nHIATL1\tTHRAP1\nHIATL1\tPAPOLA\nHIATL1\tSF3B1\nFAIM3\tTRA@\nFAIM3\tHCLS1\nFAIM3\tRAC2\nIGKV1D-13\tIGLJ3\nIGKV1D-13\tIGLV3-25\nIGKV1D-13\tIGLC2\nIGKV1D-13\tLOC91316\nIGKV1D-13\tIGL@\nIGKV1D-13\tIGLC1\nIGKV1D-13\tIGKV1OR15-118\nHSP90AA1\tMATR3\nHSP90AA1\tSRP9\nHSP90AA1\tSERBP1\nHSP90AA1\tSEPT2\nHSP90AA1\tNARS\nRPL19\tRPS20\nLOC283412\tRPL29\nLOC283412\tRPS9\nDGCR6\tPOLR2I\nLTB\tTRBC1\nLTB\tTRB@\nHMHA1\tPSMB8\nHMHA1\tTRBC1\nHMHA1\tRAC2\nPSMB8\tTAPBP\nPSMB8\tPSMB9\nIGKV1-5\tIGL@\nIGKV1-5\tIGKV1OR2-108\nIGKV1-5\tIGLC2\nIGKV1-5\tIGLJ3\nIGKV1-5\tIGLV3-25\nIGKV1-5\tIGLC1\nIGKV1-5\tIGKV1D-13\nIGKV1-5\tIGKV1OR15-118\nRPL28\tRPS10\nRPL28\tRPS2\nRPL28\tRPS19\nRPL28\tRPS16\nRPL28\tRPLP2\nHCLS1\tITGB2\nHCLS1\tSELL\nHCLS1\tRAC2\nHCLS1\tTRA@\nFBLN1\tMMP2\nFBLN1\tPTRF\nNDUFA2\tUQCRFS1\nNDUFA2\tUQCR\nNDUFA2\tNDUFS8\nNDUFA2\tUQCRC1\nNDUFA2\tNDUFB4\nNDUFA2\tTCEB2\nNDUFA2\tNDUFA3\nNDUFA2\tUCRC\nHLA-DRB4\tHLA-DRB5\nFAM96B\tNDUFV1\nFAM96B\tNDUFS8\nFAM96B\tNDUFS3\nFAM96B\tTUFM\nFAM96B\tNDUFS7\nFAM96B\tNDUFA10\nFAM96B\tNDUFA2\nFAM96B\tNDUFA3\nCOQ9\tNDUFB4\nCOQ9\tUQCRFS1\nEEF1B2\tRPL30\nEEF1B2\tRPL10A\nEEF1B2\tRPS23\nEEF1B2\tRPS15A\nEEF1B2\tRPL35A\nEEF1B2\tRPL14\nEEF1B2\tRPL36A\nEEF1B2\tRPL37\nEEF1B2\tRPL39\nEEF1B2\tLOC440055\nPUM2\tWAC\nPUM2\tSF3B1\nALOX5AP\tLAPTM5\nALOX5AP\tITGB2\nDKFZP566N034\tZNF160\nDKFZP566N034\tSLC35E1\nARL6IP5\tPAFAH1B1\nTRB@\tTRBV3-1\nTRB@\tTRBC1\nIFI16\tROCK1\nIFI16\tIQGAP1\nIFI16\tTRIM22\nIFI16\tMBNL1\nIFI16\tIFNGR1\n211641_x_at\t211908_x_at\n211641_x_at\t211645_x_at\n211641_x_at\tIGHG3\n211641_x_at\tIGKV1-5\n211641_x_at\t217281_x_at\n211641_x_at\tIGHA1\n211641_x_at\tIGHM\n211908_x_at\tIGHV1-69\n211908_x_at\t217281_x_at\n211908_x_at\tIGHM\nWSB1\tXPO1\nARPC1B\tHLA-C\nARPC1B\tHLA-A\nARPC1B\tHLA-B\nARPC1B\tISG20\nARPC1B\tHLA-G\nCD53\tITGB2\nCD53\tIL10RA\nCD53\tHCLS1\nHLA-E\tHLA-F\nHLA-E\tPSMB8\nHLA-E\tHLA-J\nHLA-E\tMAN2B1\nHLA-E\tHLA-G\n211645_x_at\tIGHA1\n211645_x_at\tIGKC\n211645_x_at\tIGHM\n211645_x_at\tIGLC2\n211645_x_at\tIGHG3\n211645_x_at\tIGL@\n211645_x_at\tIGLV3-25\n211645_x_at\tIGLJ3\n211645_x_at\tLOC91316\n211645_x_at\tIGLC1\n211645_x_at\tIGKV1-5\n211645_x_at\t221651_x_at\n211645_x_at\tIGKV1OR15-118\n211645_x_at\tCTA-246H3.1\n211645_x_at\tIGKV1D-13\n211645_x_at\t214836_x_at\n211645_x_at\t215176_x_at\nNDUFB2\tNDUFS7\nNDUFB2\tUQCRC1\nNDUFB2\tUQCR\nNDUFB2\tNDUFV1\nFLJ10154\tLOC58486\nFLJ10154\tXPO1\nFLJ10154\tPFAAP5\nFLJ10154\tRNPC2\nFLJ10154\tHNRPDL\nFLJ10154\tSFPQ\nFLJ10154\tSFRS11\nFLJ10154\tNPM1\nEIF4A2\tTTC3\nEIF4A2\tMATR3\nGPX4\tR3HCC1\nGPX4\tPOLR2I\nGPX4\tMRPL34\nPRKAR2A\tSLC35E1\nSLC35E1\tUBE2D2\nSLC35E1\tWDR45\nSLC35E1\tZNF611\nSLC35E1\tZNF160\nIGLC1\tIGLJ3\nIGLC1\tIGLV3-25\nIGLC1\tIGLC2\nIGLC1\tLOC91316\nUQCRC1\tUQCRFS1\nTMED5\tWAC\nMDH2\tNDUFB11\nMDH2\tUQCRQ\nMDH2\tUQCRC1\nMDH2\tUQCRFS1\nMDH2\tNDUFS8\nIGL@\tIGLV3-25\nIGL@\tIGLC2\nIGL@\tIGLC1\nIGL@\tIGLJ3\nHLA-DRB1\tHLA-F\nHLA-DRB1\tHLA-DRB4\nHLA-DRB1\tHLA-DRB5\nCD74\tHLA-DQB1\nCD74\tHLA-DRB4\nCD74\tHLA-DRB1\nCD74\tHLA-DRA\nCD74\tHLA-DMA\nCD74\tHLA-DPB1\nCD74\tHLA-DRB5\nCD74\tHLA-DPA1\nCLTC\tNARS\nRPS17\tRPS25\nRPS17\tRPS4X\nRPS17\tRPS6\nRPS17\tRPS7\nRPS17\tRPS3A\nRPS17\tRPS20\nRPS17\tRPS23\nRPS17\tRPS27\nACTR3\tCAP1\nACTR3\tARPC3\nACTR3\tYWHAZ\nACTR3\tCAPZA1\nRPS10\tRPS16\nRPS10\tRPS21\nRPS10\tRPS19\nRPL27A\tRPS11\nRPL27A\tRPLP2\nRPL27A\tRPS19\nRPL27A\tRPL38\nRPL27A\tRPS20\nRPL27A\tRPS18\nDDX17\tSON\nDDX17\tHNRPH1\nC11orf58\tYTHDC1\nC11orf58\tHNRPK\nC11orf58\tLOC342346\nC11orf58\tSON\nNAPA\tWIPI2\nRGPD5\tTTC3\nCD3D\tTRA@\nCD3D\tTRB@\nCD3D\tTRBC1\nIGKV1OR2-108\tLOC91316\nIGKV1OR2-108\tIGLC2\nIGKV1OR2-108\tIGLC1\nIGKV1OR2-108\tIGLJ3\nCAB39\tSUB1\nATP6V0B\tGPX1\nATP6V0B\tCOPE\nATP6V0B\tMRPS12\nATP6V0B\tATP6V0D1\nATP6V0B\tATP6V0C\nGPX1\tPPP1CA\nGPX1\tPGLS\nGPX1\tOAZ1\nENDOG\tNDUFS7\nLOC388344\tRPS2\nLOC388344\tRPL18\nLOC388344\tRPL18A\nLOC388344\tRPL13\nLOC388344\tRPS15\nLOC440055\tRPL22\nLOC440055\tRPL17\nLOC440055\tRPS7\nLOC440055\tRPL9\nLOC440055\tRPS3A\nLOC440055\tRPS15A\nLOC440055\tRPL21\nLOC440055\tRPS4X\nLOC440055\tU16\nLOC440055\tRPL31\nLOC440055\tRPL37\nLOC440055\tRPL35A\nLOC440055\tMDS1\nLOC440055\tRPS6\nLOC440055\tRPS17\nLOC440055\tRPL32\nLOC440055\tRPL23\nLOC440055\tRPL4\nLOC440055\tRPL39\nLOC440055\tRPS20\nLOC440055\tRPL36A\nLOC440055\tRPL30\nLOC440055\tRPS23\nLOC440055\tRPL11\nCYBA\tIFI30\nCYBA\tHLA-G\nCYBA\tHLA-B\nCYBA\tHLA-J\nCYBA\tTGFB1\nRBPMS\tTGFB1I1\nKNS2\tPRR11\nKNS2\tSPG21\nKNS2\tLOC152719\nKNS2\tPGF\nKNS2\tUBE2D2\nKNS2\tPRKAR2A\nKNS2\tRIOK3\nKNS2\tZNF611\nKNS2\tZNF160\nKNS2\tPDE4C\nKNS2\tSLC35E1\nNPEPPS\tSEPT7\nMRPS12\tZNHIT1\nGLTSCR2\tRPS2\nGLTSCR2\tRPL13\nGLTSCR2\tLOC388344\nHLA-DMB\tHLA-DRA\n220725_x_at\tCDC5L\n220725_x_at\tSLC25A16\n220725_x_at\tC12orf38\n220725_x_at\tOPHN1\n220725_x_at\tMCM3AP\n220725_x_at\tLOC56902\nCDC5L\tLOC56902\nCDC5L\tMCM3AP\nARL8B\tHSP90AA1\nARL8B\tNARS\nCTA-246H3.1\tIGLC2\nCTA-246H3.1\tIGLV3-25\nCTA-246H3.1\tIGKC\nCTA-246H3.1\tIGKV1OR15-118\nCTA-246H3.1\tIGHM\nCTA-246H3.1\tIGLJ3\nCTA-246H3.1\tIGKV1OR2-108\nCTA-246H3.1\tIGL@\nCTA-246H3.1\tIGHG3\nCTA-246H3.1\tIGKV1-5\nCTA-246H3.1\tIGKV1D-13\nCTA-246H3.1\tLOC91316\nCTA-246H3.1\tIGLC1\nCTA-246H3.1\tIGHA1\n218041_x_at\tBCLAF1\n218041_x_at\tPUM2\n218041_x_at\tSLC38A2\nMEFV\tSPN\nSMTN\tTGFB1I1\nZNF160\tZNF611\nTUFM\tUQCRFS1\nTUFM\tUQCRC1\nCD164\tCOPB\nARF5\tPOLR2J\nARF5\tATP6V0D1\nHLA-B\tMYH9\nHLA-B\tMAN2B1\nHLA-B\tHLA-G\nHLA-B\tHLA-F\nHLA-B\tHLA-E\nHLA-B\tHLA-C\nHLA-B\tHLA-J\nIGHD\tIGHM\nADRM1\tEXOSC4\nADRM1\tAP2S1\nADRM1\tFIBP\nADRM1\tAURKAIP1\n211650_x_at\tIGLV3-25\n211650_x_at\tIGHG3\n211650_x_at\tIGKC\n211650_x_at\tIGHV1-69\n211650_x_at\tIGHM\nFLJ12151\tPPP2CA\nFLJ12151\tLOC152719\nFLJ12151\tPRR11\nFLJ12151\tPDE4C\nFLJ12151\tZNF611\nFLJ12151\tSLC35E1\nFLJ12151\tZNF160\nHNRPK\tHNRPU\nHNRPK\tSET\nTOMM20\tZMYND11\nTOMM20\tTTC3\nTOMM20\tTTC19\nCORO1A\tLAPTM5\nAD7C-NTP\tARL6IP2\nAD7C-NTP\tMEFV\nAD7C-NTP\tCDC5L\nARL6IP2\tCDC5L\nARL6IP2\tLOC56902\nARL6IP2\tMEFV\nC1orf63\tPAPOLA\nC1orf63\tLOC342346\nRPL31\tRPL5\nRPL31\tTPT1\nRPL31\tRPS27A\nRPL31\tRPS7\nRPL31\tRPS17\nRPL31\tRPL9\nRPL31\tRPS3A\nRPL31\tRPL6\nRPL31\tRPL35A\nRPL31\tRPL36A\nRPL31\tRPL7\nRPL31\tRPS6\nRPL31\tRPL4\nRPL31\tRPS24\nRPL31\tRPS23\nRPL31\tRPL34\nRPL31\tRPS4X\nSDHA\tTUFM\nUQCR\tUQCRQ\nUQCR\tUQCRC1\nFNBP4\tRPL26\nFNBP4\tRNPC2\nFNBP4\tRPL17\nFNBP4\tOGT\nFNBP4\tRPL6\nFNBP4\tRPS27A\nPNRC2\tZNF638\nPNRC2\tPUM2\nPNRC2\tSF3B1\nPNRC2\tROCK1\nIGLV3-10\tIGLV3-25\nMSN\tPRG1\nPPIB\tRPN1\nPPIB\tSEC61A1\nTHRAP1\tZNF638\nIRF7\tPRKD2\nIRF7\tMAN2B1\n221651_x_at\tIGLJ3\n221651_x_at\tIGKV1OR15-118\n221651_x_at\tIGLC2\n221651_x_at\tIGKV1-5\n221651_x_at\tIGKC\n221651_x_at\tIGKV1OR2-108\n221651_x_at\tIGHG3\n221651_x_at\tLOC91316\n221651_x_at\tIGKV1D-13\n221651_x_at\tCTA-246H3.1\n221651_x_at\tIGHA1\n221651_x_at\tIGLC1\nLOC152719\tPRR11\nLOC152719\tLRRFIP1\nLOC152719\tPDE4C\nLOC152719\tRIOK3\nLOC152719\tZNF611\nLOC152719\tSLC35E1\nLOC152719\tZNF160\nPRKAR1A\tRNF11\nPRKAR1A\tSRP9\nPRKAR1A\tTMEM66\nINPP5D\tITGB2\n216524_x_at\tZNF611\n216524_x_at\tZNF160\n216524_x_at\tPGF\n216524_x_at\tKNS2\n216524_x_at\tCEP27\n216524_x_at\tRIOK3\n216524_x_at\tDBT\n216524_x_at\tPDE4C\n216524_x_at\tFBXW12\nRPL10A\tRPL39\nRPL10A\tRPL14\nRPL10A\tRPL12\nRPL27\tRPS20\nRPL27\tRPS11\nRPL27\tRPLP2\nRPL27\tRPL38\nRPL27\tRPS19\nRPL27\tRPL27A\nERH\tHNRPK\nERH\tSUMO2\nPGF\tPOLR1B\nPGF\tPRR11\nPGF\tZNF160\nPGF\tWDR45\nPGF\tRIOK3\nPGF\tZNF611\nCAPNS1\tCLPTM1\nCLPTM1\tNAPA\nRPS19\tRPS2\nRPS19\tRPS21\nRPS19\tRPS20\nDDX42\tMT1H\nDDX42\tMT1M\nDDX42\tMT1G\nDDX42\tMT1F\nMT1H\tMT1L\nMT1H\tMT2A\nMT1H\tMT1M\nMT1H\tMT1X\nCD37\tPTPRCAP\nCD37\tISG20\nPTPRCAP\tTRBV3-1\nEIF3S6\tRPS4X\nEIF3S6\tRPL26\nEIF3S6\tZNF638\nEIF3S6\tMDS1\nEIF3S6\tRPS3A\nEIF3S6\tRPL24\nEIF3S6\tRPL7\nEIF3S6\tRPL31\nEIF3S6\tRPL4\nEIF3S6\tRPL21\nEIF3S6\tU16\nEIF3S6\tNPM1\nEIF3S6\tRNPC2\nEIF3S6\tNACA\nEIF3S6\tRPS7\nEIF3S6\tTINP1\nEIF3S6\tVDP\nEIF3S6\tRPS27A\nEIF3S6\tRPL34\nEIF3S6\tRPL5\nEIF3S6\tRPS24\nEIF3S6\tRPL17\nEIF3S6\tRPL6\nIGKV1OR15-118\tIGL@\nIGKV1OR15-118\tIGLC2\nIGKV1OR15-118\tIGLV3-25\nNDUFB6\tTCEB2\nACTA2\tLMOD1\nACTA2\tMYH11\nACTA2\tTAGLN\nGMFG\tITGB2\nHNRPU\tSET\nHNRPH3\tLOC58486\nHNRPH3\tRNPC2\nHNRPH3\tMATR3\nHNRPH3\tKIDINS220\nC16orf24\tSCAND1\nNBPF1\tNBPF12\nARPP-19\tPRKACB\n217052_x_at\tG3BP\n217052_x_at\t220725_x_at\nCCL5\tHLA-F\nCCL5\tHLA-C\nCCL5\tHLA-G\nRPLP2\tRPS16\nRPLP2\tRPS20\nRPLP2\tRPS19\nRPLP2\tRPS11\nECH1\tUQCRQ\nRPL12\tRPL14\nRPL12\tRPL39\nRPL12\tRPL19\nCECR1\tLAPTM5\nATP6V0D1\tCHMP2A\nATP6V0D1\tPOLR2J\nATP6V0D1\tPSMB3\nATP6V0D1\tOAZ2\nCBX3\tHNRPA2B1\nCBX3\tXPO1\nSFRS10\tSON\nTIMM8B\tUQCRC1\nCD48\tIL16\nARF1\tCOPE\nARF1\tYIPF3\nARF1\tCAPNS1\nARF1\tRPN1\nKPNB1\tPAFAH1B1\nHLA-J\tIFI30\nHLA-J\tMAN2B1\nUSP47\tZNF638\nUSP47\tVDP\nRPL36\tRPS16\nRPL36\tRPS18\nRPL36\tRPS19\nRPL36\tRPS10\nCUL4B\tSH3BGRL\nADD3\tWAC\nADD3\tSEPT7\nADD3\tMATR3\nDEXI\tNDUFB11\nLYSMD4\tSEPT7\nPFAAP5\tZNF638\nPFAAP5\tRNPC2\n215182_x_at\tSPN\n215182_x_at\tCDC5L\n215182_x_at\t220725_x_at\n215182_x_at\tLOC56902\n215182_x_at\tMCM3AP\nHLA-DQA1\tHLA-DRB4\nHLA-DQA1\tHLA-DRA\nDDT\tZNHIT1\nDDT\tMRPS12\nRAVER2\tSF3B1\nHLA-DMA\tHLA-DQB1\nHLA-DMA\tHLA-DRA\nHLA-DMA\tHLA-DRB1\nHLA-DMA\tHLA-DPA1\nHLA-DMA\tHLA-DQA1\nHLA-DMA\tHLA-DRB4\nHLA-DMA\tHLA-DPB1\nHLA-DMA\tHLA-DRB5\nRAB6A\tRAB6C\nC1orf160\tTCEB2\nC1orf160\tNDUFA13\n217679_x_at\tPDE4C\n217679_x_at\tDBT\n217679_x_at\tPGF\n217679_x_at\tCEP27\n217679_x_at\tZNF160\n217679_x_at\tFLJ12151\n217679_x_at\tSLC35E1\n217679_x_at\tKNS2\n217679_x_at\tFBXW12\n217679_x_at\tZNF611\nRPL21\tRPL26\nRPL21\tRPL36A\nRPL21\tRPL5\nRPL21\tRPL9\nRPL21\tRPL30\nRPL21\tRPS17\nRPL21\tTPT1\nRPL21\tRPS7\nRPL21\tRPL39\nRPL21\tRPS23\nRPL21\tRPL24\nRPL21\tRPS27A\nRPL21\tRPS3A\nRPL21\tU16\nRPL21\tRPL22\nRPL21\tRPS6\nRPL21\tRPL6\nRPL21\tRPL23\nRPL21\tRPL7\nRPL21\tRPS4X\nRPL21\tRPL4\nRPL21\tRPS24\nRPL21\tRPL34\nRPL21\tRPL31\nRPS13\tRPS20\nRPS13\tRPS27\nRPL18\tRPS2\nRPL18\tRPL18A\nRPS11\tRPS20\nNPTN\tRAB1A\nNPTN\tPRKAR1A\nMCM3AP\tOPHN1\nMCM3AP\tORC6L\nEIF4G1\tTHRAP5\n216412_x_at\tIGLV3-10\n216412_x_at\tIGLC2\nCXCR4\tLAPTM5\nCXCR4\tPRG1\nCXCR4\tHCLS1\nFAU\tRPS9\nFAU\tRPL35\nFAU\tRPS5\nFAU\tRPS16\nFAU\tRPL28\nRPL39\tRPL4\nRPL39\tRPS4X\nRPL39\tRPS20\nRPL39\tRPS15A\nRPL39\tRPS17\nRPL39\tRPS23\nIL2RG\tTRA@\nKIDINS220\tZFR\nKIDINS220\tSEPT7\nCOX4I1\tNDUFA7\nORC6L\tSLC25A16\nORC6L\tRPS11\nGBL\tGPX4\nATPIF1\tFIBP\nMGC2474\tTXNL2\nSEPT2\tSFRS11\nMT1F\tMT1L\nMT1F\tMT1X\nMT1F\tMT2A\nMT1F\tMT1G\nMT1F\tMT1H\nMT1F\tMT1M\nMT1L\tMT1M\nMT1L\tMT2A\nMT1L\tMT1X\nC11orf2\tFLJ14346\nATP6AP2\tPRKAR1A\nSTAT1\tTRIM22\nRPL35\tRPS9\nRPL35\tRPS5\nRPL35\tRPS16\nMT1E\tMT1L\nMT1E\tMT1F\nMT1E\tMT1G\nMT1E\tMT1X\nMT1E\tMT2A\nMT1E\tMT1M\nMT1E\tMT1H\nAKT1\tPGLS\nLOC56902\tMCM3AP\nLOC56902\tMGC2474\nHNRPM\tSERBP1\nCALD1\tPLEKHC1\nCALD1\tTPM1\nMT1M\tMT2A\nMT1M\tMT1X\nC17orf62\tIRF7\nKIAA1840\tPAPOLA\nDES\tTPM1\nMASP1\tPOLR2J\n212498_at\tTTC19\nHLA-DPB1\tHLA-DQB1\nHLA-DPB1\tHLA-DQA1\nHLA-DPB1\tHLA-DRB1\nHLA-DPB1\tHLA-DRB4\nHLA-DPB1\tHLA-DRB5\nFBXW12\tPRR11\nFBXW12\tRIOK3\nFBXW12\tPDE4C\nFBXW12\tFLJ42393\nFBXW12\tFLJ12151\nFBXW12\tPGF\nFBXW12\tSLC35E1\nFBXW12\tZNF611\nFBXW12\tLOC152719\nFBXW12\tKNS2\nFBXW12\tZNF160\nREEP5\tZMYND11\nPHIP\tZNF638\nRPS5\tRPS9\nWDR45\tZNF611\nMT1X\tMT2A\nCNOT2\tFLJ11021\nP4HB\tSEC61A1\nTMEM93\tUQCRFS1\nRPL37\tRPS15A\nRPL37\tRPS17\nRPL37\tRPS13\nRPL37\tRPL39\nACO2\tNDUFB2\nMT1G\tMT1L\nMT1G\tMT1M\nMT1G\tMT2A\nMT1G\tMT1H\nMT1G\tMT1X\nATP5J2\tUCRC\nRPL32\tRPL35A\nRPL32\tRPS15A\nRPL32\tRPS17\nRPL32\tRPL37\nGOLGA8A\tGOLGA8B\nCAST\tNFE2L2\nLOC645745\tMT1L\nLOC645745\tMT1F\nLOC645745\tMT2A\nLOC645745\tMT1M\nLOC645745\tMT1E\nLOC645745\tMT1X\nLOC645745\tMT1G\nLOC645745\tMT1H\nITGAV\tZMYND11\nCNN1\tMYH11\n208238_x_at\tRIOK3\nFLJ20294\tRPLP2\nFLJ20294\tRPS11\nFLJ20294\tRPL38\nFLJ20294\tH3F3A\nFLJ20294\tRPS20\nFLJ20294\tRPS19\nFLJ20294\tRPL27A\nFLJ20294\tRPL27\nSPCS2\tSRP9\n208120_x_at\tORC6L\n', - }, - { - kind: 'annotation', - name: 'Co-expression KEGG pathways', - fileName: 'NORMA_Human_coexpression_Annotation_KEGG.txt', - text: 'Ribosome\tRPS5,RPS5,RPL35,FAU,RPL18,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL10A,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL23A,RPL36A,RPL7,RPS2,U16,RPL35A,RPL18A,RPL13,RPL29,RPS15A,RPS3A,RPL14,RPL22,RPL34,RPS27A,RPL9,RPL6,RPL4,RPS4X,RPS9,RPL13A,RPL17,RPL5,MDS1,LOC402057\nOxidative phosphorylation\tNDUFC1,NDUFC1,ATP5J2,NDUFA10,NDUFA7,COX4I1,ATP6V0D1,NDUFB6,UCRC,SDHA,ATP6V0B,UQCRC1,NDUFB2,UQCRFS1,NDUFA2,NDUFV1,ATP6V0C,NDUFA3,NDUFA13,NDUFS7,COX5B,ATP5D,NDUFS3,NDUFS8,COX5A,NDUFB4,NDUFAB1,ATP5G3,CYC1,COX8A,RPA1\nAntigen processing and presentation\tHLA-DPB1,HLA-DPB1,TAPBP,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,HLA-DRB5,CD74,HLA-DRB1,HLA-E,HLA-DRB4,HSP90AA1,C6orf12,HLA-DQB1,HLA-DPA1,IFI30,HLA-A,HLA-G,EIF3S3,HLA-C\nCell adhesion molecules (CAMs)\tITGAV,ITGAV,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,SELL,HLA-DRB5,HLA-DRB1,HLA-E,HLA-DRB4,CD2,C6orf12,HLA-DQB1,HLA-DPA1,HLA-A,SPN,HLA-G,ITGB2,HLA-C\nType I diabetes mellitus\tHLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-E,HLA-DRB4,C6orf12,HLA-DQB1,HLA-DPA1,HLA-A,HLA-G,HLA-C\nFocal adhesion\tITGAV,ITGAV,AKT1,RAP1B,LAMC1,PGF,MYLK,COL6A3,COL1A1,COL6A1,ROCK1,COL4A2,COL4A1,BIRC2,COL3A1,COL1A2,CAV1,RAC2\nNatural killer cell mediated cytotoxicity\tHLA-J,HLA-J,CD48,HLA-B,HLA-E,C6orf12,IFNGR1,HLA-A,LCK,HLA-G,RAC2,ITGB2,HLA-C\nRegulation of actin cytoskeleton\tITGAV,ITGAV,MSN,MYH9,MYLK,ARPC1B,ROCK1,ARPC3,ARPC5,IQGAP1,RAC2,ITGB2\nCell Communication\tDES,DES,LAMC1,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2,RPL38\nPyrimidine metabolism\tPOLR2L,POLR2L,POLR2J,HMHA1,POLR1B,208246_x_at,POLR2I,RPS15A,POLR2E\nECM-receptor interaction\tITGAV,ITGAV,LAMC1,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2\nPurine metabolism\tGUK1,GUK1,POLR2L,POLR2J,HMHA1,POLR1B,POLR2I,RPS15A,POLR2E,PDE4C\nHematopoietic cell lineage\tCD37,CD37,HLA-DRA,CD3D,HLA-DRB5,HLA-DRB1,HLA-DRB4,CD2,CD7,IL7R\nCytokine-cytokine receptor interaction\tIL2RG,IL2RG,CXCR4,CCL5,IL10RA,LTB,IFNGR1,TGFB1,IL7R,IL6ST\nLeukocyte transendothelial migration\tRAP1B,RAP1B,CXCR4,NCF1,MSN,CYBA,ROCK1,MMP2,RAC2,ITGB2\nJak-STAT signaling pathway\tAKT1,AKT1,STAT1,IL2RG,RAVER2,IL10RA,IFNGR1,IL7R,IL6ST\nCell cycle\tORC6L,ORC6L,YWHAZ,217718_s_at,TGFB1,CDKN1B,SKP1A\nRNA polymerase\tPOLR2L,POLR2L,POLR2J,HMHA1,POLR1B,POLR2I,POLR2E\nApoptosis\tAKT1,AKT1,PRKAR1A,ENDOG,PRKAR2A,BIRC2,PRKACB\nCholera - Infection\tSEC61A1,SEC61A1,ATP6V0D1,ATP6V0B,ATP6V0C\nPathogenic Escherichia coli infection - EPEC\tHCLS1,HCLS1,YWHAZ,ROCK1,NCL,RPL38,ARPC5\nPathogenic Escherichia coli infection - EHEC\tHCLS1,HCLS1,YWHAZ,ROCK1,NCL,RPL38,ARPC5\nEpithelial cell signaling in Helicobacter pylori infection\tATP6V0D1,ATP6V0D1,CCL5,ATP6V0B,ATP6V0C\nWnt signaling pathway\tROCK1,ROCK1,SKP1A,CSNK1A1,RAC2,PRKACB\nPancreatic cancer\tAKT1,AKT1,STAT1,RAVER2,PGF,TGFB1,RAC2\nMAPK signaling pathway\tAKT1,AKT1,RAP1B,TGFB1,RAC2,PRKACB\nInsulin signaling pathway\tAKT1,AKT1,PRKAR1A,PRKAR2A,PRKACB\nProtein export\tSEC61A1,SEC61A1,SRP72,SRP9\nUbiquitin mediated proteolysis\tTCEB2,TCEB2,SKP1A,UBE2D2\nCitrate cycle (TCA cycle)\tACO2,ACO2,SDHA,MDH2,RPA1\nTGF-beta signaling pathway\tROCK1,ROCK1,TGFB1,SKP1A\nCalcium signaling pathway\tMYLK,MYLK,ATP2A3,PRKACB\nHedgehog signaling pathway\tCSNK1A1,CSNK1A1,PRKACB\nChronic myeloid leukemia\tAKT1,AKT1,TGFB1,CDKN1B\nAxon guidance\tCXCR4,CXCR4,ROCK1,RAC2\nToll-like receptor signaling pathway\tAKT1,AKT1,STAT1,CCL5\nTight junction\tAKT1,AKT1,MYH9,HCLS1\nGlycan structures - degradation\tPOPDC3,POPDC3,MAN2B1\nColorectal cancer\tAKT1,AKT1,TGFB1,RAC2\nB cell receptor signaling pathway\tAKT1,AKT1,CD79A,RAC2\nPrion disease\tNFE2L2,NFE2L2,LAMC1\nT cell receptor signaling pathway\tAKT1,AKT1,CD3D,LCK\nLong-term potentiation\tRAP1B,RAP1B,PRKACB\nAdherens junction\tIQGAP1,IQGAP1,RAC2\nGlycan structures - biosynthesis 2\tCOL6A3,COL6A3,OGT\nGnRH signaling pathway\tMMP2,MMP2,PRKACB\nAdipocytokine signaling pathway\tAKT1,AKT1,RAVER2\nBasal transcription factors\tTAF7,TAF7,GTF2I\nVEGF signaling pathway\tAKT1,AKT1,RAC2\nReductive carboxylate cycle (CO2 fixation)\tACO2,ACO2,MDH2\nGlyoxylate and dicarboxylate metabolism\tACO2,ACO2,MDH2\nGlutathione metabolism\tGPX1,GPX1,GPX4\nFc epsilon RI signaling pathway\tAKT1,AKT1,RAC2\nArachidonic acid metabolism\tGPX1,GPX1,GPX4\nmTOR signaling pathway\tAKT1,AKT1,PGF\nTyrosine metabolism\tECH1,ECH1,MIF\n', - }, - { - kind: 'annotation', - name: 'Co-expression GO biological process', - fileName: 'NORMA_Human_coexpression_Annotation_GO_BP.txt', - text: 'protein biosynthesis\tRPS15,RPS15,RPL37,RPS5,RPL35,FAU,RPS11,RPS21,RPL18,RPS13,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,MRPS12,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL19,RPS16,RPS14,NARS,RPL7,RPS6,RPS2,U16,NACA,RPL18A,RPL13,RPL29,RPS15A,RPS20,RPS3A,RPS18,RPL14,MRPL34,RPL22,RPL34,RPS27A,EEF1D,RPL15,RPL9,RPL6,RPL26,RPL4,RPS4X,RPS24,RPS9,RPL13A,RPL17,EEF1A1,RPL5,RPL23,MDS1,RPS7,LOC402057,RPS25\nimmune response\tHLA-DPB1,HLA-DPB1,TAPBP,IL2RG,HLA-DMA,HLA-DQA1,IL16,221651_x_at,IGLV3-10,ARL6IP2,HLA-B,CD164,HLA-DRA,HLA-DMB,TRIM22,HLA-DRB5,HLA-DRB1,IGL@,IGLC1,IGLV3-25,TRB@,HLA-DRB4,LTB,ARHGDIB,C6orf12,HLA-DQB1,HLA-DPA1,TRBV3-1,GPSM3,IGLC2,TRA@,214836_x_at,IGHV1-69,IGHA1,HLA-A,CD7,IGHG3,IGLJ3,IL7R,IL6ST,HLA-C,IGKC,IGHM\nsignal transduction\tAKT1,AKT1,STAT1,IL2RG,CXCR4,OPHN1,CCL5,PGF,INPP5D,PRKAR1A,CD164,RPS27,HLA-DRB5,CD74,HLA-DRB1,PRKAR2A,CD53,HLA-DRB4,LTB,HSP90AA1,OGT,ROCK1,IFNGR1,PTGES3,HMGB1,GTF2I,GNB2L1,CAP1,SPN,NPM1,IQGAP1,PAFAH1B1,EIF3S3,RAC2,COPS2,PRKACB,PDE4C\nantigen presentation, endogenous antigen\tHLA-J,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,CD74,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM\nantigen processing, endogenous antigen via ...\tCAST,CAST,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM\nregulation of transcription, DNA-dependent\tNFE2L2,NFE2L2,STAT1,MGC2474,LYSMD4,ZMYND11,ZNF160,CDC5L,TRIM22,SFPQ,IFI16,HCLS1,ATRX,HMGB1,CSDE1,SCAND1,RPL6,LOC342346,SUB1\nelectron transport\tMT1X,MT1X,MT1L,COX4I1,NCF1,SDHA,UQCRQ,UQCRC1,GPX4,UQCRFS1,NDUFV1,COX5B,NDUFS3,NDUFS8,COX5A,NDUFB4,CYC1,COX8A,RPA1\nprotein amino acid phosphorylation\tAKT1,AKT1,RAVER2,GMFG,PRKAR1A,PRKD2,MYLK,PRKAR2A,ROCK1,TGFB1,PCM1,U16,LCK,CSNK1A1,HNRPDL,RPL4,PRKACB\nantigen processing, exogenous antigen via M...\tHLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1\nantigen presentation, exogenous antigen\tHLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1\ncell adhesion\tITGAV,ITGAV,LAMC1,CCL5,FNBP4,ADRM1,CD164,TGFB1I1,SELL,DGCR6,COL6A1,PLEKHC1,CD2,ITGB2\nintracellular signaling cascade\tSTAT1,STAT1,RAVER2,INPP5D,PRKAR1A,PRKD2,NCF1,PRKAR2A,WSB1,HCLS1,HMHA1,ROCK1,LCK\ntranscription from RNA polymerase II promoter\tTARDBP,TARDBP,NFE2L2,STAT1,POLR2J,TGFB1I1,TCEA1,HMHA1,POLR2I,BTF3,COPS2,POLR2E\ncell motility\tCALD1,CALD1,CAPZA1,CCL5,TPM1,MSN,ACTR3,ARPC1B,ARHGDIB,ARPC3,ARPC5,PAFAH1B1\ndetection of pest, pathogen or parasite\tHLA-DPB1,HLA-DPB1,HLA-DMA,HLA-J,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-G\nnuclear mRNA splicing, via spliceosome\tHNRPM,HNRPM,SFRS10,HNRPH3,SFPQ,HIATL1,RPL35A,SF3B1,HNRPA2B1,HNRPA1\nmitochondrial electron transport, NADH to u...\tNDUFC1,NDUFC1,NDUFB6,NDUFB2,NDUFV1,NDUFS7,NDUFS3,NDUFS8,NDUFB4\ndevelopment\tCECR1,CECR1,CLPTM1,CD164,ARHGDIB,COL6A3,MYH11,ITGB2,DDX1\nphosphate transport\tCOL6A3,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2\nantigen presentation\tHLA-J,HLA-J,HLA-B,HLA-E,C6orf12,HLA-F,HLA-A,HLA-G,HLA-C\ntransport\tG3BP,G3BP,SLC25A16,UCP2,SLC25A11,TMED5,SLC25A3,ATP2A3\ncell proliferation\tRPS21,RPS21,PGF,ZMYND11,RPS27,CD74,IFI16,MIF,ISG20\nanti-apoptosis\tAKT1,AKT1,FAIM3,SON,HMGB1,HSP90B1,NPM1,TPT1,PRDX2\ncell surface receptor linked signal transdu...\tCD3D,CD3D,CD2,CD79A,BIRC2,MIF,CSNK1A1,IL7R,IL6ST\ngeneration of precursor metabolites and energy\tACO2,ACO2,ATPIF1,COX4I1,ECH1,UQCR,SLC25A3,COX8A\nprotein complex assembly\tTAPBP,TAPBP,CAPZA1,GPAA1,LAMC1,TCEB2,CD3D,CD74\nmRNA processing\tSFPQ,SFPQ,SRP46,DHX15,HNRPR,SFRS11,SFRS5,RNPC2\npositive regulation of cell proliferation\tSSR1,SSR1,CAPNS1,PGF,RPL31,HCLS1,TGFB1,NAP1L1\nRNA splicing\tHNRPH3,HNRPH3,SFPQ,DHX15,PPP2CA,ZNF638,SFRS11\ntranscription\tLYSMD4,LYSMD4,POLR2J,ZNF160,SFPQ,POLR1B,PCM1\nregulation of translational initiation\tEIF4G1,EIF4G1,EIF4A2,EIF4G2,EIF5,EIF3S3,DDX1\npathogenesis\tHLA-DPB1,HLA-DPB1,HLA-DRB5,HLA-DRB1,HLA-DRB4\nproton transport\tATP5J2,ATP5J2,ATP6V0D1,UCP2,ATP6V0B,ATP6V0C\nRNA processing\tHNRPU,HNRPU,RBPMS,DDX17,HNRPH1,HNRPDL,RNPC2\nregulation of transcription from RNA polyme...\tTHRAP5,THRAP5,PRKAR1A,HMGB1,LOC342346,SUB1\nprotein transport\tRAB1A,RAB1A,RAB6A,RAB6C,TLOC1,HSP90B1,COPB\nsmall GTPase mediated signal transduction\tRAP1B,RAP1B,RGS19,RAB1A,RAB6A,RAB6C,RPL29\nresponse to virus\tCCL5,CCL5,IRF7,TRIM22,IFI16,IFNGR1,ISG20\ninflammatory response\tCCL5,CCL5,IRF7,MEFV,ALOX5AP,TGFB1,ITGB2\nactin cytoskeleton organization and biogenesis\tARHGDIB,ARHGDIB,PLEKHC1,DST,ROCK1,ARPC5\nprotein folding\tPPIB,PPIB,HSP90AA1,HSP90B1,EIF3S3,KTN1\ninduction of apoptosis\tCD2,CD2,PPP2CA,BCLAF1,LCK,RPS3A,SCAND1\nskeletal development\tCNOT2,CNOT2,COL1A1,TGFB1,COL1A2,MYH11\nregulation of progression through cell cycle\tSTAT1,STAT1,PGF,ERH,PPP2CA,LCK,SCAND1\nnegative regulation of cell proliferation\tCD164,CD164,TGFB1I1,TGFB1,CDKN1B,NPM1\ncellular defense response\tHLA-J,HLA-J,CCL5,NCF1,FAIM3,SPN,HLA-G\ntranslational elongation\tRPLP2,RPLP2,TUFM,EEF1B2,EEF1D,EEF1A1\nmuscle development\tTAGLN,TAGLN,LYSMD4,SMTN,COL6A3,MBNL1\nATP synthesis coupled proton transport\tATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3\nproteolysis\tCAST,CAST,MASP1,YME1L1,MMP2,IGHG3\nmicrotubule-based movement\t220725_x_at,220725_x_at,KNS2,KTN1\nmetabolism\tACO2,ACO2,ECH1,POPDC3,IGLC2,DIP2A\napoptosis\tAD7C-NTP,AD7C-NTP,FIS1,ITGB2,CROP\namino acid transport\tSLC38A2,SLC38A2,218041_x_at,RPL24\nresponse to unfolded protein\tHSP90AA1,HSP90AA1,HSP90B1,EIF3S3\nintracellular protein transport\tVDP,VDP,NAPA,CLTC,CD74,NPM1,COPE\ntranscription initiation from RNA polymeras...\tTHRAP5,THRAP5,IRF7,THRAP1,GTF2I\nresponse to oxidative stress\tCCL5,CCL5,GPX1,GPX4,CSDE1,PRDX2\nnucleobase, nucleoside, nucleotide and nucl...\tNDUFA10,NDUFA10,ERH,208246_x_at\ncarbohydrate metabolism\tLOC91316,LOC91316,MAN2B1,AKR7A2\nprotein modification\tSUMO2,SUMO2,RPN1,MAN2B1,RPS27A\nubiquitin-dependent protein catabolism\tUSP34,USP34,UBE4A,PSMB9,PSMB8\ndefense response\tCD48,CD48,PTPRCAP,HLA-B,CD79A\nDNA replication\tMCM3AP,MCM3AP,NARS,NAP1L1,SET\nandrogen receptor signaling pathway\tTHRAP5,THRAP5,THRAP1,TGFB1I1\nprotein targeting to mitochondrion\tTIMM8B,TIMM8B,TOMM20,TIMM13\noxygen transport\tSLC38A2,SLC38A2,218041_x_at\nG-protein coupled receptor protein signalin...\tAKT1,AKT1,RGS19,CXCR4,FNBP4\nnervous system development\tOPHN1,OPHN1,MBNL1,PAFAH1B1\nmitochondrial transport\tUCP2,UCP2,HSP90AA1,EIF3S3\nembryo implantation\tLOC283412,LOC283412,RPL29\nDNA repair\tSFPQ,SFPQ,ATRX,HMGB1,RPA1\nprotein refolding\tHSP90AA1,HSP90AA1,EIF3S3\npositive regulation of nitric oxide biosynt...\tHSP90AA1,HSP90AA1,EIF3S3\nlipid metabolism\tPAFAH1B1,PAFAH1B1,ATP8B1\ncell cycle arrest\tEIF4G2,EIF4G2,DST,CDKN1B\ncell cycle\tCUL4B,CUL4B,RPS27A,SEPT7\nprotein ubiquitination\tUBE4A,UBE4A,PCNP,RPS27A\nprotein targeting\tTOMM20,TOMM20,SRP9,COPB\nnucleosome assembly\tNAP1L1,NAP1L1,SET,H3F3A\nnegative regulation of cell adhesion\tCD164,CD164,ARHGDIB,SPN\ncell-cell signaling\tCCL5,CCL5,PGF,LTB,ITGB2\npositive regulation of transcription\tTGFB1I1,TGFB1I1,RPS27A\nnegative regulation of transcription\tIRF7,IRF7,SUMO1,BCLAF1\nhemopoiesis\tCD164,CD164,ZNF160,LCK\ncellular defense response (sensu Vertebrata)\tIGHA1,IGHA1,IGHG3,IGHM\nretrograde vesicle-mediated transport, Golg...\tTAPBP,TAPBP,COPE,COPB\nprotein import into nucleus\tMCM3AP,MCM3AP,NDUFA13\npositive regulation of transcription, DNA-d...\tTGFB1I1,TGFB1I1,TGFB1\nnegative regulation of cell growth\tPPP2CA,PPP2CA,NDUFA13\nintegrin-mediated signaling pathway\tITGAV,ITGAV,DST,ITGB2\ncotranslational protein targeting to membrane\tSSR1,SSR1,TLOC1,TRAM1\nRho protein signal transduction\tARHGDIB,ARHGDIB,ROCK1\nsensory perception of sound\tTIMM8B,TIMM8B,TIMM13\nregulation of translation\tPPP2CA,PPP2CA,EEF1A1\nregulation of transcription\tPOLR2L,POLR2L,PPP2CA\nprostaglandin biosynthesis\tCD74,CD74,PTGES3,MIF\nmitosis\tTARDBP,TARDBP,CORO1A\ninsulin receptor signaling pathway\tPHIP,PHIP,AKT1,MYH11\nDNA recombination\tATRX,ATRX,HMGB1,RPA1\ntricarboxylic acid cycle\tACO2,ACO2,SDHA,RPA1\nnegative regulation of transcription, DNA-d...\tRPS14,RPS14,NDUFA13\nintra-Golgi vesicle-mediated transport\tNAPA,NAPA,COPE,COPB\ncell death\tEIF4G2,EIF4G2,TGFB1\nprotein amino acid autophosphorylation\tRPS2,RPS2,U16,RPL4\ncirculation\tRPL31,RPL31,COL3A1\ncalcium ion transport\tATP2A3,ATP2A3,TPT1\naxon guidance\tOPHN1,OPHN1,RPS27A\nER-associated protein catabolism\tRPL27,RPL27,RPS27A\nregulation of actin filament polymerization\tARPC3,ARPC3,ARPC5\npositive regulation of I-kappaB kinase/NF-k...\tBIRC2,BIRC2,EEF1D\norgan morphogenesis\tDGCR6,DGCR6,TGFB1\nnegative regulation of transcription from R...\tIRF7,IRF7,ZMYND11\nmRNA splice site selection\tSRP46,SRP46,SFRS5\nerythrocyte differentiation\tRPS19,RPS19,HCLS1\ncytokinesis\tSEPT2,SEPT2,SEPT7\nchromosome segregation\tARL8B,ARL8B,RIOK3\ncell-matrix adhesion\tITGAV,ITGAV,ITGB2\nactivation of NF-kappaB transcription factor\tU16,U16,NPM1,RPL4\nT cell activation\tCD3D,CD3D,CD2,CD7\nubiquitin cycle\tTPT1,TPT1,UBE2D2\nregulation of cell adhesion\tNPTN,NPTN,PPP2CA\ncell growth\tTGFB1,TGFB1,DDX5\nangiogenesis\tCAST,CAST,ATPIF1\ntranslational initiation\tEIF5,EIF5,RPS3A\ntransforming growth factor beta receptor si...\tFNTA,FNTA,TGFB1\nresponse to drug\tRAB6C,RAB6C,LCK\nregulation of apoptosis\tTPT1,TPT1,PRDX2\nchromosome organization and biogenesis (sen...\tATRX,ATRX,H3F3A\ncaspase activation\tSTAT1,STAT1,LCK\nantimicrobial humoral response (sensu Verte...\tIL7R,IL7R,ITGB2\nI-kappaB kinase/NF-kappaB cascade\tPHIP,PHIP,STAT1\ntransmembrane receptor protein tyrosine kin...\tCD7,CD7,COL1A2\nresponse to stress\tNPM1,NPM1,CROP\nresponse to metal ion\tMT1X,MT1X,MT1L\ncalcium ion homeostasis\tCCL5,CCL5,TPT1\nregulation of macrophage activation\tCD74,CD74,MIF\nnucleocytoplasmic transport\tNPM1,NPM1,SET\nnegative regulation of apoptosis\tCD74,CD74,MIF\nmuscle contraction\tDES,DES,CALD1\nestablishment and/or maintenance of cell po...\tCAP1,CAP1,SPN\nchemotaxis\tCCL5,CCL5,SPN\nRas protein signal transduction\tG3BP,G3BP,LCK\nregulation of heart contraction\tDES,DES,TPM1\nactivation of MAPKK activity\tU16,U16,RPL4\nJNK cascade\tU16,U16,RPL4\ncytoskeleton organization and biogenesis\tDES,DES,DST\n', - }, - { - kind: 'annotation', - name: 'Co-expression GO molecular function', - fileName: 'NORMA_Human_coexpression_Annotation_GO_MF.txt', - text: "protein binding\tTARDBP,TARDBP,ITGAV,CAST,RPS15,PHIP,NFE2L2,OAZ1,STAT1,SEPT2,TXNL2,ORC6L,IL2RG,RGS19,EIF4G1,RPS13,C1orf160,RAB6A,HLA-DMA,ZNHIT1,GPAA1,CUL4B,HLA-J,KPNB1,ARF1,IL16,CD48,CBX3,G3BP,SUMO2,ACTA2,TCEB2,EIF3S6,MT1H,RPS19,INPP5D,RAB6C,RNF11,PRKAR1A,NCF1,ZMYND11,HNRPK,SUMO1,TGFB1I1,CDC5L,KNS2,RBPMS,CAB39,SFPQ,CD3D,NAPA,ACTR3,CLTC,TTC3,EIF4A2,IFI16,ARL6IP5,EEF1B2,HCLS1,PSMB8,HMHA1,HSP90AA1,HNRPH1,SRP46,EIF4G2,FIS1,PLEKHC1,CD2,OGT,TAF7,217718_s_at,NARS,C6orf12,TGFB1,CDKN1B,NDUFA13,GPSM3,BCLAF1,HSP90B1,SERBP1,GTF2I,RPS6,U16,BIRC2,RPL35A,HLA-A,RPS15A,EXOSC4,RPS20,RPL38,ACTG2,CAV1,ACTR2,SKP1A,ACTR10,MIF,PCNP,RPS3A,RPL14,HLA-G,IL7R,AURKAIP1,FIBP,EIF3S3,HNRPDL,RPS27A,RAC2,ITGB2,COPS2,XPO1,SIAHBP1,POLR2E,NAP1L1,RPL4,SFRS5,HNRPA2B1,EEF1A1,UBE2D2,SEPT7,COPB,ATP5G3,RPL5,RPS7,RPA1,HNRPA1,RPL24\nRNA binding\tTARDBP,TARDBP,RPS5,HNRPM,LOC56902,EIF4G1,RPL18,RPL21,SFRS10,RPLP2,G3BP,HNRPH3,HNRPU,DDX42,RPS19,HNRPK,RBPMS,LOC440055,RPS27,LOC388344,SFPQ,DDX17,RPL27A,RPS10,RPS17,EIF4A2,RPL28,LOC283412,RPS14,HNRPH1,SRP46,EIF4G2,HNRPR,NCL,RPL7,HSP90B1,SRP9,HNRPA3P1,RPS2,U16,DDX5,ZNF638,RPL18A,RPL13,RPL29,SFRS11,DDX50,NPM1,RPS18,RPL14,PAPOLA,RPL22,MATR3,HNRPDL,RPL34,SCAND1,RPL9,RPL6,SIAHBP1,RPL4,RPS4X,SFRS5,SFRS3,HNRPA2B1,RPS9,RNPC2,RPL5,MDS1,LOC402057,HNRPA1\nstructural constituent of ribosome\tRPS15,RPS15,RPL37,RPS5,RPL35,FAU,RPS11,RPS21,RPL18,RPS13,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,MRPS12,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL19,RPS16,RPS14,RPL7,RPS6,RPS2,U16,RPL18A,RPL13,RPL29,RPS15A,RPS20,RPS3A,RPS18,RPL14,MRPL34,RPL22,RPL34,RPS27A,RPS23,RPL15,RPL9,RPL6,RPL26,RPL4,RPS4X,RPS24,RPS9,RPL13A,RPL17,RPL5,RPL23,MDS1,RPS7,LOC402057,RPS25\nnucleotide binding\tTARDBP,TARDBP,HNRPM,RAP1B,MCM3AP,RAB1A,RAB6A,RAVER2,SFRS10,G3BP,HNRPH3,ACTA2,DDX42,RAB6C,PRKD2,RBPMS,YME1L1,SFPQ,DDX17,EIF4A2,HSP90AA1,HNRPH1,SRP46,NARS,HNRPR,NCL,PCM1,HSP90B1,HNRPA3P1,U16,DDX5,ACTG2,SFRS11,DDX50,CSNK1A1,EIF3S3,MATR3,HNRPDL,DDX3X,SIAHBP1,RPL4,SFRS5,HNRPA2B1,EEF1A1,RNPC2,SEPT7,PRKACB,HNRPA1\nnucleic acid binding\tTARDBP,TARDBP,ZFR,HNRPM,LOC56902,MGC2474,RPS11,SFRS10,G3BP,HNRPH3,ZNF9,DDX42,HNRPK,ZNF160,MRPS12,SFPQ,DDX17,R3HCC1,EIF4A2,IFI16,HNRPH1,SRP46,NARS,ZNF611,SON,HNRPR,DDX5,SFRS11,DDX50,ZNF552,NPM1,MBNL1,MATR3,HNRPDL,DDX3X,SIAHBP1,SFRS5\nATP binding\tNDUFA10,NDUFA10,AKT1,RAVER2,ACTA2,DDX42,PRKD2,MYH9,220725_x_at,YME1L1,DDX17,EIF4A2,HSP90AA1,208246_x_at,DHX15,NARS,ROCK1,NDUFA13,PCM1,HSP90B1,ATP2A3,RPS2,U16,DDX5,LCK,MYH11,ACTG2,DDX50,CSNK1A1,EIF3S3,DDX3X,RPL4,PRKACB\nreceptor activity\tITGAV,ITGAV,HNRPM,IL2RG,CXCR4,THRAP5,PTPRCAP,RPL27,IL10RA,THRAP1,TLOC1,HLA-DRB5,HLA-DRB1,HLA-DRB4,CD2,IFNGR1,TRBV3-1,IGHA1,U16,CD7,IGHG3,IL7R,IL6ST,RPL4,EEF1A1,IGHM,TRAM1\nMHC class I receptor activity\tHLA-J,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM\nzinc ion binding\tCAST,CAST,ZFR,MT1E,MT1F,KPNB1,MSRB2,TIMM8B,ZNF9,DDX42,RNF11,PRKD2,SDHA,ZMYND11,ZNF160,MEFV,RPS27,TTC3,ZNF611,TIMM13,POLR2I,MMP2,ZNF552,MBNL1,MATR3\nbinding\tSRP72,SRP72,VDP,CAPZA1,TTC19,KPNB1,SLC25A16,MSN,UCP2,KNS2,CAB39,RGPD5,NAPA,SLC25A11,TTC3,SLC25A3,ALOX5AP,HIATL1,OGT,CHCHD2,SF3B1,COPS2,COPE,COPB\nDNA binding\tMCM3AP,MCM3AP,HNRPU,RNF11,IRF7,SDHA,POLR2L,POLR2J,CDC5L,SFPQ,POLR1B,SON,HMGB1,PCM1,BCLAF1,CSDE1,RPL6,LOC342346,SUB1,LOC58486,H3F3A\nNADH dehydrogenase (ubiquinone) activity\tNDUFC1,NDUFC1,NDUFA10,NDUFA7,NDUFB6,NDUFB2,NDUFA2,NDUFV1,NDUFA3,NDUFA13,NDUFS7,NDUFS3,NDUFS8,NDUFB4,NDUFAB1\nMHC class II receptor activity\tHLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1,HLA-C\nelectron carrier activity\tP4HB,P4HB,MT1X,MT1L,NCF1,UQCR,CYBA,FLJ14346,NDUFS3,NDUFS8,COX5A,ETFB,KTN1,AKR7A2,CYC1\nGTPase activity\tSEPT2,SEPT2,RAB6A,ARF1,RAB6C,NCF1,ARL6IP2,ARF5,ARL8B,RPL29,EIF5,RAC2,EEF1A1\nantigen binding\tIGL@,IGL@,IGLC1,IGLV3-25,IGLC2,IGHV1-69,IGHA1,IGHG3,IGLJ3,IL7R,IGKC,IGHM\ntransferase activity\tFNTA,FNTA,PRKD2,POLR2J,POLR1B,COL6A3,NARS,RPS2,U16,CSNK1A1,RPL4,PRKACB\ntranscription factor activity\tTARDBP,TARDBP,NFE2L2,STAT1,LYSMD4,MSRB2,TRIM22,HCLS1,GTF2I,SCAND1,MDS1\nmetal ion binding\tMT1X,MT1X,ZFR,MT1M,MT1L,PRKD2,ZNF160,TTC3,ZNF611,ZNF552,MATR3,NDUFS8\nstructural molecule activity\tDES,DES,FNBP4,CORO1A,NPEPPS,CLTC,COL6A3,RPL38,CAV1,MATR3,SEPT7\nGTP binding\tRAP1B,RAP1B,RAB1A,RAB6A,RAB6C,NCF1,ARL6IP2,ARL8B,EEF1A1,SEPT7\nextracellular matrix structural constituent\tLAMC1,LAMC1,FBLN1,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2\nstructural constituent of cytoskeleton\tDES,DES,ADD3,ACTA2,TPM1,MSN,ARPC1B,DST,ACTG2,ARPC3,ARPC5\nhydrolase activity\tDDX42,DDX42,PPP1CA,DDX17,EIF4A2,PPP2CA,DDX5,DDX50,DDX3X\nunfolded protein binding\tTAPBP,TAPBP,PPIB,HSP90AA1,PTGES3,HSP90B1,NPM1,EIF3S3\ntransporter activity\tG3BP,G3BP,UCP2,ATP6V0B,SLC25A11,SLC25A3,ATP5D,ATP5G3\nkinase activity\tGUK1,GUK1,MYLK,PRKAR2A,WSB1,208246_x_at,ROCK1,CDKN1B\niron ion binding\tACO2,ACO2,MT1X,MT1L,PPP1CA,NDUFV1,PPP2CA,NDUFS8,CYC1\nDNA-directed RNA polymerase activity\tPOLR2L,POLR2L,POLR2J,HMHA1,POLR1B,PCM1,POLR2I,POLR2E\nATP-dependent helicase activity\tDDX42,DDX42,DDX17,EIF4A2,DHX15,DDX5,DDX50,DDX3X,DDX1\nprotein serine/threonine kinase activity\tAKT1,AKT1,PRKD2,ROCK1,RPS2,U16,CSNK1A1,RPL4,PRKACB\ncalcium ion binding\tMASP1,MASP1,SPARCL1,STAT1,FBLN1,HSP90B1,IGHG3,TPT1\nactin binding\tCNN1,CNN1,CALD1,CAPZA1,GMFG,SMTN,MEFV,NPEPPS,FXYD5\nprotein homodimerization activity\tATPIF1,ATPIF1,HSP90AA1,U16,NPM1,EIF3S3,RPL4\nsignal transducer activity\tSTAT1,STAT1,RGS19,BIRC2,HNRPDL,EEF1D,COPS2\noxidoreductase activity\tNDUFA10,NDUFA10,GPX1,MDH2,GPX4,IGLC2,PRDX2\ntranscription coactivator activity\tTGFB1I1,TGFB1I1,TAF7,NPM1,LOC342346,SUB1\ntranslation initiation factor activity\tEIF4G1,EIF4G1,EIF4A2,EIF4G2,EIF5,EIF3S3\nprotein-tyrosine kinase activity\tRAVER2,RAVER2,PCM1,U16,LCK,HNRPDL,RPL4\ncatalytic activity\tKIAA1840,KIAA1840,LOC91316,DIP2A,PDE4C\nhydrogen-transporting ATPase activity, rota...\tATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3\nhydrogen-transporting ATP synthase activity...\tATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3\nhematopoietin/interferon-class (D200-domain...\tSTAT1,STAT1,IL2RG,IL10RA,IFNGR1,IL7R\noxidoreductase activity, acting on NADH or ...\tNDUFV1,NDUFV1,NDUFS7,NDUFS3,NDUFS8\nsingle-stranded DNA binding\tHNRPDL,HNRPDL,LOC342346,SUB1,RPA1\namino acid-polyamine transporter activity\tSLC38A2,SLC38A2,218041_x_at,RPL24\nreceptor binding\tCCL5,CCL5,ARPP-19,MSN,LTB,GNB2L1\ncalmodulin binding\tCALD1,CALD1,MARCKS,ATPIF1,IQGAP1\ncytochrome-c oxidase activity\tCOX4I1,COX4I1,COX5B,COX5A,COX8A\nubiquinol-cytochrome-c reductase activity\tUCRC,UCRC,UQCRQ,UQCRC1,UQCRFS1\nprotein transporter activity\tVDP,VDP,KPNB1,TLOC1,AP2S1,COPE\ntranslation elongation factor activity\tTUFM,TUFM,EEF1B2,EEF1D,EEF1A1\npeptide antigen binding\tTAPBP,TAPBP,TRB@,TRBV3-1,TRA@\nmicrotubule motor activity\t220725_x_at,220725_x_at,KNS2\nphosphotransferase activity, alcohol group ...\tNDUFA10,NDUFA10,208246_x_at\noxygen transporter activity\tSLC38A2,SLC38A2,218041_x_at\nRNA helicase activity\tDDX17,DDX17,DHX15,DDX5,DDX1\ngrowth factor activity\tCECR1,CECR1,GMFG,PGF,TGFB1\nRNA splicing factor activity, transesterifi...\tSFRS10,SFRS10,HIATL1,SF3B1\nheparin binding\tLOC283412,LOC283412,RPL29\ndouble-stranded DNA binding\tIFI16,IFI16,ZNF638,HNRPDL\nRNA polymerase II transcription mediator ac...\tCNOT2,CNOT2,THRAP5,THRAP1\nnitric-oxide synthase regulator activity\tHSP90AA1,HSP90AA1,EIF3S3\nisomerase activity\tP4HB,P4HB,ECH1,PPIB,KTN1\nTPR domain binding\tHSP90AA1,HSP90AA1,EIF3S3\nubiquitin-protein ligase activity\tUBE4A,UBE4A,TPT1,UBE2D2\nprotein domain specific binding\tYWHAZ,YWHAZ,217718_s_at\ncAMP-dependent protein kinase regulator act...\tPRKAR1A,PRKAR1A,PRKAR2A\ntranscriptional repressor activity\tIRF7,IRF7,IFI16,BCLAF1\nprotein kinase binding\tU16,U16,LCK,ITGB2,RPL4\ngeneral RNA polymerase II transcription fac...\tTCEA1,TCEA1,TAF7,GTF2I\nMHC protein binding\tTRB@,TRB@,TRBV3-1,TRA@\nATP-dependent RNA helicase activity\tG3BP,G3BP,RPL35A,DDX3X\nprotein heterodimerization activity\tCD3D,CD3D,PPP2CA,NPM1\npeptidase activity\tMASP1,MASP1,ZNF9,CLPP\nidentical protein binding\tAKT1,AKT1,CD74,SCAND1\nvitamin D receptor binding\tTHRAP5,THRAP5,THRAP1\nthyroid hormone receptor binding\tTHRAP5,THRAP5,THRAP1\nserine-type endopeptidase inhibitor activity\tNFE2L2,NFE2L2,COL6A3\nsequence-specific DNA binding\tNFE2L2,NFE2L2,LYSMD4\nprotein dimerization activity\tNFE2L2,NFE2L2,POLR2J\nmotor activity\tMYH9,MYH9,KNS2,MYH11\ncopper ion binding\tMT1E,MT1E,MT1F,DDX42\ncadmium ion binding\tMT1E,MT1E,MT1F,DDX42\ntransmembrane receptor activity\tCD3D,CD3D,CD79A,SPN\ntranslation factor activity, nucleic acid b...\tEIF3S3,EIF3S3,EEF1D\ntranscriptional activator activity\tTHRAP5,THRAP5,TGFB1\n3'-5'-exoribonuclease activity\tEXOSC4,EXOSC4,ISG20\ntropomyosin binding\tCALD1,CALD1,LMOD1\ntranscription factor binding\tYWHAZ,YWHAZ,HMGB1\nthreonine endopeptidase activity\tPSMB9,PSMB9,PSMB8\nserine-type endopeptidase activity\tMASP1,MASP1,IGHG3\nperoxidase activity\tCSDE1,CSDE1,PRDX2\nenzyme activator activity\tGMFG,GMFG,ALOX5AP\nantioxidant activity\tRPL27,RPL27,PRDX2\nactin filament binding\tMARCKS,MARCKS,DST\nacetylglucosaminyltransferase activity\tCOL6A3,COL6A3,OGT\nATPase binding\tATPIF1,ATPIF1,LCK\ntranscription regulator activity\tRPL7,RPL7,RPS27A\nsugar binding\tMASP1,MASP1,SELL\nligase activity\tNARS,NARS,UBE2D2\ncytokine binding\tCD74,CD74,IFNGR1\naminopeptidase activity\tCAST,CAST,NPEPPS\nprotein phosphatase inhibitor activity\tPSMB9,PSMB9,SET\nsuccinate dehydrogenase activity\tSDHA,SDHA,RPA1\nstructural constituent of muscle\tTPM1,TPM1,SMTN\nprotein disulfide oxidoreductase activity\tP4HB,P4HB,KTN1\nprotein disulfide isomerase activity\tP4HB,P4HB,KTN1\nornithine decarboxylase inhibitor activity\tOAZ2,OAZ2,OAZ1\nglutathione peroxidase activity\tGPX1,GPX1,GPX4\ndouble-stranded RNA binding\tSON,SON,MBNL1\nCD4 receptor binding\tLCK,LCK,SPG21\nMAP kinase kinase kinase activity\tU16,U16,RPL4\nprotein C-terminus binding\tDST,DST,LCK\n", - }, - { - kind: 'annotation', - name: 'Co-expression GO cellular component', - fileName: 'NORMA_Human_coexpression_Annotation_GO_CC.txt', - text: 'integral to membrane\tSPCS2,SPCS2,ITGAV,CAST,REEP5,HLA-DPB1,SSR1,C11orf2,TAPBP,IL2RG,CXCR4,HLA-J,RPLP2,CD37,SLC25A16,221651_x_at,RPN1,IGLV3-10,FNBP4,UCP2,AD7C-NTP,ADRM1,IGHD,HLA-B,HLA-DMB,TLOC1,ATP6V0B,HLA-DRB5,SLC25A11,CD74,HLA-DRB1,IGL@,TMED5,IGLC1,HLA-E,SLC25A3,IGLV3-25,TRB@,ARL6IP5,HLA-DRB4,TRBC1,LAPTM5,CD2,C6orf12,IFNGR1,HLA-DQB1,FXYD5,POPDC3,ATP6V0C,TRBV3-1,IGLC2,214836_x_at,IGHV1-69,IGHA1,U16,HLA-F,HLA-A,CD7,CAV1,IGHG3,215176_x_at,IGLJ3,HLA-G,IL7R,ITGB2,IL6ST,RPL4,HLA-C,IGKC,IGHM,TRAM1,RPA1\nmembrane\tHLA-DPB1,HLA-DPB1,SLC38A2,RAP1B,VDP,IL2RG,CXCR4,RAB1A,NPTN,RAB6A,HLA-DMA,HLA-DQA1,ADD3,SLC25A16,PGF,IL10RA,RAB6C,221651_x_at,MSN,IGLV3-10,RPL31,UCP2,IGHD,218041_x_at,HLA-DRA,HLA-DMB,YME1L1,SELL,ATP6V0B,HLA-DRB5,SLC25A11,HLA-DRB1,IGL@,IGLC1,SLC25A3,IGLV3-25,TRB@,HLA-DRB4,TRBC1,LTB,COL6A3,IFNGR1,HLA-DQB1,HLA-DPA1,CD79A,FXYD5,ATP6V0C,PPP2CA,TRBV3-1,IGLC2,ATP2A3,214836_x_at,IGHV1-69,IGHA1,IGHG3,215176_x_at,IGLJ3,IL7R,COPE,HLA-C,IGKC,IGHM,ATP5G3,RPA1,RPL24\nnucleus\tTARDBP,TARDBP,CNOT2,NFE2L2,ZFR,STAT1,SEPT2,MCM3AP,TINP1,LYSMD4,KPNB1,SFRS10,CBX3,THRAP5,G3BP,HNRPH3,IRF7,THRAP1,ZMYND11,POLR2J,ZNF160,MEFV,CDC5L,TRIM22,DDX17,IFI16,HCLS1,ATRX,HNRPH1,POLR1B,OGT,DHX15,ZNF611,MORF4L1,HNRPR,HMGB1,PPP2CA,CDKN1B,PCM1,BCLAF1,TIMM13,SRP9,RPS6,DDX5,ZNF638,RPL35A,SFRS11,PCNP,DDX50,ZNF552,RPS3A,NPM1,MBNL1,PAPOLA,AURKAIP1,FIBP,SH3BGRL,MATR3,RPS27A,SCAND1,DDX3X,SET,HNRPA2B1,RNPC2,SEPT7,H3F3A\ncytoplasm\tAKT1,AKT1,MT1E,FNTA,STAT1,MT1F,SEPT2,VDP,EIF4G1,KPNB1,DDX42,IRF7,MSN,ARL8B,CD3D,PRKAR2A,HCLS1,ARHGDIB,COL6A3,COL1A1,COL6A1,DST,217718_s_at,RPL36A,NARS,COL4A2,COL4A1,CDKN1B,NDUFA13,PCM1,ZNF638,NACA,EXOSC4,COL3A1,COL1A2,ARPC5,EIF5,NPM1,MBNL1,PAPOLA,SH3BGRL,TPT1,PRDX2,COPS2,COPE,XPO1,DDX3X,EEF1A1,HNRPA1\nribosome\tRPL37,RPL37,RPS11,RPS21,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,LOC440055,LOC388344,RPL27A,RPL28,LOC283412,RPL19,RPS16,RPL36A,RPS6,RPS2,U16,RPL18A,RPL13,RPL29,RPS3A,RPS18,RPL14,RPL22,RPL34,RPS23,RPL15,RPL9,RPL6,RPL4,RPS4X,RPS24,RPL5,RPL23,MDS1,RPS7\nintracellular\tRPL37,RPL37,RAP1B,MGC2474,RPS21,RPL21,RPLP2,G3BP,GMFG,RPS19,RAB6C,PRKD2,RPL31,ZNF160,TGFB1I1,MEFV,GLTSCR2,MRPS12,CD74,RPL28,LOC283412,RPL19,ROCK1,SON,RPS6,RPS2,U16,RPL18A,RPL29,RPS18,RPL14,RPL22,RPL34,RPL15,RPL4,RPS4X,RPS24,RPL13A,RPL5,MDS1,RPS7\nintegral to plasma membrane\tHNRPM,HNRPM,IL2RG,HLA-DQA1,CD48,CD37,CLPTM1,ADRM1,HLA-B,CD164,HLA-DRA,SELL,HLA-DRB5,SLC25A11,HLA-DRB1,SLC25A3,HLA-DRB4,LAPTM5,CD2,C6orf12,IFNGR1,HLA-DPA1,ATP2A3,IGHA1,HLA-A,CAV1,SPN,IGHG3,CD52,IL6ST,HLA-C,IGHM,ATP8B1\ncytosolic small ribosomal subunit (sensu Eu...\tRPS15,RPS15,RPS5,FAU,RPS11,RPS21,RPS13,RPS19,LOC440055,RPS27,RPS10,RPS17,RPS16,RPS14,RPS6,RPS2,RPS15A,RPS20,RPS3A,RPS18,RPS27A,RPS23,RPS24,RPS9,RPS7,LOC402057,RPS25\ncytosolic large ribosomal subunit (sensu Eu...\tRPL35,RPL35,RPL18,RPL21,RPL36,RPLP2,RPL27A,RPL28,LOC283412,RPL7,U16,RPL18A,RPL29,RPL14,RPL22,RPL34,RPL6,RPL4,RPL5,MDS1\nplasma membrane\tARF1,ARF1,CD48,PTPRCAP,CD37,IL10RA,MSN,CD164,HLA-DRA,SELL,PRKAR2A,CD53,TRB@,TRBV3-1,TRA@,LCK,CD7,IL6ST\nmitochondrion\tACO2,ACO2,ATPIF1,MSRB2,SDHA,UCP2,TUFM,LOC440055,GPX4,SLC25A3,PPP2CA,NDUFA13,FIBP,DBT,NDUFB4,CYC1,RPA1\ncytosol\tCAST,CAST,NCF1,NPEPPS,KNS2,PSMB9,PSMB8,HSP90AA1,OGT,PPP2CA,HSP90B1,EIF5,RPS3A,EIF3S3,SPG21,COPB\nmembrane fraction\tRGS19,RGS19,ADRM1,HLA-B,PRKAR2A,LMOD1,COL6A3,IGHV1-69,IGHA1,U16,CD7,IGHG3,CD52,FIBP,RPL4,IGHM\nendoplasmic reticulum\tCAST,CAST,P4HB,SSR1,TAPBP,RPN1,PPIB,TLOC1,NAPA,HSP90B1,CAV1,KTN1,SET,TRAM1\ncytoskeleton\tCALD1,CALD1,RAVER2,ACTA2,TPM1,MSN,NPEPPS,ARHGDIB,LMOD1,ACTG2,ARPC3,ARPC5\nribonucleoprotein complex\tHNRPM,HNRPM,SRP72,HNRPH3,HNRPU,HNRPK,HNRPR,HNRPA3P1,SIAHBP1,HNRPA1\nextracellular region\tCAST,CAST,P4HB,CNOT2,MASP1,IGL@,FBLN1,IFI30,COL4A1,MIF,IGHG3,IGKC\nMHC class I protein complex\tHLA-J,HLA-J,RPLP2,HLA-B,HLA-E,C6orf12,HLA-F,HLA-A,HLA-G,HLA-C\nheterogeneous nuclear ribonucleoprotein com...\tHNRPH3,HNRPH3,HNRPU,HNRPH1,HNRPR,HNRPDL,HNRPA2B1,HNRPA1\nmitochondrial inner membrane\tNDUFA7,NDUFA7,SLC25A16,UCP2,SLC25A11,SLC25A3,NDUFV1\nextracellular space\tCNOT2,CNOT2,IL16,AD7C-NTP,FBLN1,TGFB1,MMP2,SPN,TPT1\nnucleoplasm\tIFI16,IFI16,NDUFA13,ZNF638,XPO1,ISG20,RNPC2,HNRPA1\ncollagen\tCOL1A1,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2\nnucleolus\tRPL35,RPL35,RPS19,IFI16,NCL,EXOSC4,NPM1,RPS7\nspliceosome complex\tHNRPM,HNRPM,HIATL1,SF3B1,HNRPA2B1,HNRPA1\nproton-transporting two-sector ATPase complex\tATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3\nextracellular matrix (sensu Metazoa)\tLAMC1,LAMC1,FBLN1,DGCR6,COL6A3,TGFB1\nperinuclear region\t217718_s_at,217718_s_at,HSP90B1,SET\nDNA-directed RNA polymerase II, core complex\tPOLR2J,POLR2J,HCLS1,HMHA1,POLR2E\nactin cytoskeleton\tMARCKS,MARCKS,FNBP4,CORO1A,SMTN\ncAMP-dependent protein kinase complex\tPRKAR1A,PRKAR1A,PRKAR2A,PRKACB\nArp2/3 protein complex\tACTR3,ACTR3,ARPC1B,ARPC3,ARPC5\nlysosome\tHLA-DRA,HLA-DRA,LAPTM5,IFI30\nlarge ribosomal subunit\tRPL7,RPL7,RPL26,RPL13A,RPL17\nGolgi apparatus\tRGS19,RGS19,RAB6A,RAB6C,NAPA\nsoluble fraction\tNCF1,NCF1,FBLN1,NARS,PPP2CA\neukaryotic translation initiation factor 4F...\tEIF4G1,EIF4G1,EIF4A2,EIF4G2\nproton-transporting ATP synthase complex (s...\tATP5J2,ATP5J2,ATP5D,ATP5G3\nmicrosome\tSPCS2,SPCS2,TAPBP,HSP90B1\ncytosolic ribosome (sensu Eukaryota)\tLOC388344,LOC388344,RPL13\nGolgi membrane\tTAPBP,TAPBP,VDP,CAV1,COPB\ntranscription factor complex\tLOC342346,LOC342346,SUB1\nendoplasmic reticulum membrane\tTAPBP,TAPBP,HSP90B1,KTN1\nactin filament\tACTA2,ACTA2,ACTG2,IQGAP1\nproteasome complex (sensu Eukaryota)\tPSMB3,PSMB3,PSMB9,PSMB8\nnuclear envelope\tPAFAH1B1,PAFAH1B1,XPO1\nendoplasmic reticulum lumen\tCAST,CAST,PPIB,HSP90B1\nstress fiber\tPLEKHC1,PLEKHC1,SEPT7\nsmall ribosomal subunit\tRPS5,RPS5,MRPS12,RPS2\nmitochondrial electron transport chain\tUQCRC1,UQCRC1,NDUFA13\nmitochondrial ribosome\tMRPS12,MRPS12,MRPL34\nmitochondrial inner membrane presequence tr...\tTIMM8B,TIMM8B,TIMM13\nmediator complex\tTHRAP5,THRAP5,THRAP1\ncollagen type VI\tCOL6A3,COL6A3,COL6A1\nER-Golgi intermediate compartment\tP4HB,P4HB,TMED5,KTN1\nrespiratory chain complex III (sensu Eukary...\tUQCRQ,UQCRQ,UQCRFS1\neukaryotic translation elongation factor 1 ...\tEEF1B2,EEF1B2,EEF1D\ncytoplasmic membrane-bound vesicle\tARHGDIB,ARHGDIB,DST\nmicrotubule associated complex\tMEFV,MEFV,PAFAH1B1\nubiquitin ligase complex\tRNF11,RNF11,UBE4A\nprotein complex\tPSMB9,PSMB9,PSMB8\nproteasome core complex (sensu Eukaryota)\tPSMB9,PSMB9,PSMB8\nnuclear pore\tKPNB1,KPNB1,RGPD5\nintegrin complex\tITGAV,ITGAV,ITGB2\nsignal recognition particle (sensu Eukaryota)\tSRP72,SRP72,SRP9\ncell surface\tP4HB,P4HB,ATPIF1\nmyosin\tMYH9,MYH9,MYH11\nmitochondrial envelope\tUCRC,UCRC,COX5B\ncentrosome\tNPM1,NPM1,CEP27\nbasement membrane\tLAMC1,LAMC1,DST\nmitochondrial matrix\tMDH2,MDH2,ETFB\nCOPI vesicle coat\tCOPE,COPE,COPB\npericentriolar material\tPCM1,PCM1,LCK\nintermediate filament\tDES,DES,RPL38\nlipid raft\tLCK,LCK,CAV1\n', - }, - { - kind: 'colors', - name: 'Co-expression MCODE clusters', - fileName: 'NORMA_Human_coexpression_Expression_MCODE.txt', - text: '207730_x_at\tgreen\n214836_x_at\tgreen\n215176_x_at\tgreen\n215182_x_at\tgreen\n216342_x_at\tgreen\n216524_x_at\tgreen\n217281_x_at\tgreen\n220725_x_at\tgreen\n221651_x_at\tgreen\nACTA2\tgreen\nACTR2\tgreen\nACTR3\tgreen\nAD7C-NTP\tgreen\nAP2S1\tgreen\nARF5\tgreen\nARHGDIB\tgreen\nARL6IP2\tgreen\nARPC3\tgreen\nATP8B1\tgreen\nATRX\tgreen\nAURKAIP1\tgreen\nBIRC2\tgreen\nC6orf12\tgreen\nCAV1\tgreen\nCD52\tgreen\nCD74\tgreen\nCDC5L\tgreen\nCEP27\tgreen\nCHCHD2\tgreen\nCOL1A1\tgreen\nCOL1A2\tgreen\nCOL3A1\tgreen\nCOL6A1\tgreen\nCOPE\tgreen\nCOX5A\tgreen\nCOX5B\tgreen\nCOX8A\tgreen\nCROP\tgreen\nCTA-246H3.1\tgreen\nCYBA\tgreen\nDBT\tgreen\nDDT\tgreen\nDDX3X\tgreen\nDDX5\tgreen\nEIF3S3\tgreen\nEIF3S6\tgreen\nEIF4G2\tgreen\nFAU\tgreen\nFBLN1\tgreen\nFBXW12\tgreen\nFLJ11021\tgreen\nFLJ12151\tgreen\nFLJ20294\tgreen\nFLJ42393\tgreen\nGNB2L1\tgreen\nH3F3A\tgreen\nHIATL1\tgreen\nHLA-A\tgreen\nHLA-B\tgreen\nHLA-C\tgreen\nHLA-DMA\tgreen\nHLA-DPA1\tgreen\nHLA-DQB1\tgreen\nHLA-DRB1\tgreen\nHLA-DRB4\tgreen\nHLA-DRB5\tgreen\nHLA-E\tgreen\nHLA-F\tgreen\nHLA-G\tgreen\nHLA-J\tgreen\nHNRPDL\tgreen\nHNRPH3\tgreen\nIFI30\tgreen\nIGHA1\tgreen\nIGHG3\tgreen\nIGHM\tgreen\nIGHV1-69\tgreen\nIGKC\tgreen\nIGKV1-5\tgreen\nIGKV1D-13\tgreen\nIGKV1OR15-118\tgreen\nIGKV1OR2-108\tgreen\nIGL@\tgreen\nIGLC1\tgreen\nIGLC2\tgreen\nIGLJ3\tgreen\nIGLV3-25\tgreen\nIQGAP1\tgreen\nKNS2\tgreen\nLAPTM5\tgreen\nLCK\tgreen\nLMOD1\tgreen\nLOC152719\tgreen\nLOC388344\tgreen\nLOC440055\tgreen\nLOC91316\tgreen\nMAN2B1\tgreen\nMATR3\tgreen\nMBNL1\tgreen\nMCM3AP\tgreen\nMDS1\tgreen\nMGEA5\tgreen\nMORF4L1\tgreen\nMRPL34\tgreen\nMT1E\tgreen\nMT1F\tgreen\nMT1G\tgreen\nMT1H\tgreen\nMT1L\tgreen\nMT1M\tgreen\nMT1X\tgreen\nMT2A\tgreen\nMYH11\tgreen\nNACA\tgreen\nNAP1L1\tgreen\nNARS\tgreen\nNDUFA2\tgreen\nNDUFA3\tgreen\nNDUFAB1\tgreen\nNDUFB11\tgreen\nNDUFB2\tgreen\nNDUFB4\tgreen\nNDUFS3\tgreen\nNDUFS7\tgreen\nNPM1\tgreen\nPAPOLA\tgreen\nPCNP\tgreen\nPDE4C\tgreen\nPFAAP5\tgreen\nPGF\tgreen\nPNRC2\tgreen\nPOLR2J\tgreen\nPRG1\tgreen\nPRR11\tgreen\nPTGES3\tgreen\nRAC2\tgreen\nRIOK3\tgreen\nROCK1\tgreen\nRPL11\tgreen\nRPL13\tgreen\nRPL13A\tgreen\nRPL17\tgreen\nRPL18\tgreen\nRPL21\tgreen\nRPL22\tgreen\nRPL23\tgreen\nRPL24\tgreen\nRPL27\tgreen\nRPL27A\tgreen\nRPL28\tgreen\nRPL29\tgreen\nRPL30\tgreen\nRPL34\tgreen\nRPL35A\tgreen\nRPL36\tgreen\nRPL36A\tgreen\nRPL38\tgreen\nRPL4\tgreen\nRPL5\tgreen\nRPL6\tgreen\nRPL7\tgreen\nRPL9\tgreen\nRPLP2\tgreen\nRPS14\tgreen\nRPS17\tgreen\nRPS18\tgreen\nRPS19\tgreen\nRPS2\tgreen\nRPS23\tgreen\nRPS24\tgreen\nRPS25\tgreen\nRPS27A\tgreen\nRPS3A\tgreen\nRPS4X\tgreen\nRPS5\tgreen\nRPS6\tgreen\nRPS7\tgreen\nRPS9\tgreen\nSEPT2\tgreen\nSERBP1\tgreen\nSFRS11\tgreen\nSH3BGRL\tgreen\nSKP1A\tgreen\nSLC35E1\tgreen\nSON\tgreen\nSUMO2\tgreen\nTAF7\tgreen\nTLOC1\tgreen\nTRA@\tgreen\nTRB@\tgreen\nTUG1\tgreen\nU16\tgreen\nUQCR\tgreen\nUQCRC1\tgreen\nUQCRFS1\tgreen\nUSP34\tgreen\nXPO1\tgreen\nYTHDC1\tgreen\nZNF160\tgreen\nZNF611\tgreen\nZNF638\tgreen\nZNHIT1\tgreen\n208246_x_at\tyellow\n211637_x_at\tyellow\n211645_x_at\tyellow\nATP5G3\tyellow\nATP6V0B\tyellow\nATP6V0D1\tyellow\nBCLAF1\tyellow\nCAPZA1\tyellow\nCOL6A3\tyellow\nCXCR4\tyellow\nHLA-DPB1\tyellow\nHNRPH1\tyellow\nIFI16\tyellow\nLOC342346\tyellow\nLOC56902\tyellow\nLOC58486\tyellow\nLOC645745\tyellow\nMEFV\tyellow\nMRPS12\tyellow\nNDUFS8\tyellow\nPTRF\tyellow\nRPL31\tyellow\nRPL35\tyellow\nRPS11\tyellow\nTAGLN\tyellow\nTGFB1\tyellow\nTRBC1\tyellow\n208120_x_at\tred\n208238_x_at\tred\n211639_x_at\tred\n211641_x_at\tred\n211650_x_at\tred\n211908_x_at\tred\n212498_at\tred\n216412_x_at\tred\n216858_x_at\tred\n217052_x_at\tred\n217258_x_at\tred\n217679_x_at\tred\n217718_s_at\tred\n218041_x_at\tred\nAASDHPPT\tred\nACO2\tred\nACTG2\tred\nACTR10\tred\nADD3\tred\nADRM1\tred\nAKR7A2\tred\nAKT1\tred\nALOX5AP\tred\nARF1\tred\nARL6IP5\tred\nARL8B\tred\nARPC1B\tred\nARPC5\tred\nARPP-19\tred\nATP2A3\tred\nATP5D\tred\nATP5J2\tred\nATP6AP2\tred\nATP6V0C\tred\nATPIF1\tred\nBTF3\tred\nC11orf2\tred\nC11orf58\tred\nC12orf10\tred\nC12orf38\tred\nC16orf24\tred\nC17orf62\tred\nC1orf160\tred\nC1orf63\tred\nC21orf33\tred\nC3orf60\tred\nCAB39\tred\nCALD1\tred\nCAP1\tred\nCAPNS1\tred\nCAST\tred\nCBX3\tred\nCCL5\tred\nCCNL1\tred\nCD164\tred\nCD2\tred\nCD37\tred\nCD3D\tred\nCD48\tred\nCD53\tred\nCD7\tred\nCD79A\tred\nCDKN1B\tred\nCECR1\tred\nCHMP2A\tred\nCLPP\tred\nCLPTM1\tred\nCLTC\tred\nCNN1\tred\nCNOT2\tred\nCOL4A1\tred\nCOL4A2\tred\nCOPB\tred\nCOPS2\tred\nCOQ9\tred\nCORO1A\tred\nCOX4I1\tred\nCSDE1\tred\nCSNK1A1\tred\nCUL4B\tred\nCXorf9\tred\nCYC1\tred\nDDX1\tred\nDDX17\tred\nDDX42\tred\nDDX50\tred\nDES\tred\nDEXI\tred\nDGCR6\tred\nDHX15\tred\nDIP2A\tred\nDKFZP566N034\tred\nDST\tred\nDYNLT3\tred\nECH1\tred\nEEF1A1\tred\nEEF1B2\tred\nEEF1D\tred\nEIF4A2\tred\nEIF4G1\tred\nEIF5\tred\nENDOG\tred\nERH\tred\nETFB\tred\nEXOSC4\tred\nFAIM3\tred\nFAM96B\tred\nFIBP\tred\nFIS1\tred\nFLJ10154\tred\nFLJ14346\tred\nFNBP4\tred\nFNTA\tred\nFXYD5\tred\nG3BP\tred\nGBL\tred\nGLTSCR2\tred\nGMFG\tred\nGOLGA8A\tred\nGOLGA8B\tred\nGPAA1\tred\nGPSM3\tred\nGPX1\tred\nGPX4\tred\nGTF2I\tred\nGUK1\tred\nHCLS1\tred\nHLA-DMB\tred\nHLA-DQA1\tred\nHLA-DRA\tred\nHMGB1\tred\nHMHA1\tred\nHNRPA1\tred\nHNRPA2B1\tred\nHNRPA3P1\tred\nHNRPK\tred\nHNRPM\tred\nHNRPR\tred\nHNRPU\tred\nHSP90AA1\tred\nHSP90B1\tred\nIFNGR1\tred\nIGHD\tred\nIGLV3-10\tred\nIL10RA\tred\nIL16\tred\nIL2RG\tred\nIL6ST\tred\nIL7R\tred\nINPP5D\tred\nIRF7\tred\nISG20\tred\nITGAV\tred\nITGB2\tred\nKIAA0907\tred\nKIAA1840\tred\nKIDINS220\tred\nKPNB1\tred\nKTN1\tred\nLAMC1\tred\nLOC283412\tred\nLOC402057\tred\nLRRFIP1\tred\nLTB\tred\nLYSMD4\tred\nMARCKS\tred\nMASP1\tred\nMDH2\tred\nMGC2474\tred\nMIF\tred\nMMP2\tred\nMRP63\tred\nMSN\tred\nMSRB2\tred\nMYH9\tred\nMYLK\tred\nNAPA\tred\nNBPF1\tred\nNBPF12\tred\nNCF1\tred\nNCL\tred\nNDUFA10\tred\nNDUFA13\tred\nNDUFA7\tred\nNDUFB6\tred\nNDUFC1\tred\nNDUFV1\tred\nNFE2L2\tred\nNPEPPS\tred\nNPTN\tred\nOAZ1\tred\nOAZ2\tred\nOGT\tred\nOPHN1\tred\nORC6L\tred\nOSBPL8\tred\nP4HB\tred\nPAFAH1B1\tred\nPCM1\tred\nPDCD10\tred\nPGLS\tred\nPHIP\tred\nPLEKHC1\tred\nPOLR1B\tred\nPOLR2E\tred\nPOLR2I\tred\nPOLR2L\tred\nPOPDC3\tred\nPPIB\tred\nPPP1CA\tred\nPPP2CA\tred\nPRDX2\tred\nPRKACB\tred\nPRKAR1A\tred\nPRKAR2A\tred\nPRKD2\tred\nPSMB3\tred\nPSMB8\tred\nPSMB9\tred\nPTPRCAP\tred\nPUM2\tred\nR3HCC1\tred\nRAB1A\tred\nRAB6A\tred\nRAB6C\tred\nRAP1B\tred\nRAVER2\tred\nRBPMS\tred\nREEP5\tred\nRGPD5\tred\nRGS19\tred\nRNF11\tred\nRNPC2\tred\nRPA1\tred\nRPL10A\tred\nRPL12\tred\nRPL14\tred\nRPL15\tred\nRPL18A\tred\nRPL19\tred\nRPL23A\tred\nRPL26\tred\nRPL32\tred\nRPL37\tred\nRPL39\tred\nRPL41\tred\nRPN1\tred\nRPS10\tred\nRPS13\tred\nRPS15\tred\nRPS15A\tred\nRPS16\tred\nRPS20\tred\nRPS21\tred\nRPS27\tred\nSCAND1\tred\nSDHA\tred\nSEC61A1\tred\nSELL\tred\nSEPT7\tred\nSET\tred\nSF3B1\tred\nSFPQ\tred\nSFRS10\tred\nSFRS3\tred\nSFRS5\tred\nSIAHBP1\tred\nSLC25A11\tred\nSLC25A16\tred\nSLC25A3\tred\nSLC38A2\tred\nSMTN\tred\nSPARCL1\tred\nSPCS2\tred\nSPG21\tred\nSPN\tred\nSRP46\tred\nSRP72\tred\nSRP9\tred\nSSR1\tred\nSTAT1\tred\nSUB1\tred\nSUMO1\tred\nTAPBP\tred\nTARDBP\tred\nTCEA1\tred\nTCEB2\tred\nTGFB1I1\tred\nTHRAP1\tred\nTHRAP5\tred\nTIMM13\tred\nTIMM8B\tred\nTINP1\tred\nTMED5\tred\nTMEM123\tred\nTMEM66\tred\nTMEM93\tred\nTOMM20\tred\nTPM1\tred\nTPT1\tred\nTRAM1\tred\nTRBV3-1\tred\nTRIM22\tred\nTTC19\tred\nTTC3\tred\nTUFM\tred\nTXNL2\tred\nUBE2D2\tred\nUBE4A\tred\nUCP2\tred\nUCRC\tred\nUQCRQ\tred\nUSP47\tred\nVDP\tred\nWAC\tred\nWDR45\tred\nWIPI2\tred\nWSB1\tred\nYIPF3\tred\nYME1L1\tred\nYWHAZ\tred\nZC3H11A\tred\nZFR\tred\nZMYND11\tred\nZNF403\tred\nZNF552\tred\nZNF9\tred\nhfl-B5\tred\n', - }, - ], - }, - covid: { - title: 'COVID-19 (IntAct)', - source: 'IntAct database', - link: null, - files: [ - { - kind: 'network', - name: 'COVID-19 IntAct', - fileName: 'Intact-data_COVID19_no_self_loops.txt', - text: 'Source\tTarget\nP31809\tP11224\nP33767\tP41811\nQ3T133\tP41811\nP39656\tP41811\nP41811\tO15503\nQ91AV1\tP41811\nQ96WV5\tP68978\nQ9Y5U4\tP41811\nP53622\tP41811\nP53622\tQ3T133\nP53622\tP41811\nP53622\tP33767\nP53622\tP41811\nP53622\tP68978\nP53622\tP41811\nP53622\tP39656\nQ3T133\tQ96WV5\nQ96WV5\tP39656\nQ96WV5\tP33767\nQ96WV5\tP68978\nP53622\tP41811\nP53622\tQ9Y5U4\nP53622\tP41811\nP53622\tQ91AV1\nP53622\tP41811\nP53622\tO15503\nQ96WV5\tQ9Y5U4\nQ96WV5\tO15503\nQ91AV1\tQ96WV5\nP53622\tP41811\nP53622\tP41811\nP53622\tP41811\nP53622\tP41811\nP53622\tP41811\nP27487\tK0BRG7\nP27487\tK0BRG7\nQ9BYF1-1\tP59594\nP27487\tK0BRG7\nQ9BYF1-1\tP59594\nP27487\tK0BRG7\nP27487\tK0BRG7\nP0C6U8\tP62753\nQ9BYF1-1\tQ6Q1S2\nQ9BYF1-1\tQ6Q1S2\nQ9BYF1-1\tQ6Q1S2\nQ7TLC7\tO14964\nQ7TLC7\tP05155\nQ7TLC7\tQ13561\nQ7TLC7\tQ9NRH1\nJ9TC74\tQ13561\nJ9TC74\tP62258\nJ9TC74\tQ99471\nP59633\tQ13561\nP59633\tP05155\nP59633\tP02768\nQ6S8E0\tP08708\nQ6S8E0\tP25787\nP59634\tQ13561\nP59634\tQ92994\nQ19QW4\tQ9BQB6\nQ19QW4\tQ9H4F8\nQ7T6S2\tP49069\nQ7T6S2\tP05155\nQ6S8E0\tP60866\nQ6S8E0\tP27448\nQ19QW2\tP46379\nQ6S8E0\tP60866\nQ19QW2\tP46379\nP59634\tQ13561\nQ7T6S2\tP49069\nQ19QW4\tQ9BQB6\nQ6S8E0\tP27448\nQ6S8E0\tP25787\nQ7TLC7\tO14964\nQ7TLC7\tQ13561\nQ1HVL3\tP15144\nQ1HVL8\tP15144\nQ1HVK9\tP15144\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nK9N5Q8\tP27487\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nP61769\tP04439\nP61769\tP59595\nP61769\tP04439\nP61769\tP03407\nP61769\tP04439\nP61769\tP59595\nP61769\tP04439\nP61769\tP59596\nP61769\tP04439\nP61769\tQ692E0\nP61769\tP04439\nP61769\tP59596\nP61769\tP04439\nP61769\tQ76R37\nP61769\tP04439\nP61769\tP59637\nQ9BYF1\tP0DTC2\nQ9BYF1\tP0DTC2\nQ9BYF1\tP0DTC2\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP0DTC2\nQ9BYF1\tQ695T7\nQ9BYF1\tQ695T7\nQ9BYF1\tP0DTC2\nQ9BYF1\tQ695T7\nQ9BYF1\tP37173\nQ9BYF1\tP37173\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nP11021\tK0BRG7\nK0BRG7\tP11021\nP0DTC4\tQ8IWA5\nP0DTC4\tQ86VM9\nP0DTC4\tQ6UX04\nP0DTC4\tP25440\nP0DTC4\tO60885\nP0DTC4\tO00203\nP0DTC9\tQ9Y3U8\nP0DTC9\tQ9NW13\nP0DTC9\tQ9NR30\nP0DTC9\tQ9UN86\nP0DTC9\tQ9HCE1\nP0DTC9\tQ92900\nP0DTC9\tQ8TAD8\nP0DTC9\tQ8NCA5\nP0DTC9\tQ6PKG0\nP0DTC9\tQ13310\nP0DTC9\tQ13283\nP0DTC9\tP67870\nP0DTC9\tP19784\nP0DTC9\tP11940\nP0DTC9\tO43818\nP0DTC2\tQ9C0B5\nP0DTC2\tQ7Z5G4\nP0DTC5\tQ9Y6E2\nP0DTC5\tQ9Y312\nP0DTC5\tQ9ULX6\nP0DTC5\tQ9UDR5\nP0DTC5\tQ9UBU6\nP0DTC5\tQ9NQC3\nP0DTC5\tQ9BW92\nP0DTC5\tQ9BSJ2\nP0DTC5\tQ9BQT8\nP0DTC5\tQ96HW7\nP0DTC5\tQ96HR9\nP0DTC5\tQ96ER3\nP0DTC5\tQ96D53\nP0DTC5\tQ96CW5\nP0DTC5\tQ8NEW0\nP0DTC5\tQ7L8L6\nP0DTC5\tQ6PML9\nP0DTC5\tQ5JRX3\nP0DTC5\tQ4KMQ2\nP0DTC5\tQ10713\nP0DTC5\tQ00765\nP0DTC5\tP48556\nP0DTC5\tP38606\nP0DTC5\tP38435\nP0DTC5\tP27105\nP0DTC5\tP13804\nP0DTC5\tP11310\nP0DTC5\tP05026\nP0DTC5\tO95070\nP0DTC5\tO75439\nP0DTC6\tQ9NZJ7\nP0DTC6\tP78406\nP0DTC6\tP52948\nP0DTC3\tQ9Y673\nP0DTC3\tQ9UH99\nP0DTC3\tQ9H270\nP0DTC3\tQ96S66\nP0DTC3\tQ96JC1\nP0DTC3\tQ8N6S5\nP0DTC3\tQ8IWR1\nP0DTC3\tP09601\nP0DTC7\tQ9NU22\nP0DTC7\tQ7Z4Q2\nP0DTC8\tQ96IV0\nP0DTC8\tQ8N0Z8\nP0DTC8\tQ96AY3\nP0DTC8\tQ13443\nP0DTC8\tQ9Y680\nP0DTC8\tQ9Y4L1\nP0DTC8\tQ9UHI8\nP0DTC8\tQ9P2E5\nP0DTC8\tQ9NYU1\nP0DTC8\tQ9NXK8\nP0DTC8\tQ9H8W4\nP0DTC8\tQ9H4F8\nP0DTC8\tQ9H488\nP0DTC8\tQ9H173\nP0DTC8\tQ9BZQ6\nP0DTC8\tQ9BS26\nP0DTC8\tQ9BRN9\nP0DTC8\tQ99988\nP0DTC8\tQ99519\nP0DTC8\tQ99470\nP0DTC8\tQ96MM7\nP0DTC8\tQ96F46\nP0DTC8\tQ96DZ1\nP0DTC8\tQ92820\nP0DTC8\tQ8N766\nP0DTC8\tQ8IZ52\nP0DTC8\tQ8IWF2\nP0DTC8\tQ8IV08\nP0DTC8\tQ86YB8\nP0DTC8\tQ7Z4H8\nP0DTC8\tQ6UW63\nP0DTC8\tQ15818\nP0DTC8\tQ13438\nP0DTC8\tQ08431\nP0DTC8\tP61916\nP0DTC8\tP58166\nP0DTC8\tP29122\nP0DTC8\tP28300\nP0DTC8\tP26358\nP0DTC8\tP15151\nP0DTC8\tP12109\nP0DTC8\tP0C7P0\nP0DTC8\tP05556\nP0DTC8\tP00750\nP0DTC8\tO76061\nP0DTC8\tO14656\nP0DTC8\tO00469\nP0DTD2\tQ9UL15\nP0DTD2\tQ9UKA9\nP0DTD2\tQ9P0L2\nP0DTD2\tQ9H773\nP0DTD2\tQ9H2P9\nP0DTD2\tQ7KZI7\nP0DTD2\tP27448\nP0DTD2\tO94826\nP0DTD2\tO75534\nP0DTD2\tO43633\nP0DTD2\tO14745\nP0DTD3\tQ9Y3A6\nP0DTD3\tQ2PZI1\nP0DTD3\tQ96S52\nP0DTD3\tQ9Y6M9\nP0DTD3\tQ9Y375\nP0DTD3\tQ9Y276\nP0DTD3\tQ9NV92\nP0DTD3\tQ9H845\nP0DTD3\tQ9H3K2\nP0DTD3\tQ9GZU3\nP0DTD3\tQ9BVK2\nP0DTD3\tQ9BQ95\nP0DTD3\tQ96K12\nP0DTD3\tQ8TEQ8\nP0DTD3\tQ86VR2\nP0DTD3\tQ86UT6\nP0DTD3\tQ7Z2K6\nP0DTD3\tQ6NXT6\nP0DTD3\tQ6NXT4\nP0DTD3\tQ5BJF2\nP0DTD3\tQ12770\nP0DTD3\tP55085\nP0DTD3\tP33527\nP0DTD3\tO76024\nP0DTD3\tO43292\nP0DTD3\tO00124\nQ1LZX8\tP59594\nQ1LZX8\tP84198\nQ1LZX8\tP84198\nQ1LZX8\tP59594\nP84198\tP59594\nP84198\tP59594\nP59637\tQ64373\nP59637\tO35843\nQ64373\tQ07817\nQ64373\tP59637\nP59637\tO35843\nP59637\tQ07817\nP06731-1\tK0BRG7\nP06731\tK0BRG7\nK0BRG7\tP06731\nP06731\tK0BRG7\nP06731\tK0BRG7\nP59637\tP59636\nP59633\tP59637\nQ7TFA0\tP59636\nQ7TFA1\tQ19QW5\nQ80H93\tQ7TFA0\nP59596\tP59632\nP59594\tP59635\nQ7TFA1\tP59637\nP59594\tQ80H93\nQ7TFA1\tQ80H93\nQ7TFA0\tQ80H93\nQ7TLC7\tP59636\nQ7TFA1\tP59636\nQ80H93\tP59636\nQ7TLC7\tQ80H93\nQ7TFA1\tP59637\nP59595\tP59596\nQ7TFA1\tQ19QW5\nQ7TLC7\tP59636\nQ7TLC7\tP59636\nQ9BYF1\tP02768\nF1RG45\tK7GLM4\nQ9BYF1\tP05814\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ92793\tQ14653\nQ92793\tQ14653\nQ07817\tP59635\nP59635\tP10415\nQ07820\tP59635\nQ16548\tP59635\nP59635\tQ92843\nQ9BYF1\tP59594\nQ9BYF1\tQ6Q1S2\nQ9BYF1\tP59594\nQ9BYF1\tQ6Q1S2\nQ9BYF1\tQ6Q1S2\nP59594\tP00747\nP59594\tP00762\nQ6ZMR5\tP59594\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nQ9BYF1\tP59594\nP59594\tQ56NL1\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP20290\tQ86VG3\nQ86U32\tQ86VG3\nP24928\tQ86VG3\nP20290\tQ86VG3\nP20290\tQ86VG3\nP20290\tQ86VG3\nO00303\tP59594\nO00303\tP59594\nO00303\tP11223\nO00303\tP59594\nO00303\tP11223\nO00303\tP59594\nO00303\tP59594\nP59595\tQ05639\nP03070\tP02340\nP59595\tQ05639\nQ05639\tP59595\nP59595\tQ05639\nP59595\tQ05639\nP15130\tQ05639\nQ05639\tP59595\nQ05639\tP08107\nQ05639\tP59595\nQ05639\tP08107\nP59595\tQ05639\nP59595\tQ05639\nP59595\tQ05639\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP63279\nP59595\tP62937\nP59595\tP62937\nP35613\tP62937\nQ9BYF1\tP59594\nP59594\tQ5EGZ1\nQ9BYF1\tP59594\nP59594\tQ5EGZ1\nP59594\tQ56NL1\nQ9BYF1\tQ5GDB5\nQ9BYF1\tP59594\nQ56NL1\tP59594\nQ9BYF1\tP59594\nP59594\tQ56NL1\nQ56NL1\tQ5GDB5\nQ9BYF1\tQ5GDB5\nQ9BYF1\tP59594\nP0DTC2\tP62269\nP0DTC2\tQ96N67\nP0DTC2\tP63244\nP0DTC2\tP40227\nP0DTC2\tP07195\nP0DTC2\tP08621\nP0DTC2\tP60660\nP0DTC2\tP47756\nP0DTC2\tP52907\nP0DTC2\tQ9NYL9\nP0DTC2\tP13797\nP0DTC2\tQ9ULV4\nP0DTC2\tP59998\nP0DTC2\tP61158\nP0DTC2\tQ9UM54\nP0DTC4\tQ9ULX6\nP0DTC4\tP50402\nP0DTC4\tQ00325\nP0DTC4\tP05023\nP0DTC4\tO75746\nP0DTC4\tP16615\nP0DTC4\tQ9P035\nP0DTC4\tQ5H9R7\nP0DTC5\tP30876\nP0DTC5\tO15397\nP0DTC5\tQ9H078\nP0DTC5\tP62195\nP0DTC5\tP26599\nP0DTC5\tO60762\nP0DTC5\tQ9H936\nP0DTC5\tO00231\nP0DTC5\tQ9ULX6\nP0DTC5\tQ9UHI6\nP0DTC5\tQ7Z4H7\nP0DTC5\tQ96EY1\nP0DTC5\tQ00325\nP0DTC5\tQ15008\nP0DTC5\tO14818\nP0DTC5\tP40939\nP0DTC5\tP50402\nP0DTC5\tQ5H9R7\nP0DTC5\tO95757\nP0DTC5\tP57678\nP0DTC5\tP31689\nP0DTC5\tO15027\nP0DTC5\tP60900\nP0DTC5\tP25705\nP0DTC5\tP51665\nP0DTC5\tP62191\nP0DTC5\tP06576\nP0DTC5\tQ14257\nP0DTC5\tQ99615\nP0DTC5\tO95816\nP0DTC5\tO95071\nP0DTC5\tP46379\nP0DTC5\tO75746\nP0DTC5\tQ9Y5A9\nP0DTC5\tQ13200\nP0DTC5\tP53618\nP0DTC5\tQ9NZ01\nP0DTC5\tP35998\nP0DTC5\tQ16891\nP0DTC5\tO60884\nP0DTC5\tO43852\nP0DTC5\tQ99460\nP0DTC5\tQ9NTJ3\nP0DTC5\tP53621\nP0DTC5\tP55786\nP0DTC5\tQ15293\nP0DTC5\tP34932\nP0DTC5\tQ9UBF2\nP0DTC5\tO43592\nP0DTC5\tP35606\nP0DTC5\tP05023\nP0DTC5\tO95347\nP0DTC5\tP16615\nP0DTC5\tO14980\nP0DTC5\tQ96CS3\nP0DTC5\tQ9P035\nP0DTC9\tP09622\nP0DTC9\tP08621\nP0DTC9\tP38159\nP0DTC9\tQ13151\nP0DTC9\tQ9UN86\nP0DTC9\tQ13283\nP0DTC9\tQ9UM54\nP0DTC3\tP40939\nP0DTC3\tQ14257\nP0DTC3\tP22314\nP0DTC3\tP04406\nP0DTC3\tP57088\nP0DTC3\tQ8IXB1\nP0DTC3\tQ00325\nP0DTC3\tP31689\nP0DTC3\tP25705\nP0DTC3\tO43852\nP0DTC3\tO95757\nP0DTC3\tP46379\nP0DTC3\tP53621\nP0DTC3\tQ9H936\nP0DTC3\tO60884\nP0DTC3\tP26599\nP0DTC3\tO75746\nP0DTC3\tP11586\nP0DTC3\tQ9NZ01\nP0DTC3\tP53007\nP0DTC3\tP05023\nP0DTC3\tQ96CS3\nP0DTC3\tO43592\nP0DTC3\tO95347\nP0DTC3\tP16615\nP0DTC3\tQ16891\nP0DTC3\tO14980\nP0DTC3\tQ9P035\nP0DTC6\tQ5SW79\nP0DTC6\tP25705\nP0DTC6\tQ92552\nP0DTC6\tP06576\nP0DTC6\tP15924\nP0DTC6\tP53396\nP0DTC6\tP16615\nP0DTC6\tQ5T9A4\nP0DTC6\tP05023\nP0DTC6\tO75592\nP0DTC6\tO75934\nP0DTC6\tO14980\nP0DTC6\tQ96CS3\nP0DTC6\tP78406\nP0DTC6\tQ9NVI7\nP0DTC6\tP33176\nP0DTC6\tQ13561\nP0DTC6\tQ6P2E9\nP0DTC7\tP30153\nP0DTC7\tP06576\nP0DTC7\tP40939\nP0DTC7\tQ8IXB1\nP0DTC7\tQ9UHI6\nP0DTC7\tO95816\nP0DTC7\tP53007\nP0DTC7\tQ96EY1\nP0DTC7\tP50402\nP0DTC7\tO43852\nP0DTC7\tQ9H936\nP0DTC7\tP62195\nP0DTC7\tP30876\nP0DTC7\tQ99615\nP0DTC7\tP55786\nP0DTC7\tP57678\nP0DTC7\tQ00325\nP0DTC7\tO75746\nP0DTC7\tO15027\nP0DTC7\tP34932\nP0DTC7\tQ15008\nP0DTC7\tQ9UNM6\nP0DTC7\tO95071\nP0DTC7\tO60884\nP0DTC7\tP62191\nP0DTC7\tP05023\nP0DTC7\tP51665\nP0DTC7\tP53618\nP0DTC7\tQ9NZ01\nP0DTC7\tP35998\nP0DTC7\tQ13200\nP0DTC7\tP53621\nP0DTC7\tQ5H9R7\nP0DTC7\tP16615\nP0DTC7\tQ99460\nP0DTC7\tO43592\nP0DTC7\tQ9NXS2\nP0DTC7\tP46379\nP0DTC7\tQ16891\nP0DTC7\tO95347\nP0DTC7\tP35606\nP0DTC7\tQ9UBF2\nP0DTC7\tQ9P035\nP0DTC7\tO14980\nP0DTC7\tQ96CS3\nP0DTD8\tQ14257\nP0DTD8\tO60762\nP0DTD8\tO43852\nP0DTD8\tP25705\nP0DTD8\tP31689\nP0DTD8\tP06576\nP0DTD8\tQ8WTT2\nP0DTD8\tQ7KZF4\nP0DTD8\tQ9NVI7\nP0DTD8\tP05023\nP0DTD8\tP55786\nP0DTD8\tQ00325\nP0DTD8\tP53618\nP0DTD8\tP57088\nP0DTD8\tO43592\nP0DTD8\tO95347\nP0DTD8\tQ9NZ01\nP0DTD8\tQ16891\nP0DTD8\tP16615\nP0DTD8\tP53621\nP0DTD8\tQ9NXS2\nP0DTD8\tQ96CS3\nP0DTD8\tQ02978\nP0DTD8\tP35606\nP0DTD8\tQ9UBF2\nP0DTD8\tQ9P035\nP0DTD8\tO14980\nP0DTD8\tP10155\nP0DTC8\tQ14152\nP0DTC8\tP50402\nP0DTC8\tQ9UHI6\nP0DTC8\tP06576\nP0DTC8\tP62195\nP0DTC8\tP25705\nP0DTC8\tP40939\nP0DTC8\tQ5JWF2\nP0DTC8\tO14818\nP0DTC8\tP55786\nP0DTC8\tP57678\nP0DTC8\tO43852\nP0DTC8\tQ12789\nP0DTC8\tQ9H936\nP0DTC8\tP31689\nP0DTC8\tQ15008\nP0DTC8\tQ9UNM6\nP0DTC8\tQ00325\nP0DTC8\tO15027\nP0DTC8\tP30876\nP0DTC8\tQ7KZF4\nP0DTC8\tQ9NVI7\nP0DTC8\tO60884\nP0DTC8\tQ13200\nP0DTC8\tP05023\nP0DTC8\tO95071\nP0DTC8\tP35998\nP0DTC8\tO43592\nP0DTC8\tQ02978\nP0DTC8\tQ9NZ01\nP0DTC8\tP53618\nP0DTC8\tQ99460\nP0DTC8\tO75746\nP0DTC8\tP16615\nP0DTC8\tP53621\nP0DTC8\tQ16891\nP0DTC8\tO95347\nP0DTC8\tO14980\nP0DTC8\tQ9P035\nP0DTC8\tQ9UBF2\nP0DTC8\tP46379\nP0DTD2\tP19105\nP0DTD2\tQ14315\nP0DTD2\tP62140\nP0DTD2\tP0DP23\nP0DTD2\tP09622\nP0DTD2\tP52907\nP0DTD2\tP60660\nP0DTD2\tQ9NYL9\nP0DTD2\tP13797\nP0DTD2\tQ9UM54\nP0DTD2\tQ9ULV4\nP0DTD2\tP59998\nP0DTD3\tQ9BUQ8\nP0DTD3\tQ96N67\nP0DTD3\tQ15365\nP0DTD3\tP17987\nP0DTD3\tO00231\nP0DTD3\tQ14257\nP0DTD3\tQ16576\nP0DTD3\tP62269\nP0DTD3\tQ58FF8\nP0DTD3\tP27348\nP0DTD3\tP40227\nP0DTD3\tP63244\nP0DTD3\tP49368\nP0DTD3\tQ9Y265\nP0DTD3\tP07355\nP0DTD3\tP46379\nP0DTD3\tP48643\nP0DTD3\tQ99832\nP0DTD3\tQ9UHI6\nP0DTD3\tP31948\nP0DTD3\tQ3ZCQ8\nP0DTD3\tQ9NVI7\nP0DTD3\tP46782\nP0DTD3\tP25205\nP0DTD3\tP30153\nP0DTD3\tQ9ULX6\nP0DTD3\tP42677\nP0DTD3\tP61158\nP0DTD3\tP09874\nP0DTD3\tP50402\nP0DTD3\tP30876\nP0DTD3\tQ9NXS2\nP0DTD3\tP40939\nP0DTD3\tP06493\nP0DTD3\tP51570\nP0DTD3\tQ7Z4H7\nP0DTD3\tP57088\nP0DTD3\tQ14739\nP0DTD3\tP10155\nP0DTD3\tQ9UM54\nP0DTD3\tP04406\nP0DTD3\tQ9H936\nP0DTD3\tP11586\nP0DTD3\tQ9UBX3\nP0DTD3\tQ5H9R7\nP0DTD3\tP53618\nP0DTD3\tP53007\nP0DTD3\tP12004\nP0DTD3\tP05023\nP0DTD3\tQ16891\nP0DTD3\tQ9NZ01\nP0DTD3\tO14980\nP0DTD3\tO95347\nP0DTD3\tQ9NTJ3\nP0DTD3\tP53621\nP0DTD3\tP16615\nP0DTD3\tO43592\nP0DTD3\tQ9UBF2\nP0DTD3\tP35606\nP0DTD3\tQ9P035\nP59594\tP35247\nP52294\tP52630\nP52294\tP42224\nP52292\tQ14974\nP52294\tP42224\nP52294\tQ14974\nP52292\tQ14974\nP52294\tP42224\nP52292\tQ14974\nP52292\tQ14974\nQ99623\tP35232\nQ99623\tP35232\nO35129\tP67778\nO35129\tQ9JIA7\nO35129\tP67778\nO35129\tP19783\nO35129\tP00397\nO35129\tP67778\nP00405\tP19783\nP00405\tP12787\nP00405\tP19536\nP00405\tQ9CPQ1\nP00405\tP56391\nP00405\tP56392\nP12904\tP06782\nP33469\tP13010\nP33469\tP39019\nP33469\tP61254\nP33469\tP16402\nP33469\tP09651\nP33469\tP67809\nP33469\tP0DMV8\nP33469\tP11940\nP33469\tP12956\nP33469\tQ00839\nP33469\tP19338\nP33469\tP67809\nP33469\tP12956\nP33469\tQ00839\nP33469\tP19338\nK0BWD0\tP05161\nP05161\tK0BWD0\nK0BWD0\tP05161\nK0BWD0\tP05161\nR9QB93\tK0BWD0\nO43765\tP59635\nO43765\tP59635\nP59635\tP59596\nP59635\tP59637\nQ5MAG3\tP59635\nO43765\tP59635\nP59595\tP59596\nP0DTC2\tP35613\nP0DTC2\tP35613\nP0DTC2\tP35613\nP0DTC2\tP35613\nP0DTC2\tP35613\nP04135\tF1S215\nP04135\tF1RNN5\nP04135\tA6M930\nP04135\tA6M930\nP04135\tA6M930\nP04135\tA6M930\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP59594\nQ9BYF1\tP0DTC2\nQ9BYF1\tP0DTC2\nQ53F19\tP56270\nQ53F19\tQ14011\nQ53F19\tQ8N0Z6\nQ53F19\tQ8N8U2\nQ53F19\tP35052\nQ53F19\tO95373\nQ53F19\tQ9UH99\nQ53F19\tQ8NEM0\nQ53F19\tQ32M45\nQ53F19\tQ6L8Q7\nQ53F19\tQ6PJT7\nQ53F19\tQ02224\nQ53F19\tP31150\nQ53F19\tQ5DJT8\nQ53F19\tQ9UER7\nQ53F19\tP48668\nQ53F19\tO75940\nQ53F19\tJ3QSV6\nQ53F19\tO95619\nQ53F19\tQ7L590\nQ53F19\tQ96GX9\nQ53F19\tQ6IEG0\nQ53F19\tQ9BY42\nQ53F19\tP61326\nQ53F19\tQ96IZ7\nQ53F19\tQ8TF76\nQ53F19\tO00148\nQ53F19\tP49756\nQ53F19\tQ10589\nQ53F19\tP31431\nQ53F19\tP34741\nQ53F19\tP83916\nQ53F19\tP63104\nQ53F19\tO75400\nQ53F19\tQ8TDD1\nQ53F19\tP09603\nQ53F19\tQ86WX3\nQ53F19\tQ00403\nQ53F19\tQ13352\nQ53F19\tQ13011\nQ53F19\tQ9BRP8\nQ53F19\tQ9BSM1\nQ53F19\tQ6SPF0\nQ53F19\tO60684\nQ53F19\tQ16630\nQ53F19\tP55198\nQ53F19\tP78563\nQ53F19\tO00422\nQ53F19\tQ9ULL5\nQ53F19\tP52298\nQ53F19\tQ09161\nQ53F19\tQ9BXP5\nQ53F19\tQ96PV6\nQ53F19\tQ9UHI6\nQ53F19\tQ9UKV3\nQ53F19\tQ86U42\nQ53F19\tQ13155\nQ53F19\tP34897\nQ53F19\tQ9UKS6\nQ53F19\tO14950\nQ53F19\tP53582\nQ53F19\tP20700\nQ53F19\tP68400\nQ53F19\tQ5JVF3\nQ53F19\tP19784\nQ53F19\tO00629\nQ53F19\tP68036\nQ53F19\tQ96A72\nQ53F19\tQ9Y5S9\nQ53F19\tP15692\nQ53F19\tP35637\nQ53F19\tQ00688\nQ53F19\tP98179\nQ53F19\tQ9P289\nQ53F19\tO00505\nQ53F19\tQ96K17\nQ53F19\tQ9H307\nQ53F19\tQ96J01\nQ53F19\tQ8NI27\nQ53F19\tQ13769\nQ53F19\tQ13838\nQ53F19\tQ86W42\nQ53F19\tQ96FV9\nQ53F19\tP38919\nQ53F19\tQ6I9Y2\nQ53F19\tP51148\nQ53F19\tP50395\nP52298\tQ9UH99\nP52298\tP04003\nP52298\tQ8N0X7\nP52298\tK7ELV2\nP52298\tP14735\nP52298\tQ5VV67\nP52298\tP31150\nP52298\tQ5DJT8\nP52298\tP52294\nP52298\tP09603\nP52298\tP53007\nP52298\tQ9NWV8\nP52298\tQ8N5P1\nP52298\tQ9ULL5\nP52298\tO94762\nP52298\tP39880\nP52298\tQ8IXH7\nP52298\tQ9H3P2\nP52298\tQ6NZY4\nP52298\tQ8WX92\nP52298\tQ9Y580\nP52298\tQ86VM9\nP52298\tQ9H814\nP52298\tP18615\nP52298\tQ09161\nP52298\tQ9BXP5\nP52298\tQ9UHI6\nP52298\tQ8NC51\nP52298\tO75822\nP52298\tQ9P289\nP52298\tO00505\nP52298\tP51148\nP52298\tP50395\nP52298\tP61927\nP52298\tO00505\nP52298\tQ13769\nP52298\tQ9H814\nP52298\tQ09161\nP52298\tQ9BXP5\nP52298\tP38919\nQ53F19\tO00505\nQ53F19\tQ13769\nQ53F19\tQ9H814\nQ53F19\tP38919\nQ53F19\tQ09161\nQ53F19\tQ9BXP5\nQ09161\tQ53F19\nP33724\tP59632\nP33724\tP59632\nP33724\tP59632\nP33724\tP59632\nP59632\tP33724\nP0DTC9\tP0DTC4\nP0DTC3\tP0DTC4\nP0DTD2\tP0DTC4\nQ14160\tB7Z2Y1\nQ14160\tQ14155\nQ14160\tQ7Z628\nP22460\tQ14160\nQ96DN2\tQ14160\nO00429\tQ14160\nQ99569\tQ14160\nQ96DL1\tQ14160\nB7Z2Y1\tQ14160\nQ14155\tQ14160\nQ9UQB3\tQ14160\nQ9BR11\tQ14160\nQ96DL1\tQ14160\nQ14155\tQ14160\nB7Z2Y1\tQ14160\nQ9ULI0\tQ14160\nP25100\tQ14160\nP53778\tQ14160\nQ9NYB5\tQ14160\nQ15311\tQ14160\nP33402\tQ14160\nQ9P0K1\tQ14160\nQ92953\tQ14160\nO15439\tQ14160\nP35222\tQ14160\nP48065\tQ14160\nQ14160\tP08581\nP22460\tQ14160\nO00429\tQ14160\nQ14160\tQ6UY11\nQ14160\tQ9ULJ7\nQ14160\tA6NIM6\nP81408\tQ14160\nQ9NQG5\tQ14160\nQ6UXZ0\tQ14160\nQ92502\tQ14160\nQ8TA94\tQ14160\nQ9Y6R1\tQ14160\nQ9Y466\tQ14160\nQ8TBB1\tQ14160\nP22460\tQ12959\nQ9UQB3\tQ12959\nQ12959\tQ96DL1\nQ12959\tQ15311\nQ12959\tQ96A65\nQ6ZTQ3\tQ12959\nQ12959\tQ14CM0\nQ12959\tQ9NYB5\nQ12959\tQ14524\nQ12959\tQ13224\nQ12959\tP85299\nQ12959\tQ9NVW2\nQ12959\tQ99569\nQ12959\tQ6DN90\nQ12959\tQ86W11\nQ12959\tA1L4L8\nQ12959\tQ15303\nQ12959\tQ96GG9\nQ96GL9\tQ12959\nQ86UD3\tQ12959\nO60333\tQ12959\nA1A5B4\tQ12959\nQ7Z628\tQ12959\nP0C2L3\tQ12959\nQ9NS75\tQ12959\nQ96SF7\tQ12959\nB7Z2Y1\tQ12959\nQ9NS75\tQ12959\nQ9P021\tQ12959\nQ7Z628\tQ12959\nQ99569\tQ96NW7\nQ9UQB3\tQ96NW7\nQ9UQB3\tQ96RT1\nQ99569\tQ96RT1\nO00192\tQ96RT1\nQ9BY21\tQ96RT1\nQ96DL1\tQ96RT1\nQ8NHY3\tQ96RT1\nP50804\tQ12959\nP24835\tQ12959\nP50804\tQ12959\nP24835\tQ12959\nP27228\tQ12959\nQ06093\tQ12959\nP06463\tQ12959\nP21735\tQ12959\nP30911\tQ12959\nP06427\tQ12959\nP0C213\tQ12959\nP03126\tQ12959\nP27962\tQ12959\nP54667\tQ12959\nP54667\tQ12959\nP09708\tQ12959\nP17386\tQ12959\nP36807\tQ12959\nP26554\tQ12959\nQ0A442\tQ12959\nP30910\tQ12959\nP26555\tQ12959\nP16717\tQ12959\nP50804\tQ12959\nP24835\tQ12959\nP21735\tQ12959\nP06427\tQ12959\nP27228\tQ12959\nP09708\tQ12959\nA3EX99\tQ96RT1\nA3EXD5\tQ96RT1\nP06427\tQ96RT1\nQ9IDV3\tQ96RT1\nQ1A244\tQ96RT1\nP03126\tQ96RT1\nP50804\tQ96RT1\nP0C9G5\tQ96RT1\nA3EXD4\tQ96RT1\nP89432\tQ96RT1\nP0C213\tQ96RT1\nQ14160\tP06427\nQ14160\tP03126\nQ14160\tP17589\nP0C213\tQ14160\nQ14160\tP0C222\nP06427\tQ14160\nP03126\tQ14160\nP0C213\tQ14160\nP27228\tQ14160\nP24835\tQ14160\nP04299\tQ14160\nP16717\tQ14160\nP50804\tQ14160\nP21735\tQ14160\nP0C222\tQ14160\nP03333\tQ14160\nQ09SZ7\tQ14160\nQ08089\tQ14160\nQ18LE1\tQ96RT1\nQ14500\tQ14160\nP48050\tQ14160\nP12814\tQ14160\nP35609\tQ14160\nQ08043\tQ14160\nO43707\tQ14160\nQ8IWK6\tQ12959\nQ86SQ6\tQ12959\nQ96PE1\tQ12959\nP22459\tQ12959\nQ14500\tQ12959\nQ14155\tQ14160\nP33402\tQ14160\nP53778\tQ14160\nQ99569\tQ14160\nQ99569\tQ14160\nQ14155\tQ14160\nP53778\tQ14160\nP33402\tQ14160\nP35222\tQ14160\nP59595\tP63165\nP59595\tP63165\nP59595\tP09651\nP59595\tP09651\nP59595\tP09651\nP0C6X7\tO75348\nP0C6X7\tO75348\nP0C6X7\tO75348\nP0C6X7\tO75348\nP0C6X7\tO75348\n', - }, - { - kind: 'annotation', - name: 'COVID-19 KEGG pathways', - fileName: 'HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt', - text: 'Viral carcinogenesis\tQ00403,Q08043,Q14160,P04439,Q96EY1,Q92793,P06493,P35609,Q14653,P12814,P27348,Q7KZF4,P62258,O43707,P62191,P63104,Q12959\nInfluenza A\tP52294,P42224,P78406,Q92793,P53778,P52948,Q13838,Q14653,P08107,P52630,P0DMV8,O14980,P52292,Q86UT6,P00747,Q86U42\nHerpes simplex infection\tP19784,P42224,P62140,P67870,P04439,Q92793,P06493,P68400,P24928,Q14653,P52630,Q9UER7,Q86U32\nProximal tubule bicarbonate reclamation\tQ9Y6R1,Q9UBX3,P05023,P05026\n', - }, - { - kind: 'annotation', - name: 'COVID-19 SMART domains', - fileName: 'HomoSapiens_Protein_Domains_SMART_FILTERED.txt', - text: 'SM00265:BH4\tQ07817,Q92843,P10415\nSM00033:CH\tQ14315,P12814,Q08043,O43707,P13797,Q8NHY3,P35609,Q14155\nSM00361:RRM_1\tP11940,Q14011,Q13310,P19338,P38159\nSM00150:SPEC\tP12814,Q08043,P15924,O43707,P35609\nSM00088:PINT\tQ9UNM6,Q15008,Q14152,O00231\nSM00101:14_3_3\tP27348,P62258,P63104\nSM00948:SM00948\tP25787,O14818,P60900\nSM00557:IG_FLMN\tQ14315,Q7Z4H8,Q6UW63\nSM00490:HELICc\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762\nSM00487:DEXDc\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762\nSM00244:PHB\tP27105,P35232,Q99623\nSM00271:DnaJ\tQ8IXB1,Q99615,P31689,Q96EY1,O60884\n', - }, - { - kind: 'annotation', - name: 'COVID-19 InterPro domains', - fileName: 'HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt', - text: 'P-loop containing nucleoside triphosphate hydrolase\tQ9UM54,P35998,Q9NVI7,Q9ULI0,Q5T9A4,Q9HCE1,Q9NTJ3,P11586,P62195,Q02224,P33176,Q9H078,P38606,O00429,O15439,O00148,Q00839,Q9Y265,P38919,Q92900,Q96MM7,O14656,O94762,Q05639,Q5JWF2,Q9NR30,Q8TDD1,Q9UHI6,Q9BUQ8,Q13838,P51148,P25205,P06576,P33527,O95347,Q9NU22,P25705,O60333,P62191,Q9Y276,Q12959\nEF-hand-like domain\tQ08043,P42224,P60660,Q15293,Q07820,P35609,Q9Y680,O14950,P0DP23,P12814,P19105,P52630,O75746,Q9H4F8,Q96AY3,O43707,O43852,Q14257,P13797\nApoptosis regulator,Bcl-2,BH4 motif,conserved site\tQ07817,Q92843,P10415\nATPase,F1/V1/A1 complex,alpha/beta subunit,C-terminal\tP06576,P38606,P25705\nApoptosis regulator,Bcl-2 protein,BH4\tQ07817,Q92843,P10415\nDNA/RNA helicase,DEAD/DEAH box type,N-terminal\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762\nFilamin/ABP280 repeat-like\tQ14315,O75592,Q7Z4H8,Q6UW63\nChaperone DnaJ\tP31689,Q96EY1,O60884\nCalponin homology domain\tQ14315,P12814,Q08043,O43707,P13797,Q8NHY3,P35609,Q14155\nRNA recognition motif domain,eukaryote\tP11940,Q14011,Q13310,P19338,P38159\nATPase,AAA-type,conserved site\tP35998,Q9ULI0,P62191,Q9Y276,P62195\nPeptidase M16,zinc-binding site\tQ10713,O75439,P14735\nKinase associated domain 1 (KA1)\tQ9P0L2,Q7KZI7,P27448\nATPase,alpha/beta subunit,N-terminal\tP06576,P38606,P25705\nHeat shock protein DnaJ,cysteine-rich domain\tP31689,Q96EY1,O60884\nATPase,F1/V1/A1 complex,alpha/beta subunit,nucleotide-binding domain\tP06576,P38606,P25705\nATPase,alpha/beta subunit,nucleotide-binding domain,active site\tP06576,P38606,P25705\nSpectrin/alpha-actinin\tP12814,Q08043,P15924,O43707,P35609\nImmunoglobulin E-set\tQ14315,Q96DL1,O75592,P08581,Q14500,Q7Z4H8,P48050,P61916,Q6UW63\nEF-Hand 1,calcium-binding site\tP0DP23,P12814,P19105,O75746,Q9H4F8,Q15293,Q96AY3,O43707,O43852,P13797,Q14257,Q9Y680,O14950\nEF-hand domain\tQ08043,P60660,Q15293,P35609,Q9Y680,O14950,P12814,P0DP23,P19105,O75746,Q96AY3,O43707,O43852,P13797,Q14257\n26S proteasome subunit P45\tP35998,P62191,P62195\nG-protein beta WD-40 repeat\tO43818,P53621,K7ELV2,P78406,P35606,P63244,Q16576,Q96J01\n14-3-3 protein\tP27348,P62258,P63104\n14-3-3 protein,conserved site\tP27348,P62258,P63104\n14-3-3 domain\tP27348,P62258,P63104\nApoptosis regulator,Bcl-2,BH3 motif,conserved site\tQ07817,Q07820,P10415\nProteasome A-type subunit\tP25787,O14818,P60900\nProteasome,alpha-subunit,N-terminal domain\tP25787,O14818,P60900\nChaperone DnaJ,C-terminal\tP31689,Q96EY1,O60884\nHSP40/DnaJ peptide-binding\tP31689,Q96EY1,O60884\nHelicase,C-terminal\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762\nCation efflux protein\tQ8NEW0,Q6NXT4,Q6PML9\nCation efflux protein transmembrane domain\tQ8NEW0,Q6NXT4,Q6PML9\nFilamin/ABP280 repeat\tQ14315,Q7Z4H8,Q6UW63\nSpectrin repeat\tP12814,Q08043,O43707,P35609\nHelicase,superfamily 1/2,ATP-binding domain\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762\nBromodomain,conserved site\tP25440,Q9ULI0,Q92793,O60885\nAnoctamin/TMEM 16\tA1A5B4,Q32M45,Q4KMQ2\nRNA helicase,ATP-dependent,DEAD-box,conserved site\tQ8TDD1,Q9BUQ8,Q9UHI6,P38919\nMitochondrial carrier protein\tP53007,O75746,Q9BQT8,Q9H936\nTetratricopeptide TPR-1\tQ99615,O94826,P31948,O43765\nBand 7 protein\tP27105,P35232,Q99623\nWD40-repeat-containing domain\tK7ELV2,Q9ULV4,P78406,P35606,Q86W42,Q16576,Q96JC1,O43818,B7Z2Y1,P53621,Q6P2E9,Q9H270,Q12770,P63244,Q96J01\nvon Willebrand factor,type A\tQ9NWV8,P05556,P12109,P12956,Q9NU22,P13010,P10155\nDnaJ domain\tQ8IXB1,Q99615,P31689,Q96EY1,O60884\nProteasome/cyclosome,regulatory subunit\tQ13200,Q99460\nApoptosis regulator,Bcl-2/ BclX\tQ07817,P10415\nTranscription factor TFIIB,cyclin-like domain\tQ00403,Q92994\nInsulin-induced protein family\tO15503,Q9Y5U4\nCoatomer,WD associated region\tP53621,P35606\nKu70/Ku80 C-terminal arm\tP12956,P13010\nKu70/Ku80,N-terminal alpha/beta\tP12956,P13010\nKu70/Ku80 beta-barrel domain\tP12956,P13010\nRab GDI protein\tP50395,P31150\nATPase,F1 complex beta subunit/V1 complex,C-terminal\tP06576,P38606\nMago nashi protein\tP61326,Q96A72\nClathrin/coatomer adaptor,adaptin-like,N-terminal\tQ9UBF2,O00203,P53618', - }, - { - kind: 'annotation', - name: 'COVID-19 GO biological process', - fileName: 'HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt', - text: 'negative regulation of anoikis\tP06731,Q07817,P05556,Q07820,P10415\nT cell receptor signaling pathway\tP35998,P48556,Q13200,P25787,Q99460,P62195,Q9UNM6,O14818,Q15008,P51665,P60900,O00231,P62191\nregulation of cellular response to heat\tQ99615,P08107,K7ELV2,P62258,Q9UL15,O95816,P0DMV8,P78406,Q92793,P52948\nRNA secondary structure unwinding\tQ9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838\nSRP-dependent cotranslational protein targeting to membrane\tP60866,P08708,P61254,P62269,P39019,P42677,P61927,P62753,Q9Y3U8,P46782\npositive regulation of establishment of protein localization to telomere\tP40227,P17987,P48643,Q99832\nFc-epsilon receptor signaling pathway\tP35998,P48556,Q13200,P25787,Q99460,P62195,P0DP23,Q9UNM6,O14818,Q15008,P51665,P60900,O00231,P62191\nmRNA transport\tQ09161,O14980,Q9Y5S9,Q53F19,P09651,Q96A72,Q9UN86\nnegative regulation of transcription elongation from RNA polymerase II promoter\tQ8WX92,Q9H3P2,P18615,O94762\nnuclear export\tQ09161,Q9H814,P52298,P09651\nresponse to hydrogen peroxide\tO00629,Q13443,P22460,P42224,Q9P289,P09601,P10415\nintracellular protein transport\tQ9UM54,Q9H173,Q9ULJ7,Q14974,O00203,P35606,O14964,Q96JC1,P53621,O95373,Q9UBF2,Q9H270,O15397,O14980,O43592,P53618\nrRNA processing\tP60866,Q9NR30,P39019,P62753,P61927,P57678,O43818,P08708,P61254,Q9NU22,P62269,P42677,P38919,P46782,Q9Y3U8\nrelease of cytochrome c from mitochondria\tQ07817,Q16548,Q3ZCQ8,O00429,P10415\npositive regulation of translation\tP11940,Q9BRP8,Q14011,Q6PKG0,P98179,P38919,Q13838\nfocal adhesion assembly\tP12814,Q08043,P35609,Q14155,P10415\ncalcium activated phosphatidylserine scrambling\tA1A5B4,Q32M45,Q4KMQ2\ngene silencing by RNA\tP30876,Q09161,Q7KZF4,P11940,K7ELV2,P52298,O15397,P78406,P52948,P24928\nnegative regulation of programmed cell death\tP02768,Q96EY1,O76024,P62195\nprotein folding in endoplasmic reticulum\tQ8N766,Q8IXB1,Q86YB8,P11021\npositive regulation of viral genome replication\tP62937,P11940,Q6PKG0,Q9P035,P78563\nprotein complex assembly\tP52907,O14745,Q9BSJ2,O43292,Q9Y375,Q9NU22,P30153,Q92793,P06493,O00505\nmitochondrion organization\tP06576,P35232,Q5VV67,O00429,Q96EY1,P09874,Q9Y276,Q99623\nT cell differentiation in thymus\tP61769,Q96EY1,P62753,P35222,P10415\nintra-Golgi vesicle-mediated transport\tP53621,Q9UBF2,P35606,P39880,P53618\nprotein homotetramerization\tQ9H773,Q96GX9,O00429,P34897,Q14500,P35609,P14735\ncalcium activated galactosylceramide scrambling\tA1A5B4,Q32M45,Q4KMQ2\ncalcium activated phosphatidylcholine scrambling\tA1A5B4,Q32M45,Q4KMQ2\nviral entry into host cell\tP08107,P27487,P05556,P15151,P0DMV8,Q9BYF1,P15144,O00505,P14735\nDNA duplex unwinding\tQ13283,P12956,P13010,Q9Y265,P25205,O94762\nplatelet degranulation\tP0DP23,P12814,P05155,P02768,O15439,P15692,O43707,P00747,P35609\nregulation of nucleic acid-templated transcription\tP12814,P35637,O43707,P35609\ncell proliferation\tO15503,P68036,Q14160,Q13561,P08581,P13010,Q16576,P06493,Q15303,P25100,Q07817,P09603,Q9BXP5,Q6PKG0,O95071,P42677,Q7L590,P12004,Q10589,P10415\nosteoblast differentiation\tP35998,Q9NR30,Q7KZF4,P06576,P35232,P12109,Q00839,J3QSV6,P38159\nintrinsic apoptotic signaling pathway in response to DNA damage\tQ07817,Q16548,Q92843,Q07820,P09601,P10415\nregulation of DNA recombination\tP52294,P52292,Q96FV9\npositive regulation of histone H3-K4 methylation\tP26358,P35222,Q9H3P2,P18615\ntransport\tP53007,Q9Y6R1,P02768,Q9UBX3,P52948,Q15818,Q9UN86,Q9NZJ7,Q15311,P46379,P33527,P38606,Q13283,Q00325,Q02978,Q9Y3A6,Q13224,Q9H936,P48065\npositive regulation of intrinsic apoptotic signaling pathway\tO14745,Q07817,O00429,P63244,P10415\n7-methylguanosine mRNA capping\tP30876,Q09161,P52298,Q53F19,P24928\nSREBP signaling pathway\tO15503,Q9Y5U4,Q12770\nIRES-dependent viral translational initiation\tP26599,Q14152,O00303\nextrinsic apoptotic signaling pathway in absence of ligand\tQ07817,Q16548,Q92843,Q07820,P10415\nribosomal small subunit assembly\tP08708,P39019,P42677,P46782\nubiquitin-dependent protein catabolic process\tP35998,P68036,P46379,Q9UNM6,P25787,O14818,Q9NXK8,O95071,P60900,O00231,P63279,Q9NVW2\nregulation of translational initiation\tQ09161,P52298,O75822,Q14152,O00303\nestablishment of integrated proviral latency\tP62937,P12956,P13010\nresponse to endoplasmic reticulum stress\tQ8IXB1,Q13438,P57088,Q9Y4L1,P16615,O76024,Q9BS26\nchaperone-mediated protein folding\tQ96AY3,P68400,O14656,Q9Y680,Q00688\ngastrulation\tP37173,P63244,P62753,P09622\nfibrinolysis\tP07355,P00750,P05155,P00747\nnegative regulation of mRNA splicing,via spliceosome\tO00422,P26599,Q9UKV3,P38159\nRNA processing\tQ13310,Q8TDD1,Q9UHI6,P98179,Q96FV9,Q00839,P78563,Q86U42\nnegative regulation of apoptotic process\tQ9NVI7,P31689,P02768,Q96GX9,Q92843,Q96EY1,P62753,Q86W42,P06493,Q07820,Q15303,P06731,Q07817,Q16548,P46379,Q9Y466,P11021,P15692,Q5JVF3,Q99623,P10415,P63104\nnegative regulation of intrinsic apoptotic signaling pathway\tQ07817,Q92843,Q07820,P10415\nprotein targeting\tP27348,Q13438,P62258,Q96RT1,P63104\nregulation of smooth muscle cell proliferation\tP12956,P13010,P35222\npositive regulation of RNA polymerase II transcriptional preinitiation complex assembly\tP35998,P62191,P62195\nresponse to hypoxia\tP22460,P00750,P37173,P27487,O00469,Q12770,P15692,Q92793,P09601,O43707,P05026\nresponse to drug\tQ9UM54,Q5JWF2,P42224,P13010,P06493,Q92820,P35222,P61769,P37173,P33527,O14980,P40939,O15439,P28300,P05023,P10415\nmembrane repolarization during cardiac muscle cell action potential\tP62258,P05023,P05026\nendoplasmic reticulum mannose trimming\tQ13438,Q9BZQ6,Q9NYU1\npositive regulation of potassium ion transport\tP33176,P35609,Q12959\npositive regulation of cellular component movement\tP39019,P15692,O43707\nresponse to unfolded protein\tP34932,Q96CS3,P31689,O95757,Q9BS26\ncellular protein modification process\tP00750,P68036,P38435,O00469,Q86YB8,P22314,P28300,P63279\nprotein peptidyl-prolyl isomerization\tQ6UX04,P62937,Q96AY3,Q9Y680,Q00688\nmuscle contraction\tQ9NYL9,P0DP23,P19105,Q08043,P60660,Q14500,P50402,Q86U42,O14950\ntype I interferon signaling pathway\tP42224,P52630,P04439,P05161,Q10589,Q14653\npositive regulation of protein ubiquitination\tQ9NV92,P68036,Q13155,Q96EY1,P11021,O76024\ngluconeogenesis\tP04406,P53007,O75746,Q9UBX3,Q02978\nmitotic nuclear envelope disassembly\tK7ELV2,P78406,P06493,P50402,P52948\npositive regulation of mesenchymal cell proliferation\tP37173,P42224,P15692,P35222\nprotein targeting to plasma membrane\tP07355,P19105,Q92953,P35613,O14950\npositive regulation of protein targeting to mitochondrion\tP19784,P48556,Q8TAD8,P68036,Q9H270,P55786,Q9Y265\nnuclear pore complex assembly\tP57088,Q9NQC3,P52948\npositive regulation of DNA-templated transcription,elongation\tQ13769,Q96FV9,Q13838\nendoplasmic reticulum unfolded protein response\tO76061,P11021,Q9BZQ6,Q9NYU1,O76024\nER-associated misfolded protein catabolic process\tQ99470,Q9NYU1,O14656\nrelaxation of cardiac muscle\tP05023,P16615,P05026\nproteasome assembly\tP48556,Q9UNM6,O00231\nregulation of cardiac muscle cell contraction\tQ14524,P05023,P63165\nlysine catabolic process\tQ9UDR5,Q9BQT8,P09622\npotassium ion import\tQ14500,P48050,P05023,P05026\nproteolysis involved in cellular protein catabolic process\tP25787,O14818,P11021,P60900,P14735\nsubstantia nigra development\tP0DP23,P27348,O15027,P62258,P11021\nestablishment or maintenance of epithelial cell apical/basal polarity\tQ7KZI7,Q96RT1,Q12959\nmicrotubule cytoskeleton organization\tQ9P0L2,P04406,Q96N67,Q7KZI7,P06493,P27448\npositive regulation of protein insertion into mitochondrial membrane involved in apoptotic signaling pathway\tP27348,P62258,P10415,P63104\n', - }, - { - kind: 'annotation', - name: 'COVID-19 GO molecular function', - fileName: 'HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt', - text: 'poly(A) RNA binding\tP62753,J3QSV6,Q16630,Q13283,P62269,O00148,Q02978,O43707,Q00839,Q86WX3,Q13151,P68036,Q9BRP8,P17987,P39019,Q8TDD1,Q6NZY4,P11940,Q9UKV3,P62258,P56270,P16402,P67809,P61254,P63244,Q8NC51,P38159,P40227,Q9Y580,Q14974,P12956,P09874,Q00688,P78563,P30876,Q7KZF4,Q6PKG0,P63279,Q92900,P63104,Q7L8L6,Q8N5P1,Q9BUQ8,Q86VM9,Q14152,P63165,Q9UN86,Q13838,Q8WTT2,O43818,P50395,Q9BXP5,O75400,P61326,Q96A72,Q9Y3U8,P07355,Q09161,Q9Y5A9,P31948,P18615,Q6PJT7,Q9H307,Q5VV67,Q8NCA5,P09651,P15924,Q13310,P20290,P52298,P22314,P49756,P04439,P24928,P26599,P35637,Q9Y5S9,Q9NW13,P19338,P62191,Q15365,P04003,O75534,Q9ULX6,Q9HCE1,Q53F19,O00422,P52292,Q14739,P98179,P38919,Q9UKA9,P46782,Q86U42,Q16891,P60866,Q8TAD8,Q9NR30,P13010,P08708,P62937,P49368,Q14011,P25705,Q7KZI7,P42677,P08621,Q9NQC3,Q86U32,O75940,Q10589\nprotein binding\tQ9P021,Q13200,Q96GX9,Q13769,Q5H9R7,Q3ZCQ8,Q99988,Q86VR2,P62753,Q9NTJ3,P0DP23,P61769,Q9BSJ2,O43292,P62269,Q8TBB1,P59998,P50402,Q8IXH7,Q00839,P34741,Q99569,Q13561,Q07820,P55198,Q13352,Q4KMQ2,Q9NZJ7,Q96GG9,Q9H3K2,Q07817,Q9UKV3,P52630,P62258,Q86YB8,Q8NC51,P35998,P40227,Q00403,P26358,Q96PE1,P35606,Q92793,Q96FV9,Q15303,P09874,P35609,Q9Y680,Q00688,P78563,Q13443,Q15311,P27348,Q6PKG0,Q12789,P11021,Q92900,P63104,Q9ULJ7,Q8WX92,Q7L8L6,P35613,Q9BUQ8,Q9UBX3,Q9P035,P25100,Q13838,Q9NQG5,P06576,Q9Y5U4,Q13438,Q9BXP5,O60333,P55085,Q13224,Q9NS75,P53618,Q09161,O14745,Q9P0K1,Q9UL15,Q9Y5A9,P31948,P30153,Q9H773,Q6PJT7,O75592,Q8NCA5,Q9Y3A6,P57088,O00303,P05023,P09651,P05026,Q9NZ01,P52294,Q9BQ95,Q96CW5,O95619,Q13310,P20290,P52298,Q8N0Z6,P49756,P14735,P24928,P37173,Q9H270,Q9Y5S9,P51665,O00192,P60900,Q96HW7,P00747,P19338,Q15365,Q12959,P27105,Q9BSM1,P25787,P04003,Q9UQB3,Q14CM0,P81408,P67870,Q9HCE1,P68400,Q8IXB1,P33176,O00422,P00750,Q96IZ7,P08107,P46379,P83916,Q8NEM0,Q96DZ1,P48643,O00429,P52292,P05161,O43765,P46782,Q99623,Q9UKS6,Q9NR30,O75348,P52948,P51148,Q8N0X7,P49368,Q99615,P05155,O15397,P0DMV8,P08621,Q6ZTQ3,Q9NQC3,O43852,Q10589,O76024,P25440,P04406,Q9ULV4,Q14524,Q96EY1,P53778,P35247,P09601,Q16630,Q5BJF2,Q9H845,O15027,Q13283,P15151,Q9NXK8,O00148,O95071,O95070,P53396,Q7L590,O43707,O14656,P10415,Q05639,Q86WX3,P68036,Q9BRP8,Q9Y6R1,P05556,O75822,P17987,Q13155,P39019,Q00765,Q13011,Q15293,P48668,O60884,Q6NZY4,O60885,O60684,P11940,P61254,P56270,P67809,Q9NU22,Q15008,P63244,Q96HR9,P31150,P48065,P38159,Q9BS26,Q9H173,Q9UM54,P34932,Q96A65,Q14974,O43633,Q96IV0,P12956,Q14160,Q96PV6,P16615,P35222,Q99832,P62195,P12814,P30876,P19105,Q9H078,Q02224,P52907,Q7KZF4,P35232,Q9Y375,O95816,Q14257,P63279,Q5JWF2,O94826,Q8N5P1,Q86VM9,Q14152,P07195,Q9H3P2,P63165,Q9UN86,Q14155,Q6DN90,Q14653,Q96NW7,P09603,P50395,Q9H814,O95347,P40939,P31431,O75400,Q8IWR1,Q9UER7,P61326,Q96A72,P07355,P31689,P42224,K7ELV2,P39656,Q16576,Q9BYF1,P11586,P18615,P27487,Q9NWV8,Q7Z5G4,Q9H8W4,P13804,Q9Y6M9,Q8N8U2,Q9Y265,Q96SF7,O00629,Q5SW79,P15924,P62140,P22459,P22314,Q9UHI6,Q99471,Q9UH99,P04439,Q96RT1,O14950,Q96S52,P22460,P26599,O14818,P20700,P35637,Q12770,Q8NI27,Q9H4F8,Q86UT6,P15692,Q9P289,P61158,P62191,Q9Y276,P19784,Q8IV08,Q96CS3,O15503,P08581,Q92843,O75534,P06493,Q9ULX6,P61916,Q53F19,O14964,O95373,Q9NV92,Q16548,Q96F46,O00124,Q14739,P28300,O75934,P98179,P48050,P38919,P12004,Q86U42,Q16891,P60866,Q8IWF2,Q6I9Y2,Q8TAD8,Q08043,Q8TF76,P02768,P60660,P13010,P34897,P57678,Q9NYU1,O00505,P27448,P25205,Q14315,Q6P2E9,P62937,O60762,Q14011,Q92552,O14980,P25705,Q7KZI7,P42677,Q96K17,Q5JVF3,O00231,Q86U32,O75940,P49069\nprotein domain specific binding\tQ14974,O43633,Q14524,Q9UHI6,P67870,P35609,O14964,P0DP23,P27348,Q9H270,P62258,O14980,P11021,P05023,P00747,P63104\nion channel binding\tP0DP23,P12814,P27348,Q08043,P62258,Q14524,O43707,P35222,P63165,P35609,Q12959\nHsp70 protein binding\tQ8IXB1,P31689,P46379,P31948,Q96EY1,P06493\nglycoprotein binding\tQ8IWF2,P61769,P00750,Q13438,Q96DZ1,P11021,Q9BYF1,P14735\nTBP-class protein binding\tP35998,Q00403,Q92994,P62191,P62195\nestrogen receptor binding\tP35637,Q8TDD1,P09874,P12004,P35222,Q99623\nmisfolded protein binding\tQ8IXB1,P46379,P11021,O14656\nprotein transporter activity\tO00629,P52294,O60684,O95373,Q14974,O15397,P52292,O00505\nBH3 domain binding\tQ07817,Q07820,P10415\nactin filament binding\tQ14315,P12814,Q9UM54,P47756,Q9ULV4,P59998,O43707,P61158,P13797,Q8NHY3,P35609\npoly(A) binding\tP11940,Q6PJT7,Q13310,P38919\nidentical protein binding\tP04406,P42224,Q96GX9,P67870,P09874,Q99832,P35609,P61769,Q9H773,P11310,P27487,Q8NEM0,O95816,O00429,O00148,Q7L590,P12004,P10415,P63104,P02768,Q13561,Q9UH99,P34897,P07195,Q13838,Q14653,P06731,Q07817,O14818,P52630,P35637,P15692,Q9P289,P19338,P38159\nRan GTPase binding\tO95373,Q14974,O15397,O14980,O43592\nintegrin binding\tQ13443,P12814,Q08043,Q9P0K1,P05556,O43707,Q96RT1,Q08431,P35609\nproteasome-activating ATPase activity\tP35998,P62191,P62195\ndolichyl-phosphate-mannose-protein mannosyltransferase activity\tO60762,Q9Y673,Q99470\ncytoskeletal protein binding\tP07355,Q14315,Q9UKS6,O14656,P35609,Q12959\noxidoreductase activity,acting on the CH-CH group of donors\tQ9NZ01,Q9H845,P11310,Q14739\nvirus receptor activity\tP08107,P27487,P05556,P15151,P0DMV8,Q9BYF1,P15144,P14735\nprotein N-terminus binding\tP19784,Q96A65,P27348,Q9UER7,O43707,P09874,P68400,Q99623\nprotein homodimerization activity\tP27105,P42224,Q92843,O76061,P09601,Q15303,P35609,P12814,Q16548,P83916,P27487,P15151,O00429,O43707,Q96SF7,P10415,Q08043,P39019,Q07820,P14735,Q14653,Q4KMQ2,P06731,Q07817,P09603,P63244,P15692,P51665,Q9UER7,Q9P289,Q10589,Q6UY11\nNAD binding\tP04406,P40939,P07195,P09622,P09874\nprotein kinase binding\tQ05639,Q13151,P62140,Q14524,Q9UL15,P39019,Q96EY1,P62753,P09874,P35222,Q14155,P0DP23,Q07817,P22460,P62269,Q9UER7,P05023,P63104,Q12959\nrRNA binding\tQ9NR30,Q7L8L6,P62269,P61927,P46782\nhistone deacetylase binding\tP35232,P08107,P62258,P0DMV8,P52292,Q9UHI6,Q9Y466,Q9ULX6,P09874\ntelomeric DNA binding\tP12956,P13010,P19338,Q92900\nRNA cap binding\tQ09161,Q6PKG0,P52298\nchromatin binding\tP25440,P26358,Q8N0Z6,Q9ULI0,P67870,P34897,Q92793,P06493,O60885,Q9H3P2,P18615,P30876,Q8WTT2,P83916,P67809,Q92900,P12004,Q6PML9,P38159\ncation transmembrane transporter activity\tQ8NEW0,Q6NXT4,Q6PML9\nphospholipid scramblase activity\tA1A5B4,Q32M45,Q4KMQ2\npeptidyl-prolyl cis-trans isomerase activity\tQ6UX04,P62937,Q96AY3,Q9Y680,Q00688', - }, - { - kind: 'annotation', - name: 'COVID-19 GO cellular component', - fileName: 'HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt', - text: 'membrane\tP04406,Q13200,O00203,P62753,Q9NXS2,P09601,Q16630,J3QSV6,P61769,P53621,Q9BSJ2,O43292,P62269,O00148,O95071,P53396,Q2PZI1,P50402,Q00839,Q8IXH7,Q8TEQ8,O14656,P10415,P05556,Q13561,Q13155,P39019,Q8TDD1,Q13011,O60884,Q9BZQ6,Q07820,P29122,Q13352,Q4KMQ2,Q9NZJ7,Q9P2E5,O60684,P11940,Q07817,P62258,P61254,Q9NU22,P38159,Q8NC51,Q9UM54,P35998,Q96A65,Q14974,O43633,P12956,P09874,P16615,P35222,P62195,P30876,Q02224,Q7KZF4,P27348,Q15311,P35232,Q9Y673,Q6PKG0,Q12789,P11021,Q5JWF2,O94826,P35613,Q14152,P07195,Q6DN90,P06576,P50395,P09603,P33527,O75400,Q9Y3U8,P53618,P07355,O14745,P31689,Q9H488,Q99519,Q9UL15,P39656,P30153,Q9Y6E2,Q99460,Q96S66,P11586,P27487,O75592,Q9H307,O00303,Q9Y265,P05023,P09651,P05026,P51570,Q96CW5,Q9UHI6,P04439,Q99470,Q7Z2K6,Q96S52,P26599,P20700,P47756,Q00325,P15692,P51665,P61158,P62191,P19338,Q15365,P27105,P06493,P33176,Q8IXB1,O95373,P46379,O15439,Q14739,P52292,O00429,P38919,O43765,P46782,Q99623,Q16891,P60866,Q9NR30,Q9C0B5,P60660,P13010,P57678,P25205,P08708,Q6P2E9,P62937,Q99615,Q8NEW0,O60762,P38435,P12109,Q9UNM6,O14980,P25705,Q7KZI7,Q9Y4L1,O43852,O00231,Q08431,P49069,Q6NXT4,Q10589\nnucleoplasm\tP48556,Q13200,Q13769,Q5H9R7,Q3ZCQ8,P53778,P62753,Q16630,Q9NTJ3,P0DP23,Q9BSJ2,P62269,O00148,O95071,P53396,Q7L590,Q00839,Q8IXH7,Q96J01,Q86WX3,Q13151,Q9BRP8,P39019,P39880,Q6NZY4,Q86W42,Q07820,O60885,Q13352,O60684,Q6IEG0,Q9UKV3,P52630,P67809,Q15008,Q9H936,P38159,Q9UM54,P35998,Q00403,Q9Y580,Q14974,P26358,Q14160,P12956,Q92793,P09622,Q96FV9,Q15303,P09874,P35222,P62195,P78563,P30876,P35232,Q6PKG0,Q12789,Q96MM7,Q92900,P63279,O94762,P63104,Q8WX92,Q86VM9,Q9BUQ8,Q92994,Q8N5P1,Q9H3P2,Q9NVW2,P63165,Q14653,Q13838,Q9NQG5,O43818,Q9BXP5,Q9H814,O95347,Q9Y466,O75400,O43592,Q9UER7,P61326,Q09161,P42224,Q99460,Q16576,P18615,Q6PJT7,Q9NWV8,Q5VV67,Q9H307,Q9Y265,P09651,O00629,P52294,Q9BQ95,O95619,P62140,P52298,Q8N0Z6,P49756,Q9UHI6,Q96RT1,P14735,P24928,P26599,P20700,O14818,P35637,Q9Y5S9,Q8NI27,P51665,Q96HW7,P60900,P62191,P19338,Q15365,P19784,Q7Z4H7,P25787,P67870,P06493,Q9ULX6,P68400,O00422,O95373,P08107,P83916,P46379,Q8NEM0,P52292,O75934,P98179,P38919,P12004,P05161,P46782,Q86U42,P60866,Q6I9Y2,Q9UKS6,Q9NR30,Q8TAD8,P13010,P57678,O00505,P52948,P25205,P08708,Q6P2E9,Q99615,Q14011,Q9UNM6,O15397,O14980,P0DMV8,P42677,P08621,Q86U32,O00231\npseudopodium\tP12814,Q08043,P55085,O43707,P35609\nbrush border\tP12814,P19105,Q08043,P47756,P60660,O00429,O43707,P61158,O14950\nnuclear matrix\tO95619,P20700,O75400,O43592,P60900,Q9ULX6,Q96FV9,Q9Y265,Q99623,Q13838\nzona pellucida receptor complex\tP49368,P17987,P48643,Q99832\nextracellular matrix\tP07355,P60866,P40227,P04406,P15924,P60660,P39019,P29122,P08708,P06576,P00750,P12109,P62269,P40939,P25705,P11021,Q00839,Q08431,P46782\ncytosolic small ribosomal subunit\tP60866,P08708,P62269,P39019,P42677,P62753,P46782\nactin filament\tP12814,Q08043,Q9H270,P47756,Q7KZI7,Q96EY1,P13797,P35609\ncytosolic proteasome complex\tP35998,P62191,P14735,P62195\nsarcolemma\tP07355,Q14315,P05556,P12109,Q14524,P35613,P05023,P05026,Q12959\nvesicle\tP07355,P0DP23,P27105,P33176,P50395,O14745,Q8NEW0,P04406,Q13561,Q9ULV4,P60660\nplatelet alpha granule lumen\tP12814,P05155,P02768,P15692,O43707,P00747,P35609\nperinuclear region of cytoplasm\tP07355,Q9UM54,P27105,O14745,P04406,P31689,P42224,Q9UL15,O76061,P62753,P09601,P16615,P35222,P33176,Q9NV92,P08107,O00429,O43707,Q5JWF2,P05556,Q99569,P25205,Q8NEW0,P09603,P22460,P35637,P0DMV8,P63244,Q9P289,Q8NC51,Q12959\nspliceosomal complex\tP49756,O00148,O75934,P08621,Q9NW13,Q9UKA9,O75940,P09651,Q13838\nendoplasmic reticulum quality control compartment\tQ13438,Q96DZ1,Q9BZQ6,Q9NYU1\nmembrane raft\tP07355,P27105,O14745,P22460,P37173,P27487,P05556,P35613,P31431,P35052,Q9BYF1,P07195,Q10589,Q12959\nZ disc\tQ14315,P12814,P19105,P22460,Q08043,P47756,Q14524,O43707,P35222,P35609,O14950\nCOPI vesicle coat\tP53621,Q9UBF2,P35606,P53618\nsmall ribosomal subunit\tP60866,P62269,P63244,P62753,P46782\nmitochondrial nucleoid\tP06576,Q9NVI7,Q7L8L6,P40939,P34897,Q96EY1\ncell surface\tP07355,P05556,Q96PE1,Q14524,P08581,P04439,Q9BYF1,P29122,P14735,Q4KMQ2,Q13443,P06576,P00750,P35232,P27487,P15151,P31431,P11021,P15692,Q13224,Q00839,P00747,Q10589,P34741,Q9BS26,Q99623\nCRD-mediated mRNA stability complex\tP67809,O75534,Q00839\nendoplasmic reticulum-Golgi intermediate compartment\tO95070,P11021,Q9Y3A6,P15144,Q9NYU1,Q9BS26,P53618\nmicrotubule\tP40227,Q5SW79,Q96CW5,Q7Z4H7,P17987,Q13561,Q14152,Q99832,P49368,P33176,Q02224,Q9BSJ2,P48643,O00429,O60333,P50402,Q12959\nendocytic vesicle\tQ9UM54,P33176,Q9H270,P27487,P35247,P51148\ncytoplasmic stress granule\tP11940,Q14011,Q13310,Q13283,P67809\nproteasome regulatory particle,lid subcomplex\tP48556,Q9UNM6,O00231\nproteasome core complex,alpha-subunit complex\tP25787,O14818,P60900\nnuclear proteasome complex\tP35998,P62191,P62195\nmitochondrial matrix\tQ96EY1,P34897,O75439,Q07820,Q15303,P09622,Q8IZ52,Q9UDR5,Q07817,P06576,P11310,Q5JRX3,Q9BW92,Q10713,P25705,P13804,Q6L8Q7\ncytoplasmic mRNA processing body\tP35998,Q6P2E9,P25787,Q9Y5A9,P60900,Q9HCE1,Q92900\nlysosomal membrane\tP07355,Q9UM54,Q8IWA5,O75348,Q99519,O00203,P22314,Q96JC1,P51148,B7Z2Y1,P12109,P27487,Q9H270,P38606,P15144\nintracellular membrane-bounded organelle\tP35998,P04406,O14745,Q99519,P39656,Q96EY1,Q96S66,O14964,Q9NV92,P46379,O00429,O95070,Q9Y265,P05023,Q8N0Z8,P25205,P51148,Q9UDR5,Q6P2E9,O60762,Q9H814,P67809,O14980,Q15365,P53618\nmitochondrial outer membrane\tQ07817,Q16548,O94826,Q92843,O00429,Q86UT6,Q07820,Q99623,P10415,Q8N0X7\nintegral component of endoplasmic reticulum membrane\tQ9NZ01,O00124,Q14739,P11021,P57088,Q9P035,Q9NQC3,O76024\ndesmosome\tP15924,Q99569,P22314,Q9H307\ncentrosome\tQ6NXT6,O14745,Q5SW79,Q96CW5,Q8TF76,Q7Z4H7,P17987,Q13561,P06493,P35222,P25205,Q9NQG5,P0DP23,Q07817,Q9H814,Q9BSJ2,P48643,Q8IWR1,Q9P289,P12004\nfascia adherens\tP12814,P15924,P35222\ncytoplasmic ribonucleoprotein granule\tP11940,P62753,Q00839,P19338\ncaveola\tP22460,P37173,Q14524,P09601,P05023,P05026\nendoplasmic reticulum chaperone complex\tQ8IXB1,P11021,Q9Y4L1\nAP-3 adaptor complex\tQ9H270,O00203,Q96JC1\nDNA-directed RNA polymerase II,holoenzyme\tQ9NQG5,Q9UM54,O94762\nnuclear euchromatin\tP16402,P35222,P24928,P38159\nPcG protein complex\tP19784,Q9BSM1,P67870,P68400\nruffle\tP07355,P12814,Q5JWF2,Q9UM54,O14745,P05556,Q14155\nribosome\tP60866,P08708,Q86WX3,Q92552,P62269,P39019,P42677,P61927,P62753,Q9Y3U8\ntransport vesicle\tQ5JWF2,P53621,Q9UBF2,Q9H8W4,P35606,Q15818,P53618\ncell-cell junction\tP07355,Q9NYL9,P12814,Q99569,Q14160,Q9H307,O43707,P61158,P35222,Q12959\nRNA cap binding complex\tQ09161,Q53F19\nScrib-APC-beta-catenin complex\tQ14160,P35222\nproteasome storage granule\tQ13200,Q99460\n', - }, - ], - }, - gallus: { - title: 'Gallus gallus (BioGRID)', - source: 'BioGRID database', - link: null, - files: [ - { - kind: 'network', - name: 'Gallus gallus BioGRID', - fileName: 'Biogrid_no_self_loops.txt', - text: 'Source\tTarget\nP62760\tP60706\nP62764\tP60706\nO15392\tP53352\nQ9IAY5\tP49024\nQ9IAY5\tP49024\nP49024\tQ9IAY5\nQ13625\tP46936\nO93512\tO60542\nP41238\tQ7T2T1\nO42414\tQ9JI92\nO42414\tQ9JI92\nP05556\tQ00944\nP05556\tP49024\nP27986\tQ9DDT2\nP28497\tP23297\nP49024\tP12003\nP49024\tQ00944\nQ4KWZ7\tQ9DEA3\nQ9DEA3\tQ4KWZ7\nP53478\tQ9UUJ1\nP53478\tP32390\nP53478\tP78929\nP12003\tP60010\nP79987\tP56517\nP79987\tP56519\nP79987\tQ9W7I5\nP79987\tQ3C1E9\nQ9W7I5\tP79987\nP56517\tP79987\nP56519\tP79987\nQ5ZJY5\tO93257\nQ5R1T0\tP56517\nQ5R1T0\tP56519\nQ5R1T0\tP56517\nQ5R1T0\tP56519\nQ5R1T0\tP56520\nQ09472\tP17678\nP23204\tP17678\nP23204\tP23824\nP23204\tP23825\nQ92993\tP70082\nQ92993\tP0C1H3\nQ92993\tP84247\nQ92993\tP62801\nP56519\tQ9HAZ2\nO18738\tP31696\nQ8AYS7\tQ1T7B8\nQ8AYS7\tQ1T7C0\nQ8AYS7\tQ1T7B7\nQ8AYS7\tQ1T7C1\nQ8AYS7\tQ1T7B9\nQ90ZF9\tQ1T7B8\nQ90ZF9\tQ1T7C0\nQ90ZF9\tQ1T7B7\nQ90ZF9\tQ1T7C1\nQ90ZF9\tQ1T7B9\nQ76I90\tQ76I89\nQ76I89\tQ76I90\nQ8AYS8\tP09572\nQ8AYS8\tP11501\nQ8AYS8\tQ90593\nQ8AYS8\tP16053\nQ8AYS8\tP08106\nQ8AYS8\tO73885\nQ8AYS8\tQ5ZHY5\nQ8AYS8\tQ5ZL72\nQ8AYS8\tP09207\nQ8AYS8\tQ5ZLC5\nQ8AYS8\tP05094\nQ8AYS8\tP00508\nQ8AYS8\tP54097\nQ8AYS8\tP17153\nQ8AYS8\tQ5ZMT0\nQ8AYS8\tP28497\nQ8AYS8\tP62207\nQ8AYS8\tQ5ZKC9\nQ8AYS8\tQ5F3W6\nQ8AYS8\tP60878\nQ8AYS8\tQ5ZM44\nQ8AYS8\tP08250\nQ8AYS8\tQ5ZMB2\nQ8AYS8\tQ00649\nQ8AYS8\tQ5ZKK4\nQ8AYS8\tQ5ZIV5\nQ8AYS8\tQ5ZMD1\nQ8AYS8\tQ5ZLQ6\nQ8AYS8\tP07090\nQ8AYS8\tP81628\nQ8AYS8\tP42324\nQ8AYS8\tP62758\nQ8AYS8\tP62764\nQ8AYS8\tP08110\nQ8AYS8\tO93510\nQ8AYS8\tP13731\nQ8AYS8\tQ5F425\nQ8AYS8\tP18359\nQ8AYS8\tP02789\nQ8AYS8\tQ5ZL57\nQ8AYS8\tP19121\nQ8AYS8\tO57391\nQ8AYS8\tQ9PTG6\nQ8AYS8\tP63270\nQ8AYS8\tP51913\nQ8AYS8\tP05122\nQ8AYS8\tQ5ZME2\nQ8AYS8\tQ5ZHP5\nQ8AYS8\tP48463\nQ8AYS8\tP04354\nQ8AYS8\tP50890\nQ8AYS8\tP05419\nQ8AYS8\tP80566\nQ8AYS8\tP31395\nQ8AYS8\tP00940\nQ8AYS8\tQ5ZLN1\nQ8AYS8\tQ5ZJF4\nQ8AYS8\tP00340\nQ8AYS8\tP09654\nQ8AYS8\tO42163\nQ8AYS8\tP16580\nQ8AYS8\tP02112\nQ8AYS8\tP80026\nQ8AYS8\tQ5ZLG1\nQ8AYS8\tO57535\nQ8AYS8\tQ07212\nQ8AYS8\tO13268\nQ8AYS8\tP00337\nQ8AYS8\tP00356\nQ8AYS8\tP51903\nQ8AYS8\tP07341\nQ8AYS8\tP00548\nQ8AYS8\tP14732\nQ8AYS8\tP13648\nQ8AYS8\tQ5F3W6\nQ8AYS8\tP42324\nQ8AYS8\tP60878\nQ8AYS8\tP17153\nQ8AYS8\tQ5ZL72\nQ8AYS8\tQ5F425\nQ8AYS8\tP80566\nQ8AYS8\tQ01406\nQ8AYS8\tP08106\nQ8AYS8\tP31395\nQ8AYS8\tQ90593\nQ8AYS8\tQ04619\nQ8AYS8\tP14731\nQ5F3W6\tQ8AYS8\nP42324\tQ8AYS8\nP60878\tQ8AYS8\nP17153\tQ8AYS8\nQ5ZL72\tQ8AYS8\nQ5F425\tQ8AYS8\nP80566\tQ8AYS8\nQ01406\tQ8AYS8\nP08106\tQ8AYS8\nP31395\tQ8AYS8\nQ90593\tQ8AYS8\nQ04619\tQ8AYS8\nP14731\tQ8AYS8\nP84023\tP49140\nP84023\tP83038\nP38531\tP04637\nP38531\tP06876\nP38531\tP01103\nF1N9Y5\tP00548\nF1N9Y5\tP09244\nF1N9Y5\tO73885\nF1N9Y5\tQ5ZL72\nF1N9Y5\tP11501\nF1N9Y5\tQ90705\nF1N9Y5\tQ5ZLC5\nF1N9Y5\tP51913\nF1N9Y5\tQ5ZM98\nF1N9Y5\tP38024\nF1N9Y5\tP47826\nF1N9Y5\tP08070\nF1N9Y5\tQ90593\nF1N9Y5\tP15771\nF1N9Y5\tQ9IAY5\nF1N9Y5\tP50890\nF1N9Y5\tQ5ZJK8\nF1N9Y5\tP51903\nF1N9Y5\tP67883\nF1N9Y5\tP00340\nF1N9Y5\tQ6EE31\nF1N9Y5\tP22451\nF1N9Y5\tQ8UVD9\nF1N9Y5\tQ5ZJU3\nF1N9Y5\tO57535\nF1N9Y5\tQ5ZLP8\nF1N9Y5\tQ04619\nF1N9Y5\tQ5ZIQ3\nF1N9Y5\tP47836\nF1N9Y5\tQ5ZKC9\nF1N9Y5\tQ5ZK03\nF1N9Y5\tQ8AYC9\nF1N9Y5\tP00356\nF1N9Y5\tQ5ZLN4\nF1N9Y5\tQ5ZJ56\nF1N9Y5\tP18660\nF1N9Y5\tQ5ZMT0\nF1N9Y5\tQ8JFP1\nF1N9Y5\tP32429\nF1N9Y5\tP16039\nF1N9Y5\tP00337\nF1N9Y5\tP08106\nF1N9Y5\tQ5ZMS3\nF1N9Y5\tP61355\nF1N9Y5\tQ5ZJ54\nF1N9Y5\tP63247\nF1N9Y5\tQ5ZKA5\nF1N9Y5\tP08629\nF1N9Y5\tP16527\nF1N9Y5\tQ5ZMN2\nF1N9Y5\tQ5ZJZ5\nF1N9Y5\tP18359\nF1N9Y5\tQ5ZKF5\nF1N9Y5\tQ5ZLN1\nF1N9Y5\tQ9YGC1\nF1N9Y5\tQ5ZLE6\nF1N9Y5\tQ8UW59\nF1N9Y5\tQ5ZKB9\nF1N9Y5\tP13863\nF1N9Y5\tQ90WU3\nF1N9Y5\tQ5ZI72\nF1N9Y5\tQ5ZIA5\nF1N9Y5\tP11029\nF1N9Y5\tQ98TF8\nF1N9Y5\tQ5ZL42\nF1N9Y5\tQ5ZKC1\nF1N9Y5\tP17790\nF1N9Y5\tQ5ZMN3\nF1N9Y5\tQ98TF6\nF1N9Y5\tQ5ZLC6\nF1N9Y5\tP24367\nF1N9Y5\tP11799\nF1N9Y5\tQ5ZK62\nF1N9Y5\tP26584\nF1N9Y5\tP14315\nF1N9Y5\tQ5ZKG5\nF1N9Y5\tP42558\nF1N9Y5\tP41239\nF1N9Y5\tP05419\nF1N9Y5\tP43347\nF1N9Y5\tP63283\nF1N9Y5\tQ5ZJN4\nF1N9Y5\tQ5ZL57\nF1N9Y5\tQ8JG64\nF1N9Y5\tO42392\nF1N9Y5\tQ5ZK01\nF1N9Y5\tP19179\nF1N9Y5\tO93256\nF1N9Y5\tQ02391\nP28683\tP62140\nQ6A078\tP00698\nP68400\tO13016\nP12931\tO13016\nP07812\tQ04619\nP07812\tP08106\nP62207\tQ90623\nQ90ZF9\tQ76I89\nQ90ZF9\tQ76I90\nQ90ZF9\tQ76I89\nP00523\tP19785\nP11501\tQ5ZL72\nP11501\tP08106\nQ04619\tQ5ZL72\nQ04619\tP08106\nP68399\tP01109\nP67868\tP01109\nQ12341\tP02263\nQ12341\tP62801\nQ5ZL72\tP08106\nQ5ZL72\tQ04619\nQ04619\tP08106\nQ06592\tP62801\nQ06592\tP84247\nP39979\tP62801\nQ3C1E9\tP79987\nQ3C1E9\tQ5R1S9\nQ3C1E9\tP84247\nQ3C1E9\tP62801\nP07812\tQ15185\nP07812\tP07900\nP07812\tP34932\nP07812\tP25685\nP07812\tP31948\nP25293\tP70082\nP25293\tP0C1H3\nP25293\tP84247\nP25293\tP62801\nQ702N8\tP68139\nA4UGR9\tP68139\nP68139\tP42639\nP42684\tQ04929\nQ92831\tP02263\nQ92831\tP62801\nQ9DEA3\tQ4KWZ7\nQ02842\tQ5ZJL9\nQ15185\tP11501\nP00523\tP51913\nQ04982\tQ5ZIK9\nQ04982\tP48463\nQ04982\tQ5ZM98\nQ04982\tP67869\nQ04982\tO73885\nQ04982\tQ5ZMN3\nQ04982\tP16039\nQ04982\tP42558\nQ04982\tQ6U7I1\nQ04982\tP05625\nQ04982\tQ5ZMD1\nQ04982\tQ5ZLQ6\nQ04982\tQ5ZMT0\nQ04982\tQ5ZKC9\nQ04982\tP50147\nQ04982\tQ5F3W6\nQ04982\tO57476\nQ04982\tP11501\nQ04982\tQ90828\nQ04982\tQ90891\nQ04982\tQ90593\nQ04982\tP08110\nP18652\tO95071\nO42414\tP16092\nP16092\tO42414\nP61088\tQ9DEA3\nQ15819\tQ9DEA3\nQ9W7C5\tQ401C0\nQ401C0\tQ9W7C5\nP11275\tP13387\nQ7T0L4\tQ9PU53\nQ07496\tP32004\n', - }, - { - kind: 'annotation', - name: 'Gallus gallus KEGG pathways', - fileName: 'BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt', - text: 'ErbB signaling pathway\tQ00944,Q04929,P00523,P01109,Q90891,P13387,Q9W7C5,P05625,Q04982\nCarbon metabolism\tP00356,O57391,P00548,P07341,P51903,Q5ZLN1,Q5ZME2,P51913,P00940,P00508\nBiosynthesis of antibiotics\tP00356,O57391,P00548,P07341,O57535,P51903,Q5ZLN1,P00337,Q5ZME2,P51913,P00940,P00508,P00340,P38024\nGap junction\tP00523,P09244,Q90891,P08070,P13387,P50147,P09207,P05625,P13863\nRibosome\tQ98TF6,Q98TF8,P67883,P32429,P61355,P47826,Q5ZJ56,P47836,P50890,P18660,P22451\nFocal adhesion\tQ00944,Q04929,P00523,Q90623,P13387,P05094,P60706,P53478,P11799,P49024,P12003,Q04982,P62207,P05625\nProgesterone-mediated oocyte maturation\tQ04619,P07812,P18652,P11501,P50147,P05625,Q04982,P13863\nVascular smooth muscle contraction\tQ8AYS8,P11799,Q90623,P05419,P63270,Q90891,P62207,P05625,Q04982\nProtein processing in endoplasmic reticulum\tQ04619,Q5ZKF5,Q90593,Q8JG64,O73885,P11501,P08110,Q5ZHY5,P81628,Q5ZK03,P08106\nPyruvate metabolism\tP00548,P00337,Q5ZME2,P11029,P00340\nVEGF signaling pathway\tQ00944,P00523,P49024,Q90891,P05625,Q00649\nInsulin signaling pathway\tQ04929,P05419,Q90891,P11029,P62207,P05625,Q04982,O13016\nTight junction\tP00523,P48463,P05094,P60706,P53478,Q01406', - }, - ], - }, - 'docs-tp53': { - title: 'TP53 (NORMA documentation)', - source: 'Example from the NORMA help pages', - link: null, - files: [ - { - kind: 'network', - name: 'TP53 docs network', - fileName: '', - text: 'Source\tTarget\tWeight\nCDKN1A\tTP53\t5\nTP53\tMDM2\t1\nMDM4\tTP53\t3\nBCL2L1\tTP53\t4\nCHEK2\tATM\t2\nTP53\tEP300\t1\nATM\tTP53\t4\nTP53\tCREBBP\t1\nMDM4\tMDM2\t1\nCHEK2\tTP53\t2\nTP53BP2\tTP53\t8\nCDKN2A\tTP53\t3\nCDKN2A\tMDM2\t3\nATM\tMDM2\t1\nEP300\tCREBBP\t2\n', - }, - { - kind: 'annotation', - name: 'TP53 docs groups', - fileName: '', - text: 'Group-2\tBCL2L1,MDM4,MDM2,CHEK2\nGroup-5\tTP53,EP300\nGroup-1\tCDKN2A,ATM,TP53BP2,MDM2\nGroup-4\tCHEK2,CREBBP,MDM2\nGroup-3\tTP53,BCL2L1\nGroup-6\tMDM4,MDM2\n', - }, - { - kind: 'colors', - name: 'TP53 docs expression', - fileName: '', - text: 'CDKN1A\tblue\nTP53\tblue\nMDM4\t#00ff00\nBCL2L1\tred\nCHEK2\tred\nATM\tred\nTP53BP2\tred\nCDKN2A\tblue\nEP300\t#ffff00\nCREBBP\tred\nMDM2\tblue\n', - }, - ], - }, -} - -/* ============================================================ - TABS AND HELP LINKS - ============================================================ */ -export const TAB_VIEWS = { - welcome: 'welcomeView', - network: 'canvas', - network3d: 'canvas', - profiler: 'profilerView', - compare: 'compareView', - help: 'helpView', - about: 'aboutView', - api: 'apiView', -} - -export let currentTab = 'welcome' - -export function switchTab(name, helpAnchor) { - if (!TAB_VIEWS[name]) return - currentTab = name - document.querySelectorAll('#tabs .tab').forEach((btn) => { - const on = btn.dataset.tab === name - btn.setAttribute('aria-selected', on ? 'true' : 'false') - btn.tabIndex = on ? 0 : -1 - }) - const onNetwork = name === 'network' || name === 'network3d' - document.getElementById('canvas').classList.toggle('inactive', !onNetwork) - if (typeof setMode3d === 'function' && (name === 'network3d') !== net3d.active) - setMode3d(name === 'network3d') - document.getElementById('welcomeView').classList.toggle('active', name === 'welcome') - document.getElementById('profilerView').classList.toggle('active', name === 'profiler') - if (name === 'profiler' && typeof refreshEnrichmentChoices === 'function') - refreshEnrichmentChoices() - document.getElementById('compareView').classList.toggle('active', name === 'compare') - document.getElementById('helpView').classList.toggle('active', name === 'help') - document.getElementById('aboutView').classList.toggle('active', name === 'about') - document.getElementById('apiView').classList.toggle('active', name === 'api') - if (name === 'network') { - document.getElementById('cy').style.visibility = '' - cy.resize() - resizeHullCanvas() - drawGroupHulls() - } - if (name === 'profiler') renderProfilerNetworkList() - if (name === 'compare') renderCompareList() - if ((name === 'help' || name === 'about' || name === 'api') && helpAnchor) { - const target = document.getElementById(helpAnchor) - if (target) { - target.scrollIntoView({ block: 'start' }) - target.classList.remove('flash') - void target.offsetWidth - target.classList.add('flash') - } - } -} - -/* ============================================================ - NETWORK PROFILER - Topology statistics following igraph's definitions, computed on a - simple undirected graph (self-loops and parallel edges set aside). - Path-based measures use one BFS per node (Brandes' algorithm for - betweenness), so they are skipped above PROFILE_PATH_LIMIT nodes. - ============================================================ */ -export const PROFILE_PATH_LIMIT = 8000 - -export const PROFILE_STATS = [ - { key: 'n', label: 'Nodes', igraph: 'vcount(g)', desc: 'Number of nodes.' }, - { - key: 'm', - label: 'Edges', - igraph: 'ecount(g)', - desc: 'Connections between distinct node pairs.', - }, - { - key: 'density', - label: 'Density', - igraph: 'edge_density(g)', - desc: 'Share of all possible node pairs that are connected.', - }, - { - key: 'avgDegree', - label: 'Average degree', - igraph: 'mean(degree(g))', - desc: 'Mean number of neighbors per node.', - }, - { - key: 'maxDegree', - label: 'Maximum degree', - igraph: 'max(degree(g))', - desc: 'Most neighbors of any node.', - }, - { - key: 'isolated', - label: 'Isolated nodes', - igraph: 'sum(degree(g) == 0)', - desc: 'Nodes without any connection.', - }, - { - key: 'components', - label: 'Connected components', - igraph: 'count_components(g)', - desc: 'Separate pieces of the network.', - }, - { - key: 'largest', - label: 'Largest component', - igraph: 'max(components(g)$csize)', - desc: 'Nodes in the biggest piece, with its share of all nodes.', - }, - { - key: 'diameter', - label: 'Diameter', - igraph: 'diameter(g)', - desc: 'Longest shortest path between any two connected nodes.', - }, - { - key: 'radius', - label: 'Radius', - igraph: 'radius(g)', - desc: 'Smallest eccentricity of any node.', - }, - { - key: 'avgPath', - label: 'Average path length', - igraph: 'mean_distance(g)', - desc: 'Mean shortest-path length over all connected node pairs.', - }, - { - key: 'avgEcc', - label: 'Average eccentricity', - igraph: 'mean(eccentricity(g))', - desc: 'Mean, over nodes, of the distance to the farthest reachable node.', - }, - { - key: 'transitivity', - label: 'Clustering coefficient', - igraph: 'transitivity(g, "global")', - desc: 'Share of connected triples that close into triangles.', - }, - { - key: 'avgLocal', - label: 'Average local clustering', - igraph: 'transitivity(g, "average")', - desc: "Mean of each node's own clustering coefficient, over nodes with two or more neighbors.", - }, - { - key: 'triangles', - label: 'Triangles', - igraph: 'sum(count_triangles(g)) / 3', - desc: 'Number of three-node cycles.', - }, - { - key: 'assortativity', - label: 'Degree assortativity', - igraph: 'assortativity_degree(g)', - desc: 'Above 0: hubs link to hubs. Below 0: hubs link to sparsely connected nodes.', - }, - { - key: 'centrDegree', - label: 'Degree centralization', - igraph: 'centr_degree(g, loops = FALSE)$centralization', - desc: 'How strongly connections concentrate on one node (1 for a star).', - }, - { - key: 'centrBetw', - label: 'Betweenness centralization', - igraph: 'centr_betw(g)$centralization', - desc: 'How strongly shortest paths run through one node.', - }, - { - key: 'centrClo', - label: 'Closeness centralization', - igraph: 'centr_clo(g)$centralization', - desc: 'How much one node is closer to all others than the rest are.', - }, - { - key: 'modularity', - label: 'Modularity (Louvain)', - igraph: 'modularity(cluster_louvain(g))', - desc: 'How well the network splits into dense communities; above about 0.3 suggests clear structure.', - }, - { - key: 'communities', - label: 'Communities (Louvain)', - igraph: 'length(cluster_louvain(g))', - desc: 'Number of communities Louvain finds, including single nodes.', - }, - { - key: 'setAside', - label: 'Set aside before profiling', - igraph: 'simplify(g)', - desc: 'Self-loops and parallel edges not counted as connections.', - }, -] - -export function formatStat(v) { - if (v === null || v === undefined || (typeof v === 'number' && Number.isNaN(v))) return '—' - if (typeof v === 'string') return v - if (!Number.isFinite(v)) return '—' - if (Number.isInteger(v)) return v.toLocaleString('en-US') - const abs = Math.abs(v) - if (abs !== 0 && abs < 0.001) return v.toExponential(2) - return (+v.toFixed(4)).toString() -} - -// ids: node names; pairs: [source, target] per edge -export function simpleGraph(ids, pairs) { - const index = new Map(ids.map((id, i) => [id, i])) - const n = ids.length - const sets = Array.from({ length: n }, () => new Set()) - let loops = 0, - parallel = 0 - pairs.forEach(([s, t]) => { - const a = index.get(s), - b = index.get(t) - if (a === undefined || b === undefined) return - if (a === b) { - loops++ - return - } - if (sets[a].has(b)) { - parallel++ - return - } - sets[a].add(b) - sets[b].add(a) - }) - const adj = sets.map((x) => Int32Array.from(x)) - let m = 0 - adj.forEach((a) => { - m += a.length - }) - return { ids, n, m: m / 2, adj, loops, parallel } -} - -export function profileGraph(g) { - const { n, m, adj } = g - const deg = adj.map((a) => a.length) - const r = { n, m, degree: deg } - r.density = n > 1 ? (2 * m) / (n * (n - 1)) : NaN - r.avgDegree = n ? (2 * m) / n : NaN - r.maxDegree = n ? Math.max(...deg) : NaN - r.isolated = deg.filter((d) => d === 0).length - r.setAside = - g.loops || g.parallel - ? [ - g.loops ? plural(g.loops, 'self-loop') : '', - g.parallel ? plural(g.parallel, 'parallel edge') : '', - ] - .filter(Boolean) - .join(', ') - : 'none' - - // connected components - const comp = new Int32Array(n).fill(-1) - const queue = new Int32Array(Math.max(1, n)) - const compSizes = [] - for (let s = 0; s < n; s++) { - if (comp[s] >= 0) continue - const c = compSizes.length - let head = 0, - tail = 0 - queue[tail++] = s - comp[s] = c - while (head < tail) { - const v = queue[head++] - const nb = adj[v] - for (let k = 0; k < nb.length; k++) { - const w = nb[k] - if (comp[w] < 0) { - comp[w] = c - queue[tail++] = w - } - } - } - compSizes.push(tail) - } - r.components = compSizes.length - const largest = compSizes.length ? Math.max(...compSizes) : 0 - r.largest = n ? `${largest.toLocaleString('en-US')} (${((100 * largest) / n).toFixed(1)}%)` : '—' - - // triangles and clustering - const tri = new Float64Array(n) - const mark = new Int32Array(n).fill(-1) - for (let u = 0; u < n; u++) { - const nu = adj[u] - for (let k = 0; k < nu.length; k++) mark[nu[k]] = u - for (let k = 0; k < nu.length; k++) { - const v = nu[k] - if (v <= u) continue - const nv = adj[v] - for (let q = 0; q < nv.length; q++) { - const w = nv[q] - if (w > v && mark[w] === u) { - tri[u]++ - tri[v]++ - tri[w]++ - } - } - } - } - let triSum = 0, - triples = 0, - localSum = 0, - localCount = 0 - const localClustering = new Float64Array(n) - for (let v = 0; v < n; v++) { - const d = deg[v] - triSum += tri[v] - const t = (d * (d - 1)) / 2 - triples += t - if (d >= 2) { - localClustering[v] = tri[v] / t - localSum += localClustering[v] - localCount++ - } - } - r.triangles = triSum / 3 - r.transitivity = triples > 0 ? triSum / triples : NaN - r.avgLocal = localCount ? localSum / localCount : NaN - r.localClustering = localClustering - - // degree assortativity (Newman), over each undirected edge once - if (m > 0) { - let sProd = 0, - sMean = 0, - sSq = 0 - for (let u = 0; u < n; u++) { - const nu = adj[u] - for (let k = 0; k < nu.length; k++) { - const v = nu[k] - if (v <= u) continue - const j = deg[u], - q = deg[v] - sProd += j * q - sMean += (j + q) / 2 - sSq += (j * j + q * q) / 2 - } - } - const mean = sMean / m - const denom = sSq / m - mean * mean - r.assortativity = Math.abs(denom) > 1e-12 ? (sProd / m - mean * mean) / denom : NaN - } else { - r.assortativity = NaN - } - - r.centrDegree = n > 2 ? deg.reduce((s, d) => s + (r.maxDegree - d), 0) / ((n - 1) * (n - 2)) : NaN - - // shortest paths, eccentricity, closeness, betweenness - if (n <= PROFILE_PATH_LIMIT) { - const dist = new Int32Array(n).fill(-1) - const sigma = new Float64Array(n) - const delta = new Float64Array(n) - const order = new Int32Array(Math.max(1, n)) - const betw = new Float64Array(n) - const ecc = new Float64Array(n) - const closeness = new Float64Array(n).fill(NaN) - let pathSum = 0, - pathCount = 0, - diameter = 0 - for (let s = 0; s < n; s++) { - let head = 0, - tail = 0 - order[tail++] = s - dist[s] = 0 - sigma[s] = 1 - let far = 0, - sum = 0 - while (head < tail) { - const v = order[head++] - const dv = dist[v] - if (dv > far) far = dv - sum += dv - const nb = adj[v] - for (let k = 0; k < nb.length; k++) { - const w = nb[k] - if (dist[w] < 0) { - dist[w] = dv + 1 - order[tail++] = w - } - if (dist[w] === dv + 1) sigma[w] += sigma[v] - } - } - const reached = tail - 1 - ecc[s] = far - if (far > diameter) diameter = far - if (reached > 0) { - closeness[s] = reached / sum - pathSum += sum - pathCount += reached - } - for (let i = tail - 1; i >= 0; i--) { - const w = order[i] - const nb = adj[w] - for (let k = 0; k < nb.length; k++) { - const v = nb[k] - if (dist[v] === dist[w] - 1) delta[v] += (sigma[v] / sigma[w]) * (1 + delta[w]) - } - if (w !== s) betw[w] += delta[w] - } - for (let i = 0; i < tail; i++) { - const v = order[i] - dist[v] = -1 - sigma[v] = 0 - delta[v] = 0 - } - } - for (let v = 0; v < n; v++) betw[v] /= 2 - r.betweenness = betw - r.closeness = closeness - r.diameter = n ? diameter : NaN - r.radius = n ? Math.min(...ecc) : NaN - r.avgEcc = n ? ecc.reduce((a, b) => a + b, 0) / n : NaN - r.avgPath = pathCount ? pathSum / pathCount : NaN - const maxB = n ? Math.max(...betw) : 0 - r.centrBetw = - n > 2 ? betw.reduce((s, b) => s + (maxB - b), 0) / (((n - 1) * (n - 1) * (n - 2)) / 2) : NaN - const finiteClo = [...closeness].filter(Number.isFinite) - const maxC = finiteClo.length ? Math.max(...finiteClo) : NaN - r.centrClo = - n > 2 && finiteClo.length - ? finiteClo.reduce((s, c) => s + (maxC - c), 0) / (((n - 1) * (n - 2)) / (2 * n - 3)) - : NaN - } else { - r.pathsSkipped = true - ;['diameter', 'radius', 'avgEcc', 'avgPath', 'centrBetw', 'centrClo'].forEach((k) => { - r[k] = NaN - }) - } - - const lv = louvain(g) - r.louvain = lv - r.modularity = lv.modularity - r.communities = lv.count - return r -} - -// Louvain community detection (Blondel et al. 2008), unweighted input, -// nodes visited in a fixed order so results are repeatable. -export function louvain(g, gamma = 1) { - const n = g.n - const twoM = 2 * g.m - if (!twoM) { - return { - membership: Int32Array.from({ length: n }, (_, i) => i), - count: n, - modularity: NaN, - sizes: new Array(n).fill(1), - } - } - let nbr = g.adj.map((a) => { - const mp = new Map() - a.forEach((v) => mp.set(v, 1)) - return mp - }) - let size = n - let membership = Int32Array.from({ length: n }, (_, i) => i) - for (let level = 0; level < 32; level++) { - const k = new Float64Array(size) - nbr.forEach((mp, i) => { - let s = 0 - mp.forEach((w) => { - s += w - }) - k[i] = s - }) - const comm = Int32Array.from({ length: size }, (_, i) => i) - const tot = Float64Array.from(k) - let moved = true, - anyMove = false, - passes = 0 - while (moved && passes < 100) { - moved = false - passes++ - for (let i = 0; i < size; i++) { - const ci = comm[i], - ki = k[i] - const links = new Map() - nbr[i].forEach((w, j) => { - if (j !== i) { - const cj = comm[j] - links.set(cj, (links.get(cj) || 0) + w) - } - }) - tot[ci] -= ki - let best = ci - let bestGain = (links.get(ci) || 0) - (gamma * tot[ci] * ki) / twoM - links.forEach((w, c) => { - const gain = w - (gamma * tot[c] * ki) / twoM - if (gain > bestGain + 1e-12) { - bestGain = gain - best = c - } - }) - tot[best] += ki - if (best !== ci) { - comm[i] = best - moved = true - anyMove = true - } - } - } - if (!anyMove) break - const remap = new Map() - for (let i = 0; i < size; i++) if (!remap.has(comm[i])) remap.set(comm[i], remap.size) - const next = remap.size - const agg = Array.from({ length: next }, () => new Map()) - for (let i = 0; i < size; i++) { - const ci = remap.get(comm[i]) - nbr[i].forEach((w, j) => { - const cj = remap.get(comm[j]) - agg[ci].set(cj, (agg[ci].get(cj) || 0) + w) - }) - } - membership = membership.map((x) => remap.get(comm[x])) - nbr = agg - if (next === size) break - size = next - } - // modularity on the original graph - let count = 0 - membership.forEach((c) => { - if (c + 1 > count) count = c + 1 - }) - const inW = new Float64Array(count), - totW = new Float64Array(count), - sizes = new Array(count).fill(0) - for (let u = 0; u < n; u++) { - const cu = membership[u] - sizes[cu]++ - totW[cu] += g.adj[u].length - g.adj[u].forEach((v) => { - if (membership[v] === cu) inW[cu] += 1 - }) - } - let q = 0 - for (let c = 0; c < count; c++) q += inW[c] / twoM - Math.pow(totW[c] / twoM, 2) - return { membership, count, modularity: q, sizes } -} - -// page wiring, run by main.ts in the original order -export function init() { - /* ---------- wiring ---------- */ - document - .getElementById('btnNormaAdd') - .addEventListener('click', () => document.getElementById('normaFileInput').click()) - - document.getElementById('normaFileInput').addEventListener('change', (e) => { - handleNormaUploads(e.target.files) - e.target.value = '' - }) - - ;(function setupDropZone() { - const zone = document.getElementById('dropZone') - let depth = 0 - zone.addEventListener('dragenter', (e) => { - e.preventDefault() - depth++ - zone.classList.add('dragging') - }) - zone.addEventListener('dragover', (e) => { - e.preventDefault() - e.dataTransfer.dropEffect = 'copy' - }) - zone.addEventListener('dragleave', () => { - depth = Math.max(0, depth - 1) - if (!depth) zone.classList.remove('dragging') - }) - zone.addEventListener('drop', (e) => { - e.preventDefault() - depth = 0 - zone.classList.remove('dragging') - if (e.dataTransfer.files && e.dataTransfer.files.length) - handleNormaUploads(e.dataTransfer.files) - }) - })() - - document.getElementById('btnRefreshView').addEventListener('click', () => { - const v = activeView() - refreshLibraryView() - if (v && v.autoName && cy.nodes().length) { - v.name = uniqueViewName(nameForSelection()) - renderViewBar() - } - }) - - document.getElementById('btnOpenNewView').addEventListener('click', () => { - if (!libSelection.networks.size) { - setStatus('normaStatus', [{ level: 'error', text: 'Tick at least one network first.' }]) - return - } - // the ticked files move to the new view; this view keeps what it shows - const pending = { - networks: [...libSelection.networks], - annotation: libSelection.annotation, - colors: libSelection.colors, - } - const cur = activeView() - const shown = selectionFromKey(S.currentLibView) - const name = nameForSelection() - if (cur && cy.nodes().length) { - libSelection.networks = new Set(shown.networks) - libSelection.annotation = shown.annotation - libSelection.colors = shown.colors - } - openInNewView(name, () => { - libSelection.networks = new Set(pending.networks) - libSelection.annotation = pending.annotation - libSelection.colors = pending.colors - refreshLibraryView() - }) - }) - - document.getElementById('btnNormaExportNetwork').addEventListener('click', exportNormaNetwork) - - document - .getElementById('btnNormaExportAnnotation') - .addEventListener('click', exportNormaAnnotation) - - document.getElementById('btnNormaExportColors').addEventListener('click', exportNormaColors) - - // A numeric expression file for the documentation example (simulated values, - // for trying the color scale; not measured data). - NORMA_EXAMPLE_SETS['docs-tp53'].files.push({ - kind: 'colors', - name: 'TP53 docs log2FC and adjusted p-values (simulated)', - fileName: 'TP53_docs_values_simulated.txt', - text: 'Gene\tlog2FC\tpadj\nCDKN1A\t2.4\t0.000001\nTP53\t-0.3\t0.41\nMDM4\t1.1\t0.02\nBCL2L1\t-1.8\t0.0003\nCHEK2\t-1.2\t0.004\nATM\t-0.9\t0.03\nTP53BP2\t-2.2\t0.00005\nCDKN2A\t1.7\t0.0002\nEP300\t0.4\t0.21\nCREBBP\t-0.6\t0.09\nMDM2\t2.9\t0.00000001\n', - }) - - // ---------- NORMA 3.0 examples that showcase the new features ---------- - ;(function addNorma3Examples() { - // 1. The example network from Arena3D's documentation: layers become groups - const arenaRows = [ - ['An', 'Group1', 'Cn', 'Group1', 2], - ['An', 'Group1', 'Bn', 'Group1', 10], - ['Bn', 'Group1', 'Cn', 'Group1', 1], - ['Cn', 'Group1', 'Dn', 'Group1', 3], - ['En', 'Group2', 'Fn', 'Group2', 4], - ['En', 'Group2', 'Hn', 'Group2', 5], - ['Fn', 'Group2', 'Gn', 'Group2', 6], - ['Gn', 'Group2', 'Hn', 'Group2', 7], - ['In', 'Group3', 'Jn', 'Group3', 8], - ['Bn', 'Group1', 'Fn', 'Group2', 9], - ['Dn', 'Group1', 'Hn', 'Group2', 11], - ['Dn', 'Group1', 'In', 'Group3', 1], - ['Cn', 'Group1', 'Jn', 'Group3', 1], - ['Hn', 'Group2', 'In', 'Group3', 1], - ['Hn', 'Group2', 'Kn', 'Group4', 1], - ['Kn', 'Group4', 'Ln', 'Group4', 0.1], - ['Kn', 'Group4', 'Mn', 'Group5', 1], - ['An', 'Group1', 'Nn', 'Group5', 1], - ['Kn', 'Group4', 'On', 'Group5', 1], - ['Kn', 'Group4', 'Pn', 'Group5', 12], - ['Kn', 'Group4', 'Qn', 'Group6', 1], - ['Kn', 'Group4', 'Rn', 'Group6', 1], - ['Kn', 'Group4', 'Sn', 'Group7', 1], - ['Kn', 'Group4', 'Tn', 'Group7', 10], - ] - const arenaText = - [ - 'SourceNode\tSourceLayer\tTargetNode\tTargetLayer\tWeight\tChannel', - ...arenaRows.map((r) => [...r, 1].join('\t')), - ].join('\n') + '\n' - const arenaConv = convertArena3dNetwork(arenaText) - NORMA_EXAMPLE_SETS['arena3d'] = { - title: 'Arena3D example: 7 layers', - source: 'Arena3D documentation', - link: 'https://arena3d.org', - local: true, - files: [ - { - kind: 'network', - name: 'Arena3D example network', - fileName: 'arena3d_example_network.txt', - text: arenaConv.network, - }, - { - kind: 'annotation', - name: 'Arena3D example layers', - fileName: 'arena3d_example_layers.txt', - text: arenaConv.annotation, - }, - { - kind: 'raw', - name: 'Arena3D network file (original format)', - fileName: 'arena3d_example_upload_network.txt', - text: arenaText, - }, - ], - } - - // 2. A directed signalling cascade with simulated fold changes - const cascade = generateDirectedMultiNetwork(40) - const cTexts = demoToNormaTexts(cascade) - const rc = mulberry32(2024) - const gauss = () => { - let u = 0, - v = 0 - while (!u) u = rc() - while (!v) v = rc() - return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) - } - const layerShift = { receptors: 1.6, kinases: 1.0, 'transcription factors': 0.3, targets: -1.4 } - const cValues = - [ - 'Gene\tlog2FC\tpadj', - ...cascade.nodes.map((n) => { - if (rc() < 0.06) return `${n.id}\tNA\tNA` - const fc = Math.round((layerShift[n.groups[0]] + gauss() * 0.9) * 1000) / 1000 - const p = Math.min(1, Math.pow(10, -Math.abs(fc) * (1.2 + rc() * 1.8))) - return `${n.id}\t${fc}\t${p.toPrecision(3)}` - }), - ].join('\n') + '\n' - NORMA_EXAMPLE_SETS['cascade'] = { - title: 'Signalling cascade with fold changes (simulated)', - source: 'Simulated for NORMA 3.0', - link: null, - local: true, - display: { - edgeDirection: 'data', - edgeCurveStyle: 'bezier', - layoutSelect: 'fr', - legendShow: true, - sizeMetric: 'fixed', - }, - files: [ - { - kind: 'network', - name: 'Signalling cascade (directed, multi-edge)', - fileName: 'cascade_network.txt', - text: cTexts.network, - }, - { - kind: 'annotation', - name: 'Cascade levels', - fileName: 'cascade_levels.txt', - text: cTexts.annotation, - }, - { - kind: 'colors', - name: 'Cascade log2FC and padj (simulated)', - fileName: 'cascade_values_simulated.txt', - text: cValues, - }, - ], - } - - // 3. A healthy and a disease network (simulated) for Network Comparison - const base = buildModuleNetwork({ - sizes: [18, 16, 16, 14], - overlaps: [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [1, 2, 3], - ], - pIn: 0.22, - pOut: 0.01, - seed: 64, - }) - const rh = mulberry32(64) - const ids = base.nodes.map((n) => n.id) - const moduleOf = new Map(base.nodes.map((n) => [n.id, n.groups])) - const healthy = base.edges.map((e) => [e.source, e.target]) - const key = (a, b) => (a < b ? a + '\t' + b : b + '\t' + a) - const hSet = new Set(healthy.map(([a, b]) => key(a, b))) - // disease: 20% of the edges are lost, and the third module gains new links - const disease = healthy.filter(() => rh() > 0.2) - const dSet = new Set(disease.map(([a, b]) => key(a, b))) - const third = base.groupOrder - ? base.groupOrder[2] - : [...new Set(base.nodes.flatMap((n) => n.groups))][2] - const inThird = ids.filter((id) => (moduleOf.get(id) || []).includes(third)) - let added = 0 - while (added < Math.round(healthy.length * 0.15)) { - const a = - rh() < 0.7 ? inThird[Math.floor(rh() * inThird.length)] : ids[Math.floor(rh() * ids.length)] - const b = inThird[Math.floor(rh() * inThird.length)] - if (a === b || dSet.has(key(a, b))) continue - dSet.add(key(a, b)) - disease.push([a, b]) - added++ - } - const netText = (pairs) => - ['Source\tTarget', ...pairs.map((p) => p.join('\t'))].join('\n') + '\n' - const annText = demoToNormaTexts(base).annotation - const dValues = - [ - 'Gene\tlog2FC\tpadj', - ...ids.map((id) => { - const boost = (moduleOf.get(id) || []).includes(third) ? 1.8 : 0 - const fc = Math.round((boost + gauss() * 0.7) * 1000) / 1000 - const p = Math.min(1, Math.pow(10, -Math.abs(fc) * (1 + rh() * 2))) - return `${id}\t${fc}\t${p.toPrecision(3)}` - }), - ].join('\n') + '\n' - NORMA_EXAMPLE_SETS['healthy-disease'] = { - title: 'Healthy vs disease networks (simulated)', - source: 'Simulated for NORMA 3.0', - link: null, - local: true, - showAll: true, - display: { edgeCurveStyle: 'bezier', legendShow: true, showGroupHulls: true }, - files: [ - { - kind: 'network', - name: 'Healthy (simulated)', - fileName: 'healthy_network_simulated.txt', - text: netText(healthy), - }, - { - kind: 'network', - name: 'Disease (simulated)', - fileName: 'disease_network_simulated.txt', - text: netText(disease), - }, - { kind: 'annotation', name: 'Modules', fileName: 'modules.txt', text: annText }, - { - kind: 'colors', - name: 'Disease vs healthy log2FC (simulated)', - fileName: 'disease_vs_healthy_values_simulated.txt', - text: dValues, - }, - ], - } - })() - - // links between Help and About (and to the Welcome page) switch pages - document.addEventListener('click', (e) => { - const sample = e.target.closest('a[data-sample-link]') - if (sample) { - e.preventDefault() - const q = new URLSearchParams(sample.getAttribute('href').replace(/^\?/, '')) - const sel = document.getElementById('sampleSelect') - sel.value = q.get('example') - document.getElementById('btnSample').click() - switchTab(q.get('tab') === '3d' ? 'network3d' : 'network') - return - } - const a = e.target.closest( - 'a[href^="#help-"], a[href^="#about-"], a[href^="#api-"], a[href="#welcome"], [data-go-welcome]' - ) - if (!a) return - const href = a.getAttribute('href') || '#welcome' - e.preventDefault() - if (href === '#welcome') { - switchTab('welcome') - return - } - const id = href.slice(1) - const page = id.startsWith('about-') ? 'about' : id.startsWith('api-') ? 'api' : 'help' - if (currentTab !== page) switchTab(page, id) - else { - const t = document.getElementById(id) - if (t) { - t.scrollIntoView({ block: 'start' }) - t.classList.remove('flash') - void t.offsetWidth - t.classList.add('flash') - } - } - }) - - document.querySelectorAll('#tabs .tab').forEach((btn) => { - btn.addEventListener('click', () => switchTab(btn.dataset.tab)) - btn.addEventListener('keydown', (ev) => { - const tabs = [...document.querySelectorAll('#tabs .tab')] - const i = tabs.indexOf(btn) - let next = null - if (ev.key === 'ArrowRight') next = tabs[(i + 1) % tabs.length] - if (ev.key === 'ArrowLeft') next = tabs[(i - 1 + tabs.length) % tabs.length] - if (next) { - ev.preventDefault() - next.focus() - switchTab(next.dataset.tab) - } - }) - }) - - document.querySelectorAll('.helplink').forEach((btn) => { - btn.title = 'Open help for this section' - btn.addEventListener('click', () => switchTab('help', btn.dataset.help)) - }) - - document.querySelectorAll('.help-nav a').forEach((a) => { - a.addEventListener('click', (ev) => { - ev.preventDefault() - const id = a.getAttribute('href').slice(1) - switchTab('help', id) - }) - }) - - document.querySelectorAll('.help-body a[href^="#"]').forEach((a) => { - a.addEventListener('click', (ev) => { - ev.preventDefault() - switchTab('help', a.getAttribute('href').slice(1)) - }) - }) -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 3412153..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "sourceMap": true, - "noEmit": true, - "skipLibCheck": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["node", "vite/client"] - }, - "include": ["src", "playwright.config.ts", "vite.config.ts"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 148c913..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig } from 'vitest/config' -import { viteSingleFile } from 'vite-plugin-singlefile' - -const backend = process.env.VITE_PROXY_TARGET ?? 'http://localhost:8000' - -// The page is norma.html (so ?example=, ?session= links keep working) and the -// build inlines everything into dist/norma.html, which opens from disk too. -export default defineConfig({ - base: './', - plugins: [viteSingleFile()], - build: { rollupOptions: { input: 'norma.html' } }, - server: { - // examples/norma_api_client.py (the API template) is imported ?raw from outside frontend/ - fs: { allow: ['..'] }, - proxy: Object.fromEntries( - ['/api', '/string-api', '/db-api', '/arena3d-api', '/norma-config.js'].map((p) => [ - p, - backend, - ]) - ), - }, - test: { - environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], - exclude: ['e2e/**', 'node_modules/**'], - }, -}) diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts deleted file mode 100644 index 5ffca92..0000000 --- a/frontend/vitest.setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Unit tests import modules that read the page at load (canvases, controls), -// so give jsdom the real markup and a no-op 2D canvas context. -import fs from 'node:fs' -import path from 'node:path' - -const html = fs.readFileSync(path.join(process.cwd(), 'norma.html'), 'utf8') -document.body.innerHTML = html.slice(html.indexOf('')) - -const noop = () => stub -const stub: unknown = new Proxy( - {}, - { get: (_, key) => (key === 'measureText' ? () => ({ width: 0 }) : noop), set: () => true } -) -HTMLCanvasElement.prototype.getContext = (() => - stub) as typeof HTMLCanvasElement.prototype.getContext diff --git a/functions/annotations.R b/functions/annotations.R new file mode 100644 index 0000000..7eca190 --- /dev/null +++ b/functions/annotations.R @@ -0,0 +1,1527 @@ +# main functions #### + +# This function handles the Annotations - convex hulls tab +convex_hulls <- function(g, annotation_graph){ + set.seed(123) + + original_dataset_weighted <- fetchFirstSelectedStoredDataset_annotations_tab() + if (is.null(original_dataset_weighted)) return(NULL) + + + my_network<- as.data.frame(get.edgelist(g)) + my_network<- data.frame(Source = my_network$V1, Target = my_network$V2) + + annotation_graph <- as.data.frame(annotation_graph) + groups<-annotation_graph + + annotation1 <- groups + + groups<- data.frame(V1 = groups$Annotations, stri_split_fixed(groups$Nodes, ",", simplify = TRUE)) + groups<-mutate_all(groups, funs(na_if(.,""))) + number_of_groups<-dim(groups)[1] + + x <- list() + for (i in 1:number_of_groups) { + group_i<- groups[i,] + group_i<- group_i[,-1] + group_i <- group_i[!is.na(group_i)] + x[[i]]<- (group_i) + } + + GO <- list() + for (i in 1:number_of_groups) { + GO[[i]]<-rep(groups[i,1], length(x[[i]])) + } + + column1<-my_network$Source + column2<-my_network$Target + node_names<-unique(union(column1, column2)) + tt<-unlist(x) + nodes_with_NA_groups<-setdiff(node_names,tt) + + members <- data_frame(id=unlist(x),group = unlist(GO)) + members_with_NA_groups <- data_frame(id=unlist(x),group = unlist(GO)) + + dataset1<- get.edgelist(g) + dataset1 <- as.matrix(dataset1) + annotation1 <- as.matrix(annotation1) + + nrowdat <- nrow(dataset1) + nrowannot <- nrow(annotation1) + + if(length(nodes_with_NA_groups)>0){ + for (i in 1:length(nodes_with_NA_groups)){ + members_with_NA_groups[nrow(members_with_NA_groups)+1,1] <- nodes_with_NA_groups[i] + } + members_with_NA_groups<-unique(members_with_NA_groups) + } + + #---------------------------------# + if (input$convex_layout_strategy == 'Supernodes per group'){ + shinyjs::show("repeling_force") + shinyjs::show("local_layout") + } else if (input$convex_layout_strategy == 'Group gravity'){ + shinyjs::show("repeling_force") + shinyjs::hide("local_layout") + } else { + shinyjs::hide("repeling_force") + shinyjs::hide("local_layout") + } + + if(input$convex_layout_strategy == 'Simple layout'){ + lay <- layout_choices(g, input$layouts) + node_name <- names(V(g)) + } else if(input$convex_layout_strategy == 'Virtual node per group'){ + lay <- strategy1_virtualNodes(g, annotation_graph) + node_name <- unique(members_with_NA_groups$id) + } else if(input$convex_layout_strategy == 'Group gravity'){ + result <- strategy2_gravity(g, annotation_graph, input$layouts, input$repeling_force) + lay <- result$lay + node_name <- result$network_nodes + } else if(input$convex_layout_strategy == 'Supernodes per group'){ + result <- strategy3_superNodes(g, annotation_graph, input$layouts, input$local_layout, input$repeling_force) + lay <- result$lay + node_name <- result$network_nodes + } + node_name_links <- node_name + + # Opening out conn + fileConn <- file(paste(USER_TEMP_FOLDER, "/output_convex_", session$token,".html", sep=""), "w") + + s <- input$chooseGroups_rows_selected + if (length(s)==0) + { + s<-c(1:nrowannot) + } + + if (length(s)) { + s<-sort(s) + x<- length(s) + ccc<-group_pal_rows(length(x)) + tmp_selected_colors<- c() + + tmp_selected_colors<- c(tmp_selected_colors, ccc[s[i]]) + group_color <- tmp_selected_colors + group_color_fill <- adjustcolor(group_color, alpha.f = 0.2) + + cat(sprintf(paste(" + + + + + + + + + + +\n"), file = fileConn) + + + close(fileConn) + + + }#if (length(s)) +}#function + +# This function handles the Annotations - pie chart tab +pie_charts<- function(g, annotation_graph){ + dataset1<- get.edgelist(g) + annotation_graph <- as.data.frame(annotation_graph) + groups<-annotation_graph + original_dataset_weighted <- fetchFirstSelectedStoredDataset_annotations_tab() + if (is.null(original_dataset_weighted)) return(NULL) + + my_network<- as.data.frame(get.edgelist(g)) + my_network<- data.frame(Source = my_network$V1, Target = my_network$V2) + + annotation1<- groups + + groups<- data.frame(V1 = groups$Annotations, stri_split_fixed(groups$Nodes, ",", simplify = TRUE)) + groups<-mutate_all(groups, funs(na_if(.,""))) + number_of_groups<-dim(groups)[1] + + x <- list() + for (i in 1:number_of_groups) { + group_i<- groups[i,] + group_i<- group_i[,-1] + group_i <- group_i[!is.na(group_i)] + x[[i]]<- (group_i) + } + + GO <- list() + for (i in 1:number_of_groups) { + GO[[i]]<-rep(groups[i,1], length(x[[i]])) + } + + column1<-my_network$Source + column2<-my_network$Target + node_names<-unique(union(column1, column2)) + tt<-unlist(x) + nodes_with_NA_groups<-setdiff(node_names,tt) + + members <- data_frame(id=unlist(x),group = unlist(GO)) + members_with_NA_groups <- data_frame(id=unlist(x),group = unlist(GO)) + dataset1 <- as.matrix(dataset1) + annotation1 <- as.matrix(annotation1) + + nrowdat <- nrow(dataset1) + nrowannot <- nrow(annotation1) + + s <- input$chooseGroups2_rows_selected + if (length(s)==0) + { + s<-c(1:nrowannot) + } + + if (length(s)) { + s<-sort(s) + x<- length(s) + ccc<-group_pal_rows(length(x)) + tmp_selected_colors<- c() + + tmp_selected_colors<- c(tmp_selected_colors, ccc[s[i]]) + group_color <- tmp_selected_colors + group_color_fill <- adjustcolor(group_color, alpha.f = 0.2) + + fileConn <- file(paste(USER_TEMP_FOLDER, "/output_pies_", session$token,".html", sep=""), "w") + cat(sprintf(paste(" + + + + + + + + + +"), file = fileConn) + + close(fileConn) + }#if (length(s)) +}#function + +# Function for Layouts with virtual nodes (per group) that pull all in-group nodes +# @param g(igraph obj): the selected network +# @param groups(dataframe): the selected annotation file -> names and respective nodes +# @return lay: the layout coordinates +strategy1_virtualNodes <- function(g, groups){ + if (is.null(g) || is.null(groups)) return() + set.seed(123) + + my_network<- as.data.frame(get.edgelist(g)) + my_network<- data.frame(Source = my_network$V1, Target = my_network$V2) + + groups<- data.frame(V1 = groups$Annotations, stri_split_fixed(groups$Nodes, ",", simplify = TRUE)) + groups<-mutate_all(groups, funs(na_if(.,""))) + number_of_groups<-dim(groups)[1] + + x <- list() + for (i in 1:number_of_groups) { + group_i<- groups[i,] + group_i<- group_i[,-1] + group_i <- group_i[!is.na(group_i)] + x[[i]]<- (group_i) + } + + GO <- list() + for (i in 1:number_of_groups) { + GO[[i]]<-rep(groups[i,1], length(x[[i]])) + } + + column1<-my_network$Source + column2<-my_network$Target + node_names<-unique(union(column1, column2)) + tt<-unlist(x) + nodes_with_NA_groups<-setdiff(node_names,tt) + + members <- data_frame(id=unlist(x),group = unlist(GO)) + members_with_NA_groups <- data_frame(id=unlist(x),group = unlist(GO)) + + if(length(nodes_with_NA_groups)>0){ + for (i in 1:length(nodes_with_NA_groups)) + { + members_with_NA_groups[nrow(members_with_NA_groups)+1,1] <- nodes_with_NA_groups[i] + } + members_with_NA_groups<-unique(members_with_NA_groups) + } + + edge <- data_frame(Source = my_network$Source, Target = my_network$Target, group = NA) #edge --> not edges + + within_group_edges <- members %>% + split(.$group) %>% + map_dfr(function (grp) { + if(length(grp$id)>=2){ + id2id <- combn(grp$id, 2) + data_frame(Source = id2id[1,], + Target = id2id[2,], + group = unique(grp$group)) + } + }) + + # sort by group as in file + group_order<-(as.list(unique(members_with_NA_groups$group))) + EE <- new.env(hash = TRUE) + EE_positions <- new.env(hash = TRUE) + for(i in 1: length(group_order)) + { + group_name_as_key<-group_order[[i]] + EE[[ as.character(group_name_as_key) ]]<-i + EE_positions[[ as.character(i) ]]<-group_order[[i]] + } + for(i in 1: length(group_order)) + { + group_name_as_key<-group_order[[i]] + index<-EE[[ as.character(group_name_as_key) ]] + } + + group_ids_tmp <- lapply(members_with_NA_groups %>% split(.$group), function(grp) { grp$id }) + group_ids<-c() + for(i in 1: length(group_ids_tmp)) + { + group_ids<-c(group_ids, group_ids_tmp[ EE_positions[[ as.character(i) ]]]) + } + + virt_group_nodes <- length(members_with_NA_groups$id) + 1:number_of_groups + names(virt_group_nodes) <- unique(members$group) # altered from c(letters[1:number_of_groups]) + edges_virt <- data_frame(Source = edge$Source, Target = edge$Target, weight = 5, group = edge$group) + + within_virt <- members %>% split(.$group) %>% map_dfr(function (grp) { + group_name <- unique(grp$group) + virt_from <- rep(virt_group_nodes[group_name], length(grp$id)) + if(length(grp$id)>=2){ + id2id <- combn(grp$id, 2) + data_frame( + Source = c(id2id[1,], virt_from), + Target = c(id2id[2,], grp$id), # also connects from virtual_from node to each group node + weight = c(rep(0.1, ncol(id2id)), # weight between group nodes + rep(50, length(grp$id))), + to_be_deleted = c(rep(T, ncol(id2id)), # weight between group nodes + rep(T, length(grp$id))), # weight that 'ties together' the group (via the virtual group node) + group = group_name + ) + } + }) + + edges_virt <-bind_rows(mutate_all(edges_virt, as.character), mutate_all(within_virt, as.character)) # vgazei 38,39,40 + virt_group_na <- virt_group_nodes[is.na(names(virt_group_nodes))] + non_group_nodes <- (members_with_NA_groups %>% filter(is.na(group)))$id + nodes_virt <- data_frame(id = 1:(length(members_with_NA_groups$id) + length(virt_group_nodes)), + is_virt = c(rep(FALSE, length(members_with_NA_groups$id)), + rep(TRUE, length(virt_group_nodes)))) + nodes_virt$id <- as.character(nodes_virt$id) + + #replace with the right names from our network + nodes_virt[1:length(members_with_NA_groups$id), ]$id <- members_with_NA_groups$id + + nodes_virt <- unique(nodes_virt) + + edge_names <- unique(c(edges_virt$Source, edges_virt$Target)) + nodes_virt <- nodes_virt[which(nodes_virt$id %in% edge_names), ] + g_virt <- graph_from_data_frame(edges_virt, directed = FALSE, vertices = nodes_virt) + + # use "auto layout" + # lay2 <- layout_nicely(g_virt) + lay <- layout_choices(g_virt, input$layouts) + + # remove virtual group nodes from graph + nodes_to_remove <- nodes_virt[which(nodes_virt$is_virt),]$id + nodes_to_remove <- nodes_to_remove[nodes_to_remove %in% V(g_virt)$name] + if (!identical(nodes_to_remove, character(0))) + g_virt <- delete_vertices(g_virt, nodes_to_remove) + + # remove virtual group nodes' positions from the layout matrix + tmp<-which(nodes_virt$is_virt == T ) + + lay <- lay[-tmp, ] + return(lay) +} + +# Function for Layouts with enhanced gravity for in-group nodes +# @param g(igraph obj): the selected network +# @param groups(dataframe): the selected annotation file -> names and respective nodes +# @param layout(string): the user-selected layout choice +# @param repeling_force(int): the user-selected repeling force +# @return lay (2d double matrix): the layout coordinates +# @return network_nodes (character vector): the proper order of nodes(names) to correctly attach to canvas +strategy2_gravity <- function(g, groups, layout, repeling_force){ + lay <- NULL + network_nodes <- NULL + if (!(is.null(g) || is.null(groups))){ + set.seed(123) + + # network + my_network <- as.data.frame(get.edgelist(g)) + my_network <- cbind(my_network, as.double(E(g)$Weight)) + colnames(my_network) <- c('Source', 'Target', 'Weight') + network_nodes <- unique(c(my_network$Source, my_network$Target)) + + # annotations + groups_expanded <- groups %>% separate_rows(Nodes, sep=",") + groups_expanded <- groups_expanded[which(groups_expanded$Nodes %in% network_nodes), ] # removing non-existing nodes + + # 1. create dataframe with extra edges (all against all in same groups that do not already exist in my_network) + extra_edges <- merge(groups_expanded, groups_expanded, by.x = "Annotations", by.y = "Annotations") + temp_g <- graph_from_data_frame(extra_edges[, c(2,3)], directed = F) + if ('Kamada-Kawai' == str_split(layout, "\t")[[1]][1]) E(temp_g)$weight <- min(my_network$Weight) + else E(temp_g)$weight <- max(my_network$Weight) # * 1.0001 # invisible weight = max network value *2 + temp_g <- igraph::simplify(temp_g, remove.multiple = T, remove.loops = T, edge.attr.comb = "first") + extra_edges <- as.data.frame(cbind( get.edgelist(temp_g) , E(temp_g)$weight )) + colnames(extra_edges) <- c('Source', 'Target', 'Weight') + + # 2. check network edges one by one; if exist in same group, weight * 100, else weight/100 + # This brings nodes ultra-close - unneeded + for (i in 1:nrow(my_network)){ + source_groups <- groups_expanded[which( groups_expanded$Nodes %in% my_network$Source[i]), ]$Annotations + target_groups <- groups_expanded[which( groups_expanded$Nodes %in% my_network$Target[i]), ]$Annotations + if ('Kamada-Kawai' == str_split(layout, "\t")[[1]][1]){ + if (identical(source_groups, character(0)) || + identical(target_groups, character(0)) || + identical(intersect(source_groups, target_groups), character(0))) + my_network$Weight[i] <- my_network$Weight[i] * repeling_force # kamada-kawai swap + else my_network$Weight[i] <- my_network$Weight[i] / repeling_force + } else { + if (identical(source_groups, character(0)) || + identical(target_groups, character(0)) || + identical(intersect(source_groups, target_groups), character(0))) + my_network$Weight[i] <- my_network$Weight[i] / repeling_force # kamada-kawai swap + else my_network$Weight[i] <- my_network$Weight[i] * repeling_force + } + } + + # 3. append to my_network + my_network <- rbind(my_network, extra_edges) + + # 4. graph handling + out_g <- graph_from_data_frame(my_network, directed = F, vertices = network_nodes) + E(out_g)$weight <- as.numeric(my_network$Weight) + out_g <- igraph::simplify(out_g, remove.multiple = T, remove.loops = T, edge.attr.comb = "max") + lay <- layout_choices(out_g, layout) + } + + return(list(lay = lay, network_nodes = network_nodes, groups_expanded = groups_expanded)) +} + +# Function for Layouts with superNodes per Annotation Group +# @param g(igraph obj): the selected network +# @param groups(dataframe): the selected annotation file -> names and respective nodes +# @param layout(string): the user-selected layout choice +# @param local_layout(string): the user-selected layout choice for in-group layouts +# @param repeling_force(int): the user-selected repeling force +# @return lay (2d double matrix): the layout coordinates +# @return network_nodes (character vector): the proper order of nodes(names) to correctly attach to canvas +strategy3_superNodes <- function(g, groups, layout, local_layout, repeling_force){ + lay <- NULL + network_nodes <- NULL + if (!(is.null(g) || is.null(groups))){ + set.seed(123) + + # network + my_network <- as.data.frame(get.edgelist(g)) + my_network <- cbind(my_network, as.double(E(g)$Weight)) + colnames(my_network) <- c('Source', 'Target', 'Weight') + network_nodes <- unique(c(my_network$Source, my_network$Target)) + + # annotations + groups_expanded <- groups %>% separate_rows(Nodes, sep=",") + groups_expanded <- groups_expanded[which(groups_expanded$Nodes %in% network_nodes), ] # removing non-existing nodes + + noGroupNodes <- network_nodes[!(network_nodes %in% groups_expanded$Nodes)] + + # 1. create dataframe of one supernode per group plus no-group nodes + # Source Target -> swap all nodes with their respective Group Name(s) + # if multiple groups per node, add the extra edges + # e.g. Group1+2Node - noGroupNode -> Group1 - noGroupNode, Group2 - noGroupNode + # merge my_network with groups_expanded two times ( Source - Nodes, Target - Nodes) + # where annotations not NA, swap Source or Target with respective Group Name + superFrame <- merge(my_network, groups_expanded, by.x = 'Source', by.y = 'Nodes', all.x = T) + superFrame <- merge(superFrame, groups_expanded, by.x = 'Target', by.y = 'Nodes', all.x = T) + graphFrame <- superFrame # keeping this for later on + graphFrame$Source[!is.na(graphFrame$Annotations.x)] <- graphFrame$Annotations.x[!is.na(graphFrame$Annotations.x)] + graphFrame$Target[!is.na(graphFrame$Annotations.y)] <- graphFrame$Annotations.y[!is.na(graphFrame$Annotations.y)] + graphFrame <- graphFrame[, c('Source', 'Target', 'Weight')] + + # 2. create graph and apply layout on this compound supernode network + temp_g <- graph_from_data_frame(graphFrame, directed = F) + E(temp_g)$weight <- as.numeric(graphFrame$Weight) + temp_g <- igraph::simplify(temp_g, remove.multiple = T, remove.loops = T, edge.attr.comb = "max") + lay_super <- layout_choices(temp_g, layout) + # lay_super <- layout_with_fr(temp_g) + lay_super <- cbind(lay_super, names(V(temp_g))) + # plot(temp_g, layout = lay_super) + + # 3. push all nodes above away from 0,0 // also check minx maxx for layout as alternative + # foreach node, calculate a = y/x + # then multiply x by an input number n (e.g.) and solve for y + # keep the (x, y) coords system in a matrix for all supernodes + lay_super <- as.data.frame(lay_super) + lay_super$V1 <- as.numeric(lay_super$V1) + lay_super$V2 <- as.numeric(lay_super$V2) + lay_super$a <- ifelse(lay_super$V1 != 0, lay_super$V2 / lay_super$V1, lay_super$V2 / 0.01) + lay_super$X <- repeling_force * lay_super$V1 + lay_super$Y <- lay_super$a * lay_super$X + lay_super <- lay_super[, c('X', 'Y', 'V3')] + colnames(lay_super)[3] <- 'Node' + + # 4. foreach group, add low-weight within group edges and + # apply layout (2nd input choice) with the respective (x,y) coords system + # extra edges dataframe for all groups + extra_edges <- merge(groups_expanded, groups_expanded, by.x = "Annotations", by.y = "Annotations") + lay <- matrix(, nrow = 0, ncol = 3) + for (group in unique(groups_expanded$Annotations)){ + tempFrame <- superFrame + tempFrame <- tempFrame[(tempFrame$Annotations.x == group & tempFrame$Annotations.y == group), ] + tempFrame <- tempFrame[!is.na(tempFrame$Source) & !is.na(tempFrame$Target), ] + tempFrame <- tempFrame[, c('Source', 'Target', 'Weight')] + + # create any missing in-group edges with minimum weight + # (all against all in same groups that do not already exist in tempFrame) + temp_extra_edges <- extra_edges[extra_edges$Annotations == group, ] + temp_g <- graph_from_data_frame(temp_extra_edges[, c(2,3)], directed = F) + min_weight <- ifelse(identical(min(tempFrame$Weight), Inf), 1, min(tempFrame$Weight)) + if ('Kamada-Kawai' == str_split(layout, "\t")[[1]][1]) E(temp_g)$weight <- min_weight * repeling_force + else E(temp_g)$weight <- min_weight / repeling_force # * 1.0001 # invisible weight = max network value *2 + temp_g <- igraph::simplify(temp_g, remove.multiple = T, remove.loops = T, edge.attr.comb = "first") + temp_extra_edges <- as.data.frame(cbind( get.edgelist(temp_g) , E(temp_g)$weight )) + colnames(temp_extra_edges) <- c('Source', 'Target', 'Weight') + + tempFrame <- rbind(tempFrame, temp_extra_edges) + + if (nrow(tempFrame) > 0){ + temp_g <- graph_from_data_frame(tempFrame, directed = F) + E(temp_g)$weight <- as.numeric(tempFrame$Weight) + temp_g <- igraph::simplify(temp_g, remove.multiple = T, remove.loops = T, edge.attr.comb = "max") + temp_lay <- layout_choices(temp_g, local_layout) + # temp_lay <- layout_with_fr(temp_g) + temp_lay <- cbind(temp_lay, names(V(temp_g))) + # plot(temp_g, layout = temp_lay) + + groupX <- lay_super[lay_super[,3] == group, 1] + groupY <- lay_super[lay_super[,3] == group, 2] + temp_lay[, 1] <- as.numeric(temp_lay[, 1]) + groupX + temp_lay[, 2] <- as.numeric(temp_lay[, 2]) + groupY + + lay <- rbind(lay, temp_lay) + } else{ + lay <- rbind(lay, c(lay_super[lay_super[,3] == group, 1], + lay_super[lay_super[,3] == group, 2], + groups$Nodes[groups$Annotations == group])) + } + } # end for + + # 5. calculate coordinates for duplicate nodes + dflay <- as.data.frame(lay) + dflay$V1 <- as.numeric(dflay$V1) + dflay$V2 <- as.numeric(dflay$V2) + meanX <- aggregate(dflay$V1, by=list(dflay$V3), FUN=mean) + colnames(meanX) <- c("Node", "X") + meanY <- aggregate(dflay$V2, by=list(dflay$V3), FUN=mean) + colnames(meanY) <- c("Node", "Y") + dflay <- merge(meanX, meanY) + dflay <- as.matrix(dflay[, c("X", "Y", "Node")]) + + # 6. append non-group nodes from lay_super + lay_noGroupNodes <- lay_super[lay_super[,3] %in% noGroupNodes, ] + lay <- rbind(dflay, lay_noGroupNodes) + network_nodes <- lay[, 3] + + lay <- cbind(as.numeric(lay[, 1]), as.numeric(lay[, 2])) + } + + return(list(lay = lay, network_nodes = network_nodes, groups_expanded = groups_expanded)) +} + +vennDiagrams <- function(){ + g <- fetchFirstSelectedStoredIgraph_annotations_tab() + if (is.null(g)) return() + + my_network <- as.data.frame(get.edgelist(g)) + my_network <- data.frame(Source = my_network$V1, Target = my_network$V2) + + annotation_graph <- fetchFirstSelectedStoredGroups2_annotations_tab() + + if (is.null(annotation_graph)) + return() + annotation_graph <- as.data.frame(annotation_graph) + groups <- annotation_graph + groups <- data.frame(V1 = groups$Annotations, stri_split_fixed(groups$Nodes, ",", simplify = TRUE)) + groups <- mutate_all(groups, funs(na_if(., ""))) + number_of_groups <- dim(groups)[1] + + x <- list() + for (i in 1:number_of_groups) { + group_i <- groups[i, ] + group_i <- group_i[, -1] + group_i <- group_i[!is.na(group_i)] + x[[i]] <- (group_i) + } + + GO <- list() + for (i in 1:number_of_groups) { + GO[[i]] <- rep(groups[i, 1], length(x[[i]])) + } + + column1 <- my_network$Source + column2 <- my_network$Target + node_names <- unique(union(column1, column2)) + tt <- unlist(x) + nodes_with_NA_groups <- setdiff(node_names, tt) + + members <- data_frame(id = unlist(x), group = unlist(GO)) + members_with_NA_groups <- + data_frame(id = unlist(x), group = unlist(GO)) + if (length(nodes_with_NA_groups) > 0) { + for (i in 1:length(nodes_with_NA_groups)) + { + members_with_NA_groups[nrow(members_with_NA_groups) + 1, 1] <- + nodes_with_NA_groups[i] + } + members_with_NA_groups <- unique(members_with_NA_groups) + } + + venn<- members_with_NA_groups %>% group_by(id) %>% summarise_all(funs(trimws(paste(., collapse = ',')))) + + return(venn) +} + +getStoredNetsChoices_annotations_tab <- function() { + snets <- StoredNets_annotations_tab() + if (nrow(snets) == 0) return(NULL) + choices <- snets$id + names(choices) <- snets$name + return(choices) +} + +getStoredNetsChoices2_annotations_tab <- function() { + snets <- StoredNets2_annotations_tab() + if (nrow(snets) == 0) + return(NULL) + choices <- snets$id + names(choices) <- snets$name + return(choices) +} + +getStoredExpressionChoices <- function() { + sexpress <- StoredExpress() + if (nrow(sexpress) == 0) + return(NULL) + choices <- sexpress$id + names(choices) <- sexpress$name + return(choices) +} + +fetchDataset_annotations_tab <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +fetchDataset2_annotations_tab <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/",nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +SelectedStoredNets2_annotations_tab <- function() { + if (length(reactiveVars$SelectedStoredNetworksIds2_annotations_tab) > 0) { + return(StoredNets2_annotations_tab()[which( + reactiveVars$StoredNetworks2_annotations_tab$id %in% + reactiveVars$SelectedStoredNetworksIds2_annotations_tab + ),]) + } + else if (nrow(StoredNets2_annotations_tab()) == 0 || + is.na(StoredNets2_annotations_tab()[1,])) + return(NULL) + else { + updateCheckboxGroupInput( + session, + "uiLoadGraphOptionsOutput_annotations_annotations_tab", + "Selected network(s)", + choices = getStoredNetsChoices2_annotations_tab(), + selected = getStoredNetsChoices2_annotations_tab()[1] + ) + return(StoredNets2_annotations_tab()[1,]) + } +} + +SelectedStoredExpress <- function() { + if (length(reactiveVars$SelectedStoredExpressionIds) > 0) + return(StoredExpress()[which( + reactiveVars$StoredExpressions$id %in% + reactiveVars$SelectedStoredExpressionIds + ),]) + else if (nrow(StoredAnnots()) == 0 || is.na(StoredAnnots()[1,])) + return(NULL) + else return(StoredAnnots()[1,]) +} + +fetchDatasetEx <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +# sub-routines #### +fetchFirstSelectedStoredIgraph_annotations_tab <- function() { + dataset <- fetchFirstSelectedStoredDataset_annotations_tab() + if (is.null(dataset)) + return(NULL) + else + return(convert_to_igraph(dataset)) +} + +SelectedStoredNets_annotations_tab <- function() { + if (length(reactiveVars$SelectedStoredNetworksIds_annotations_tab) > 0) { + return(StoredNets_annotations_tab()[which( + reactiveVars$StoredNetworks_annotations_tab$id %in% + reactiveVars$SelectedStoredNetworksIds_annotations_tab + ),]) + } + else if (nrow(StoredNets_annotations_tab()) == 0 || + is.na(StoredNets_annotations_tab()[1,])) + return(NULL) + else { + updateCheckboxGroupInput( + session, + "uiLoadGraphOptionsOutput_annotations_tab", + "Selected network(s)", + choices = getStoredNetsChoices_annotations_tab(), + selected = getStoredNetsChoices_annotations_tab()[1] + ) + return(StoredNets_annotations_tab()[1,]) + } +} + +# Output after choosing selected annotations +SelectedStoredAnnots <- function() { + if (length(reactiveVars$SelectedStoredAnnotationIds) > 0) + return(StoredAnnots()[which( + reactiveVars$StoredAnnotations$id %in% + reactiveVars$SelectedStoredAnnotationIds + ),]) + else if (nrow(StoredAnnots()) == 0 || is.na(StoredAnnots()[1,])) + return(NULL) + else return(StoredAnnots()[1,]) +} + +layout_choices_3D <- function(igraph, layouts_3D){ + if(length(layouts_3D)==0) + return(NULL) + results<-list() + for(i in layouts_3D){ + tmp<-unlist(strsplit(i,"\t",fixed=T)) + description<-tmp[1] + command<-tmp[2] + results[[description]]<-eval(parse(text=command)) + } + + return(results[[description]]) +} + +# functions for selectbox choices +layout_choices <- function(igraph,layouts_ui){ + if(length(layouts_ui)==0) + return(NULL) + results<-list() + for(i in layouts_ui){ + tmp<-unlist(strsplit(i,"\t",fixed=T)) + description<-tmp[1] + command<-tmp[2] + results[[description]]<-eval(parse(text=command)) + } + return(results[[description]]) +} diff --git a/functions/helpDownloadHandlers.R b/functions/helpDownloadHandlers.R new file mode 100644 index 0000000..1a09d96 --- /dev/null +++ b/functions/helpDownloadHandlers.R @@ -0,0 +1,232 @@ +# Help pages - Download Example files #### +output$dros_net <- downloadHandler( + filename = function() { + paste('Drosophila (TAU) Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(dros_net, file, row.names = F, sep = "\t", quote = F) + } +) +output$dros_annot <- downloadHandler( + filename = function() { + paste('Drosophila KEGG pathways', '.txt', sep = '') + }, + content = function(file) { + write.table(dros_annot, file,row.names = F, col.names = F, sep = "\t", quote = F) + } +) +output$dros_louvain <- downloadHandler( + filename = function() { + paste('Drosophila Louvain automated annotation', '.txt', sep = '') + }, + content = function(file) { + write.table(dros_louvain, file,row.names = F, col.names = F, sep = "\t", quote = F) + } +) +output$dros_express <- downloadHandler( + filename = function() { + paste('Drosophila Expressions file', '.txt', sep = '') + }, + content = function(file) { + write.table(dros_express, file,row.names = F, col.names = F, sep = "\t", quote = F) + } +) + +#-------------------------------------------# + +output$string_net_tp53 <- downloadHandler( + filename = function() { + paste('STRING TP53 Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_net_tp53, file, row.names = F, sep = "\t", quote = F) + } +) +output$string_annot <- downloadHandler( + filename = function() { + paste('STRING TP53 Annotation file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_annot, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$string_expr <- downloadHandler( + filename = function() { + paste('STRING TP53 Expression file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_expr, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) + +#-------------------------------------------# + +output$string_net_bcar3 <- downloadHandler( + filename = function() { + paste('STRING BCAR3 Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_net_bcar3, file, row.names = F, sep = "\t", quote = F) + } +) +output$string_bp <- downloadHandler( + filename = function() { + paste('STRING BCAR3 GO Biological Processes file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_bp, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$string_mf <- downloadHandler( + filename = function() { + paste('STRING BCAR3 GO Molecular functions file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_mf, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$string_kegg <- downloadHandler( + filename = function() { + paste('STRING BCAR3 GO KEGG file', '.txt', sep = '') + }, + content = function(file) { + write.table(string_kegg, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) + +#-------------------------------------------# + +output$co_express <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express, file, row.names = F, sep = "\t", quote = F) + } +) +output$co_express_bp <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression GO Biological Processes file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express_bp, file,row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$co_express_mf <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression GO Molecura functions file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express_mf, file,row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$co_express_cc <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression Cellular Componets file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express_cc, file,row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$co_express_kegg <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression KEGG pathways file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express_kegg, file,row.names = F,col.names = F, sep = "\t", quote = F) + } +) +output$co_express_mcode <- downloadHandler( + filename = function() { + paste('Human Gene Co-expression MCODE node coloring file', '.txt', sep = '') + }, + content = function(file) { + write.table(co_express_mcode, file,row.names = F,col.names = F, sep = "\t", quote = F) + } +) +#-----------------------------------------------------------------------------# + +output$covid_19_net <- downloadHandler( + filename = function() { + paste('COVID-19 Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_net, file, row.names = F, quote = F, sep = "\t") + } +) +output$covid_19_interpro <- downloadHandler( + filename = function() { + paste('HomoSapiens Protein Domains - INTERPRO file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_interpro, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) +output$covid_19_bp <- downloadHandler( + filename = function() { + paste('HomoSapiens GO Annotation - Biological Process file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_bp, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) +output$covid_19_mf <- downloadHandler( + filename = function() { + paste('HomoSapiens GO Annotation - Molecular Function file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_mf, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) +output$covid_19_cc <- downloadHandler( + filename = function() { + paste('HomoSapiens Protein Domains GO Annotation - Cellular Components file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_cc, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) +output$covid_19_kegg <- downloadHandler( + filename = function() { + paste('HomoSapiens Protein Domains KEGG pathways file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_kegg, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) +output$covid_19_smart <- downloadHandler( + filename = function() { + paste('HomoSapiens Protein Domains SMART file', '.txt', sep = '') + }, + content = function(file) { + write.table(covid_19_smart, file, row.names = F, col.names= F, quote = F, sep = "\t") + } +) + +#-----------------------------------------------------------------------------# + +output$Gallus_gallus_net <- downloadHandler( + filename = function() { + paste('Gallus gallus Network file', '.txt', sep = '') + }, + content = function(file) { + write.table(Gallus_gallus_net, file,row.names = F, quote = F, sep = "\t") + } +) +output$Gallus_gallus_kegg <- downloadHandler( + filename = function() { + paste('BioGrid Chicken Gallus KEGG pathways file', '.txt', sep = '') + }, + content = function(file) { + write.table(Gallus_gallus_kegg, file,row.names = F, col.names = F, quote = F, sep = "\t") + } +) + +output$R_script <- downloadHandler( + filename = function() { + paste('Annotation_cleaner', '.R', sep = '') + }, + content = function(file) { + write.table(R_script, file, row.names = F,col.names = F, sep = "\t", quote = F) + } +) diff --git a/functions/network.R b/functions/network.R new file mode 100644 index 0000000..11c0de9 --- /dev/null +++ b/functions/network.R @@ -0,0 +1,96 @@ +# main functions #### +getStoredNetsChoices_just_network <- function() { + snets <- StoredNets_just_network() + if (nrow(snets) == 0) return(NULL) + choices <- snets$id + names(choices) <- snets$name + return(choices) +} + +fetchFirstSelectedStoredIgraph_just_network <- function() { + dataset <- fetchFirstSelectedStoredDataset_just_network() + if (is.null(dataset)) + return(NULL) + else + return(convert_to_igraph(dataset)) +} + +# Download-button for automated annotations +file_name <- function() { + automated_annotations <- input$automated_annotations + + if (automated_annotations == "Fast-Greedy\tcluster_fast_greedy(igraph)") { + filename <- "Fast-Greedy_" + } + if (automated_annotations == "Louvain\tcluster_louvain(igraph)") { + filename <- "Louvain_" + } + if (automated_annotations == "Label-Propagation\tcluster_label_prop(igraph)") { + filename <- "Label-Propagation_" + } + if (automated_annotations == "Walktrap\tcluster_walktrap(igraph)") { + filename <- "Walktrap_" + } + if (automated_annotations == "Betweenness\tcluster_edge_betweenness(igraph)") { + filename <- "Betweenness_" + } + return(filename) +} + +# sub-routines #### +fetchFirstSelectedStoredDataset_just_network <- reactive({ + ssn <- SelectedStoredNets_just_network() + if (!is.null(ssn) && nrow(ssn) > 0) { + return(fetchDataset_just_network(ssn[1,]$id)) + } else { + return(NULL) + } +}) + +StoredNets_just_network <- reactive({ + return(reactiveVars$StoredNetworks_just_network) +}) + +SelectedStoredNets_just_network <- function() { + if (length(reactiveVars$SelectedStoredNetworksIds_just_network) > 0) { + return(StoredNets_just_network()[which( + reactiveVars$StoredNetworks_just_network$id %in% + reactiveVars$SelectedStoredNetworksIds_just_network + ),]) + } + else if (nrow(StoredNets_just_network()) == 0 || + is.na(StoredNets_just_network()[1,])) + return(NULL) + else { + updateCheckboxGroupInput( + session, + "uiLoadGraphOptionsOutput_just_network", + "Selected network(s)", + choices = getStoredNetsChoices_just_network(), + selected = getStoredNetsChoices_just_network()[1] + ) + return(StoredNets_just_network()[1,]) + } +} + +fetchDataset_just_network <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +convert_to_igraph <- function(dataset1){ + weighted_tf <- FALSE + if (attr(dataset1, "weighted")) { + weighted_tf <- attr(dataset1, "weighted") + } + set.seed(123) + igraph <- graph.data.frame(dataset1, directed = F, vertices = NULL) + if (attr(dataset1, which = "weighted")) + E(igraph)$weight <- dataset1$Weight + return(igraph) +} + diff --git a/functions/refreshing/convex_hulls_3D.R b/functions/refreshing/convex_hulls_3D.R new file mode 100644 index 0000000..83eb9b6 --- /dev/null +++ b/functions/refreshing/convex_hulls_3D.R @@ -0,0 +1,535 @@ +convex_hull_3D <- function() { + set.seed(123) + + g <- fetchFirstSelectedStoredIgraph_annotations_tab() + if (is.null(g)) + return() + + # dataset <- get.edgelist(g) + dataset <- fetchFirstSelectedStoredDataset_annotations_tab() + if (is.null(dataset)) + return() + + original_dataset_weighted <- fetchFirstSelectedStoredDataset_annotations_tab() + if (is.null(original_dataset_weighted)) + return(NULL) + + annotation_graph <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(annotation_graph)) + return() + + #---------------------------------------------------------------# + my_network<- as.data.frame(get.edgelist(g)) + my_network<- data.frame(Source = my_network$V1, Target = my_network$V2) + + groups <- as.data.frame(annotation_graph) + + groups<- data.frame(V1 = groups$Annotations, stri_split_fixed(groups$Nodes, ",", simplify = TRUE)) + groups<-mutate_all(groups, funs(na_if(.,""))) + number_of_groups<-dim(groups)[1] + + x <- list() + for (i in 1:number_of_groups) { + group_i<- groups[i,] + group_i<- group_i[,-1] + group_i <- group_i[!is.na(group_i)] + x[[i]]<- (group_i) + } + + GO <- list() + for (i in 1:number_of_groups) { + GO[[i]]<-rep(groups[i,1], length(x[[i]])) + } + + column1<-my_network$Source + column2<-my_network$Target + node_names<-unique(union(column1, column2)) + tt<-unlist(x) + nodes_with_NA_groups<-setdiff(node_names,tt) + + members <- data_frame(id=unlist(x),group = unlist(GO)) + members_with_NA_groups <- data_frame(id=unlist(x),group = unlist(GO)) + #----------------------------------------------------------------# + + #-------------------------------------------------------------------------# + + new_nodes <- unique(union(dataset[,1], dataset[,2])) + + new_annot <- annotation_graph %>% + mutate(V2 = strsplit(as.character(annotation_graph$Nodes), ",")) %>% + unnest(V2) + + intersect_g_annot <- intersect(new_nodes, new_annot$V2) + + if(identical(intersect_g_annot, character(0))){ + showModal(modalDialog( + title = "Important message", + "Please check if the selected annotation file corresponds to the selected network.", + easyClose = T + )) + } + #-------------------------------------------------------------------------# + + set.seed(123) + lay <- layout_choices_3D(g, lay) + + #----------------------------------------------------------------# + scene_scale_x_max <- max(lay[,1])*scaling_coordinates_convex_3D_X() + scene_scale_x_min <- min(lay[,1])*scaling_coordinates_convex_3D_X() + + scene_scale_y_max <- max(lay[,2])*scaling_coordinates_convex_3D_Y() + scene_scale_y_min <- min(lay[,2])*scaling_coordinates_convex_3D_Y() + + scene_scale_z_max <- max(lay[,3])*scaling_coordinates_convex_3D_Z() + scene_scale_z_min <- min(lay[,3])*scaling_coordinates_convex_3D_Z() + #----------------------------------------------------------------# + # lay <- lay*scaling_coordinates_convex_3D() + #-- Scaling coordinates --# + coorx<- lay[,1]*scaling_coordinates_convex_3D_X() + lay <- cbind(coorx,lay[,2:ncol(lay)]) + + coory<- lay[,2]*scaling_coordinates_convex_3D_Y() + lay <- cbind(lay[,1], coory,lay[,3]) + + coorz<- lay[,3]*scaling_coordinates_convex_3D_Z() + lay <- cbind(lay[,1:2], coorz) + #-------------------------# + + colnames(lay) <- c("x", "y", "z") + # node_names_3D <- unique(union(dataset[,1], dataset[,2])) + + + #---------------------------------# + + node_names_3D <- names(V(g)) + node_name_links_3D <- names(V(g)) + #--------------------------------# + + + if(length(node_names_3D) != nrow(lay)){ + showModal(modalDialog( + title = "Important message", + "Please check if the selected annotation file corresponds to the selected network.", + easyClose = T + )) + } + else{ + node_names_with_coords <- data.frame("Source" = node_names_3D, + "x" = lay[,1], + "y" = lay[,2], + "z" = lay[,3]) + } + + + #--- hash table for names and coordinates ---# + coordinates_hashmap <- new.env(hash = TRUE) + for(i in 1:nrow(node_names_with_coords)){ + coordinates_hashmap[[as.character(node_names_with_coords[i,1])]]<-c(node_names_with_coords[i,2], node_names_with_coords[i,3], node_names_with_coords[i,4]) + } + + #---------------------------------------------------# + + nrowdat <- nrow(dataset) + nrowannot <- nrow(annotation_graph) + + + if(!(is.weighted(g))){ + original_dataset_weighted <- cbind(original_dataset_weighted[,1:2],"Weight"=rep(0.5, nrow(original_dataset_weighted))) + } else{ + minx <- min(original_dataset_weighted[,3]) + maxx <- max(original_dataset_weighted[,3]) + + scaling_weight_values<- c() + for (i in 1:nrow(original_dataset_weighted)){ + scaling_weight_values_i <- mapper(original_dataset_weighted[i,3], minx, maxx, 0.5, 20) + scaling_weight_values <- c(scaling_weight_values, scaling_weight_values_i) + } + if(maxx > 50){ + original_dataset_weighted <- cbind(original_dataset_weighted[,1:2],"Weight"=scaling_weight_values) + } + } + + if (length(s)==0) + { + s<-c(1:nrowannot) + } + + if (length(s)) { + s<-sort(s)#----------------------------------- + x<- length(s) + + dataset1 <- get.edgelist(g) + dataset1 <- data.frame("Source" = dataset[,1], "Target" = dataset[,2]) + + source <- matrix(ncol=3, nrow=nrow(dataset1)) + target <- matrix(ncol=3, nrow=nrow(dataset1)) + for(i in 1:nrow(dataset1)){ + source[i,] <- coordinates_hashmap[[as.character(dataset1[i,1])]] + target[i,] <- coordinates_hashmap[[as.character(dataset1[i,2])]] + } + + edges_for_plotly <- data.frame("Source.x" = source[,1], "Source.y" = source[,2], "Source.z" = source[,3], + "Target.x" = target[,1], "Target.y" = target[,2], "Target.z" = target[,3]) + # x<- c() + # for(i in 1:nrow(edges_for_plotly)){ + # if(i == nrow(edges_for_plotly)){ + # a <- paste(edges_for_plotly$Source.x[i], edges_for_plotly$Target.x[i], "null", sep = "," ) + # }else{ + # a <- paste(edges_for_plotly$Source.x[i], edges_for_plotly$Target.x[i], "null,", sep = "," ) + # + # } + # x<- c(x, a) + # } + # + # x <- as.vector(unlist(x)) + # x <- paste(x, collapse=" ") + # + # y<- c() + # for(i in 1:nrow(edges_for_plotly)){ + # if(i == nrow(edges_for_plotly)){ + # a = paste(edges_for_plotly$Source.y[i], edges_for_plotly$Target.y[i], "null", sep = "," ) + # }else{ + # a = paste(edges_for_plotly$Source.y[i], edges_for_plotly$Target.y[i], "null,", sep = "," ) + # } + # y<- c(y, a) + # } + # + # y <- as.vector(unlist(y)) + # y <- paste(y, collapse=" ") + # + # + # z<- c() + # for(i in 1:nrow(edges_for_plotly)){ + # if(i == nrow(edges_for_plotly)){ + # a = paste(edges_for_plotly$Source.z[i], edges_for_plotly$Target.z[i], "null", sep = "," ) + # }else{ + # a = paste(edges_for_plotly$Source.z[i], edges_for_plotly$Target.z[i], "null,", sep = "," ) + # } + # z<- c(z, a) + # } + # + # z <- as.vector(unlist(z)) + # z <- paste(z, collapse=" ") + + #--------------------------------------------------------------------# + fileConn <- file(paste(USER_TEMP_FOLDER, "/convex_3D_", session$token,".html", sep=""), "w") + #--------------------------------------------------------------------# + + cat(sprintf(" + + + + + +
    + + + + + ", sep = ""), file = fileConn, append = T) + + + + } # if(length(s)) +} \ No newline at end of file diff --git a/functions/refreshing/modularity.R b/functions/refreshing/modularity.R new file mode 100644 index 0000000..edd6b90 --- /dev/null +++ b/functions/refreshing/modularity.R @@ -0,0 +1,25 @@ +modularity<- function(method){ + + set.seed(123) + + clp <- automated_annotation_choices(net, automated_annotations) + + groups_all<- c() + for (i in 1:length(clp)){ + clp_i <- as.data.frame(clp[[i]]) + ss<-paste(clp_i[,1], collapse="," ) + groups_all[[i]]<-ss + } + + prefix <- "Group-" + suffix <- seq(1:length(clp)) + + max.length <- max(sapply(groups_all, length)) + l <- lapply(groups_all, function(v) { c(v, rep(NA, max.length-length(v)))}) + df<-as.data.frame(do.call(rbind, l)) + groups_column_name<- paste(prefix, suffix, sep="") + df<- cbind(groups_column_name, df) + names(df) <- NULL + + return(df) +} diff --git a/functions/topology.R b/functions/topology.R new file mode 100644 index 0000000..5727c9c --- /dev/null +++ b/functions/topology.R @@ -0,0 +1,131 @@ +# main functions #### +getStoredNetsChoices_topology_tab <- function() { + snets <- StoredNets_topology_tab() + if (nrow(snets) == 0) + return(NULL) + choices <- snets$id + names(choices) <- snets$name + return(choices) +} + +getDatasetName <- function(id) { + sn <- StoredNets() + idi <- which(sn$id == id) + if (is.null(sn) || nrow(sn) == 0 || length(idi) == 0) + return(NULL) + return(sn[idi,]$name) +} + +fetchDataset_topology_tab <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +fetchAllSelectedStoredDataset_topology_tab <- function() { + ssn <- SelectedStoredNets_topology_tab() + ids <- c() + if (!is.null(ssn) && nrow(ssn) > 0) { + ret <- list() + for (i in 1:nrow(ssn)) { + ret[[i]] <- fetchDataset_topology_tab(ssn[i,]$id) + ids <- c(ids, ssn[i,]$id) + } + names(ret) <- ids + return(ret) + } else { + return(NULL) + } +} + +# Statistics +stat_dataset <- function(dataset, datasetName) { + res <- tryCatch({ + columns <- c("Statistic", paste0("Value for ", datasetName)) + if (is.null(dataset)) { + dataset <- EmptyDataset(columns) + } else { + igraph <- convert_to_igraph(dataset) + if (length(input$statistics) == 0) { + dataset <- EmptyDataset(columns) + } else { + dataset <- netstats(igraph, input$statistics) + colnames(dataset) <- columns + } + } + return(dataset) + }, warning = function(w) { + + }, error = function(e) { + cat(paste0("ERROR while executing stat_dataset: ", e, "\n")) + return(NULL) + }, finally = { + + }) + return(res) +} + +stat_dataset2 <- function(dataset, datasetName) { + res <- tryCatch({ + columns <- c("Statistic", paste0("Value for ", datasetName)) + if (is.null(dataset)) { + dataset <- EmptyDataset(columns) + } else { + igraph <- convert_to_igraph(dataset) + if (length(input$statistics2) == 0) { + dataset <- EmptyDataset(columns) + } else { + dataset <- netstats(igraph, input$statistics2) + colnames(dataset) <- columns + } + } + return(dataset) + }, warning = function(w) { + + }, error = function(e) { + cat(paste0("ERROR while executing stat_dataset: ", e, "\n")) + return(NULL) + }, finally = { + + }) + return(res) +} + +# sub-routines #### +SelectedStoredNets_topology_tab <- function() { + if (length(reactiveVars$SelectedStoredNetworksIds_topology_tab) > 0) { + return(StoredNets_topology_tab()[which( + reactiveVars$StoredNetworks_topology_tab$id %in% + reactiveVars$SelectedStoredNetworksIds_topology_tab + ),]) + } + else if (nrow(StoredNets_topology_tab()) == 0 || + is.na(StoredNets_topology_tab()[1,])) + return(NULL) + else { + updateCheckboxGroupInput( + session, + "uiStoredGraphsOutputSelectTopolopgy", + "Selected network(s)", + choices = getStoredNetsChoices_topology_tab(), + selected = getStoredNetsChoices_topology_tab()[1] + ) + return(StoredNets_topology_tab()[1,]) + } +} + +netstats <- function(igraph,statistics){ + if(length(statistics)==0) + return(NULL) + results<-list() + for(i in statistics){ + tmp<-unlist(strsplit(i,"\t",fixed=T)) + description<-tmp[1] + command<-tmp[2] + results[[description]]<-eval(parse(text=command)) + } + return(data.frame(cbind(names(results),as.character(results)))) +} diff --git a/functions/upload.R b/functions/upload.R new file mode 100644 index 0000000..99295ae --- /dev/null +++ b/functions/upload.R @@ -0,0 +1,386 @@ +# main functions #### +addNetwork <- function(){ + dataset <- loadNetworkFromFile() + + if (!is.null(dataset)) { + if (nrow(dataset) < 10000) { + nid <- UUIDgenerate(T) #time-base UUID is generated + nn <- input$networkName + cnt <- 1 #count + while (nn %in% reactiveVars$StoredNetworks$name) { + #reactiveVars: represents a single reactive variable. + cnt <- cnt + 1 + nn <- + paste(input$networkName, cnt) #paste: converts its arguments (via as.character) to character strings + } + df <- data.frame(id = nid, name = nn, stringsAsFactors = F) + if (nrow(dataset) > 10000) { + dataset <- dataset[1:10000,] + } + if (ncol(dataset) == 3) attr(dataset, which = 'weighted') <- T + else attr(dataset, which = 'weighted') <- F + + reactiveVars$StoredNetworks <- + rbind(reactiveVars$StoredNetworks, df) + reactiveVars$StoredNetworks_just_network <- + reactiveVars$StoredNetworks + reactiveVars$StoredNetworks_annotations_tab <- + reactiveVars$StoredNetworks + reactiveVars$StoredNetworks_topology_tab <- + reactiveVars$StoredNetworks + + saveRDS(dataset, paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + if (length(reactiveVars$SelectedStoredNetworksIds) == 0) { + reactiveVars$SelectedStoredNetworksIds <- c(nid) + } + } + + if (nrow(dataset) >= 10000) { + dataset <- dataset[1:10000, ] + shinyalert("Warning!", "Keeping the first 10,000 connections.", type = "error") + } + + } else shinyalert("Error!", "Your input generated a NULL network.\n + Please, follow the guidelines at the Help pages to upload a network in the required format.", type = "error") +} + +remNetwork <- function(){ + if (!is.null(input$availableNetworks)) { + reactiveVars$StoredNetworks <- + reactiveVars$StoredNetworks[-which(reactiveVars$StoredNetworks$id %in% + input$availableNetworks),] + nr <- nrow(reactiveVars$StoredNetworks) + if (nr > 0) { + reactiveVars$SelectedStoredNetworksIds <- + reactiveVars$StoredNetworks[nr,]$id + } + } + + if (!is.null(input$availableNetworks)) { + reactiveVars$StoredNetworks_just_network <- + reactiveVars$StoredNetworks_just_network[-which(reactiveVars$StoredNetworks_just_network$id %in% + input$availableNetworks),] + nr <- nrow(reactiveVars$StoredNetworks_just_network) + if (nr > 0) { + reactiveVars$SelectedStoredNetworksIds_just_network <- + reactiveVars$StoredNetworks_just_network[nr,]$id + } + } + + if (!is.null(input$availableNetworks)) { + reactiveVars$StoredNetworks_annotations_tab <- + reactiveVars$StoredNetworks_annotations_tab[-which( + reactiveVars$StoredNetworks_annotations_tab$id %in% + input$availableNetworks + ),] + nr <- nrow(reactiveVars$StoredNetworks_annotations_tab) + if (nr > 0) { + reactiveVars$SelectedStoredNetworksIds_annotations_tab <- + reactiveVars$StoredNetworks_annotations_tab[nr,]$id + } + } + + if (!is.null(input$availableNetworks)) { + reactiveVars$StoredNetworks_topology_tab <- + reactiveVars$StoredNetworks_topology_tab[-which(reactiveVars$StoredNetworks_topology_tab$id %in% + input$availableNetworks),] + nr <- nrow(reactiveVars$StoredNetworks_topology_tab) + if (nr > 0) { + reactiveVars$SelectedStoredNetworksIds_topology_tab <- + reactiveVars$StoredNetworks_topology_tab[nr,]$id + } + } +} + +addAnnotations <- function(){ + annotation <- loadAnnotations() + if (!is.null(annotation)) { + if (nrow(annotation) < 300) { + nid <- UUIDgenerate(T) #time-base UUID is generated + nn <- input$annotationName + cnt <- 1 #count + while (nn %in% reactiveVars$StoredAnnotations$name) { + #reactiveVars: represents a single reactive variable. + cnt <- cnt + 1 + nn <- + paste(input$annotationName, cnt) #paste: converts its arguments (via as.character) to character strings + } + dtf <- data.frame(id = nid, + name = nn, + stringsAsFactors = F) + if (nrow(annotation) > 10000) { + annotation <- annotation[1:10000,] + } + reactiveVars$StoredAnnotations <- + rbind(reactiveVars$StoredAnnotations, dtf) + reactiveVars$StoredNetworks2_annotations_tab <- + reactiveVars$StoredAnnotations + + saveRDS(annotation, paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + if (length(reactiveVars$SelectedStoredAnnotationIds) == 0) { + reactiveVars$SelectedStoredAnnotationIds <- c(nid) + } + } + + if (nrow(annotation) >= 300) { + annotation <- annotation[1:300, ] + shinyalert("Warning!", "Keeping the first 300 lines.", type = "error") + } + + } else shinyalert("Error!", "Your input generated a NULL annotation table.\n + Please, follow the guidelines at the Help pages to upload an annotation file in the required format.", type = "error") +} + +remAnnotations <- function(){ + if (!is.null(input$availableAnnotations)) { + reactiveVars$StoredAnnotations <- + reactiveVars$StoredAnnotations[-which(reactiveVars$StoredAnnotations$id %in% + input$availableAnnotations),] + nr <- nrow(reactiveVars$StoredAnnotations) + if (nr > 0) + reactiveVars$SelectedStoredAnnotationIds <- + reactiveVars$StoredAnnotations[nr,]$id + } + + + if (!is.null(input$availableAnnotations)) { + reactiveVars$StoredNetworks2_annotations_tab <- + reactiveVars$StoredNetworks2_annotations_tab[-which( + reactiveVars$StoredNetworks2_annotations_tab$id %in% + input$availableAnnotations + ),] + nr <- nrow(reactiveVars$StoredNetworks2_annotations_tab) + if (nr > 0) + reactiveVars$SelectedStoredNetworksIds2_annotations_tab <- + reactiveVars$StoredNetworks2_annotations_tab[nr,]$id + } +} + +addExpression <- function(){ + expression <- loadExpressions() + if (!is.null(expression)) { + nid <- UUIDgenerate(T) #time-base UUID is generated + nn <- input$expressionName + cnt <- 1 #count + while (nn %in% reactiveVars$StoredExpressions$name) { + #reactiveVars: represents a single reactive variable. + cnt <- cnt + 1 + nn <- + paste(input$expressionName, cnt) #paste: converts its arguments (via as.character) to character strings + } + dtf <- data.frame(id = nid, + name = nn, + stringsAsFactors = F) + if (nrow(expression) > 10000) { + expressionName <- expressionName[1:10000,] + shinyalert("Warning!", "Keeping the 10000 first rows.", type = "error") + } + reactiveVars$StoredExpressions <- + rbind(reactiveVars$StoredExpressions, dtf) + saveRDS(expression, paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + if (length(reactiveVars$SelectedStoredExpressionIds) == 0) { + reactiveVars$SelectedStoredExpressionIds <- c(nid) + } + } else shinyalert("Error!", "Your input generated a NULL expressions table.\n + Please, follow the guidelines at the Help pages to upload an annotation file in the required format.", type = "error") +} + +remExpression <- function(){ + if (!is.null(input$availableExpressions)) { + reactiveVars$StoredExpressions <- + reactiveVars$StoredExpressions[-which(reactiveVars$StoredExpressions$id %in% + input$availableExpressions),] + nr <- nrow(reactiveVars$StoredExpressions) + if (nr > 0) + reactiveVars$SelectedStoredExpressionIds <- + reactiveVars$StoredExpressions[nr,]$id + } +} + +getStoredNetsChoices <- function() { + snets <- StoredNets() + if (nrow(snets) == 0) + return(NULL) + choices <- snets$id + names(choices) <- snets$name + return(choices) +} + +SelectedStoredNets <- function() { + if (length(reactiveVars$SelectedStoredNetworksIds) > 0) { + return(StoredNets()[which(reactiveVars$StoredNetworks$id %in% + reactiveVars$SelectedStoredNetworksIds),]) + } + else if (nrow(StoredNets()) == 0 || + is.na(StoredNets()[1,])) + return(NULL) + else return(StoredNets()[1,]) +} + +fetchDataset <- function(nid) { + retVal <- NULL + if (length(nid) > 0) { + retVal <- readRDS(paste0(USER_TEMP_FOLDER, "/", nid, ".rda")) + attr(retVal, "id") <- nid + } + return(retVal) +} + +getStoredAnnotChoices <- function() { + sannots <- StoredAnnots() + if (nrow(sannots) == 0) + return(NULL) + choices <- sannots$id + names(choices) <- sannots$name + return(choices) +} + +# sub-routines #### +loadNetworkFromFile <- function() { + dataset1 <- NULL + set.seed(123) + + switch( + input$uiLoadGraphOptionsInput, + oF = { + if (!is.null(input$file1)) { + tryCatch({ + dataset1 <- read_data(input$file1$datapath) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Network format problem. Please ensure that columns are tab separated.", type = "error") + }) + } + }, + oR_String_interactions = { + dataset1 <- string_net_bcar3 # "Examples/BCAR3/BCAR3.txt" + dataset1 <- cbind(dataset1[,1:2], "Weight" = rep(1, nrow(dataset1))) + }, + oR_Drosophila = { + n <- as.integer(input$oR_selected_size) + dataset1 <- dros_net # "Examples/TAU/TAU_network_DEGs_NORMA.txt" + dataset1 <- cbind(dataset1[,1:2], "Weight" = rep(1, nrow(dataset1))) + } + ) + + if (input$uiLoadGraphOptionsInput != "oF" && !is.null(dataset1)) { + set.seed(123) + if(ncol(dataset1) < 3) { + dataset1 <- cbind(dataset1[,1:2], "Weight" = rep(1, nrow(dataset1))) + } else { + if (ncol(dataset1) == 2) { + dataset1$V3 <- 1 + } else if (ncol(dataset1) > 3) { + dataset1 <- dataset1[, 1:3] + } else if (ncol(dataset1) != 3) + return(NULL) + } + colnames(dataset1) <- c("Source", "Target", "Weight") + } + + if (!is.null(dataset1)){ + row_to_keep <- c() + for(i in 1:nrow(dataset1)){ + if(dataset1[i,1]==dataset1[i,2]){ + row_to_keep <- c(row_to_keep, FALSE) + } + if(dataset1[i,1]!=dataset1[i,2]){ + row_to_keep <- c(row_to_keep, TRUE) + } + } + dataset1 = dataset1[row_to_keep,] + } + + return(dataset1) +} + +# Load annotations +loadAnnotations <- function() { + annotation1 <- NULL + + switch( + input$uiLoadGraphOptionsInput_annotations, + oF = { + if (!is.null(input$file2)) { + tryCatch({ + annotation1 <- read_annotations(input$file2$datapath) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Annotations format problem. Please ensure that columns are tab separated.", type = "error") + }) + } + }, + oR_String_Annotation_BP = { + annotation1 <- string_bp # "Examples/BCAR3/BCAR3_GO_BP.txt" + }, + oR_String_Annotation_MF = { + annotation1 <- string_mf # "Examples/BCAR3/BCAR3_GO_MF.txt" + }, + oR_String_Annotation_KEGG = { + annotation1 <- string_kegg # "Examples/BCAR3/BCAR3_KEGG.txt" + }, + oR_Drosophila_KEGG = { + # n <- as.integer(input$oR_selected_size) + annotation1 <- dros_annot # "Examples/TAU/TAU_KEGG_Annotation_NORMA.txt" + }, + oR_Drosophila_Luvain = { + # n <- as.integer(input$oR_selected_size) + annotation1 <- dros_louvain # "Examples/TAU/TAU_Louvain.txt" + } + ) + + if (!is.null(annotation1)) { + colnames(annotation1) <- c("Annotations", "Nodes") + } + + return(annotation1) +} + +# Load expression files +loadExpressions <- function() { + expression1 <- NULL + switch(input$uiLoadExpressionsInput, + oF = { + if (!is.null(input$file3)) { + tryCatch({ + expression1 <- read_expressions(input$file3$datapath) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Expressions format problem. Please ensure that columns are tab separated.", type = "error") + }) + } + }, + oR_Expression_file_Drosophila = { + expression1 <- dros_express # "Examples/TAU/TAU_expressions.txt" + } + ) + if (!is.null(expression1)) { + colnames(expression1) <- c("ID", "Color") + } + return(expression1) +} + +read_expressions <- function(datapath, type = c("txt"), header = F, sep = "\t", + quote = "\"", weighted = F, na.strings = c("", "NA")){ + expression1 <- read.table(datapath, header = header, sep = sep, + quote = quote, comment.char="?") +} + +read_data <- function(datapath, type = c("txt"), header = T, sep = "\t", quote = "\"", weighted = F){ + dataset1 <- read.table(datapath, header = header, sep = sep, quote = quote) + + if (ncol(dataset1) == 2) dataset1$V3 <- 1 + else if (ncol(dataset1) > 3) dataset1 <- dataset1[, 1:3] + else if (ncol(dataset1) != 3) return(NULL) + + colnames(dataset1) <- c("Source", "Target", "Weight") + + return(dataset1) +} + +read_annotations <- function(datapath, type = c("txt"), header = F, sep = "\t", + quote = "\"", weighted = F, na.strings = c("", "NA")){ + annotation1 <- read.table(datapath, header = header, sep = sep, quote = quote) + + return(annotation1) +} diff --git a/global.R b/global.R new file mode 100644 index 0000000..ee805bc --- /dev/null +++ b/global.R @@ -0,0 +1,231 @@ +# Load libraries or install dependencies #### +if (!require(shinyBS)) install.packages('shinyBS') +if (!require(bsplus)) install.packages('bsplus') +if (!require(visNetwork)) install.packages("visNetwork") +if (!require(shinyWidgets)) install.packages('shinyWidgets') +if (!require(shinythemes)) install.packages('shinythemes') +if (!require(shinyjs)) install.packages('shinyjs') +if (!require(shinyalert)) install.packages('shinyalert') +if (!require(shinycssloaders)) install.packages('shinycssloaders') +if (!require(randomcoloR)) install.packages('randomcoloR') +if (!require(DT)) install.packages('DT') +if (!require(uuid))install.packages('uuid') +if (!require(igraph)) install.packages('igraph') +if (!require(stringi)) install.packages('stringi') +if (!require(dplyr)) install.packages('dplyr') +if (!require(tidyr)) install.packages('tidyr') +if (!require(stringr)) install.packages('tidyr') +if (!require(VennDiagram)) install.packages("VennDiagram") +if (!require(lattice)) install.packages("lattice") +if (!require(purrr)) install.packages('purrr') + +# Options #### +options(shiny.usecairo = F) +options(shiny.maxRequestSize = 30*1024^2) # 30 MB for uploaded networks +# options(shiny.reactlog = TRUE) # debugging +# options(shiny.error = browser) # debugging + +# Global variables #### +PRINT_TIMES <- F # true, to benchmark some function speeds +USER_TEMP_FOLDER <- 'user_temp' + +ui_options <- c(ui_table_font_sz = "80%") +layouts_ui <- c( + "Fruchterman-Reingold"="Fructerman\tlayout_nicely(igraph, dim=2)", + # "Fruchterman-Reingold"="Fructerman\tlayout.fruchterman.reingold(igraph, dim=2)", + "Random"="Random\tlayout.random(igraph, dim=2)", + "Circle"="Circle\tlayout.circle(igraph)", + "Kamada-Kawai"="Kamada-Kawai\tlayout.kamada.kawai(igraph, dim=2)", + "Reingold-Tilford"="Reingold-Tilford\tlayout.reingold.tilford(igraph)", + "Lgl"="Lgl\tlayout.lgl(igraph)", + "Graphopt"="Graphopt\tlayout.graphopt(igraph)", + "Gem"="Graphopt\tlayout.gem(igraph)", + "Star"="Graphopt\tlayout_as_star(igraph)", + "Grid"="Grid\tlayout.grid(igraph)" + # "Auto"="Auto\tlayout.auto(igraph, dim=2)", + # "SVD"="SVD\tlayout.svd(igraph, d=shortest.paths(igraph))" + # "Nicely"="Nicely\tlayout_nicely(igraph, dim=2)", + # "Tree"="Graphopt\tlayout_as_tree(igraph)", + # "Spring"="Spring\tlayout.spring(igraph)", + # "Fruchterman-Reingold Grid"="Fruchterman Grid\tlayout.fruchterman.reingold.grid(igraph)", + # "Sphere"="Sphere\tlayout.sphere(igraph)", +) +selected_layouts <- c( "Fructerman\tlayout_nicely(igraph, dim=2)" ) +layouts_3D<-c( + "Fruchterman-Reingold"="Fructerman\tlayout_with_fr(igraph, dim=3)", + "Random"="Random\tlayout.random(igraph, dim=3)", + "Kamada-Kawai"="Kamada-Kawai\tlayout.kamada.kawai(igraph, dim=3)", + # "Reingold-Tilford"="Reingold-Tilford\tlayout.reingold.tilford(igraph, dim=3)", + # "Lgl"="Lgl\tlayout.lgl(igraph, dim=3)", + # "Graphopt"="Graphopt\tlayout.graphopt(igraph, dim=3)", + # "Gem"="Graphopt\tlayout.gem(igraph, dim=3)", + "Grid"="Grid\tlayout.grid(igraph, dim=3)" +) +selected_layouts_3d <- c( "Fructerman\tlayout_with_fr(igraph, dim=3)" ) +automated_annotations_ui <- c( + # "Fast-Greedy"="Fast-Greedy\tcluster_fast_greedy(igraph)", + "Louvain"="Louvain\tcluster_louvain(igraph)", + "Label-Propagation"="Label-Propagation\tcluster_label_prop(igraph)", + "Walktrap"="Walktrap\tcluster_walktrap(igraph)", + "Betweenness"="Betweenness\tcluster_edge_betweenness(igraph)" +) +selected_automated_annotations <- c( "Louvain\tcluster_louvain(igraph)" ) +statistics <- c( + "Number of Edges" = "Number of Edges\tecount(igraph)", + "Number of Nodes" = "Number of Nodes\tvcount(igraph)", + "Density" = "Density\tgraph.density(igraph)", + "Average path length" = "Average path length\taverage.path.length(igraph)", + "Clustering Coefficient" = "Clustering Coefficient\ttransitivity(igraph)", + "Modularity" = "Modularity\tmodularity(igraph,membership(walktrap.community(igraph)))", + "Average Eccentricity" = "Average Eccentricity\tmean(eccentricity(igraph))", + "Average number of Neighbors" = "Average number of Neighbors\t(centr_eigen(igraph)$centralization)", + "Centralization betweenness" = "Centralization.betweenness\tcentralization.betweenness(igraph)$centralization", + # "Centralization closeness" = "Centralization.closeness\tcentralization.closeness(igraph)$centralization", + "Centralization degree" = "Centralization.degree\tcentralization.degree(igraph)$centralization" +) +selected_statistics <- c("Number of Edges" = "Number of Edges\tecount(igraph)") + +max_pixels_panel <- 5000 + +# 300 Colors - Up to 100 are distinct +qual_col_pals<-c("#1B9E77","#D95F02","#7570B3","#E7298A","#66A61E","#E6AB02","#A6761D","#666666","#7FC97F","#BEAED4", + "#FDC086","#FFFF99","#386CB0","#F0027F","#BF5B17","#A6CEE3","#1F78B4","#B2DF8A","#33A02C","#FB9A99", + "#E31A1C","#FDBF6F","#FF7F00","#CAB2D6","#a17bc9","#B15928","#FBB4AE","#B3CDE3","#CCEBC5","#a9a9a9", + "#dcdcdc","#98fb98","#556b2f","#8b4513","#6b8e23","#2e8b57","#800000","#FDDAEC","#006400","#808000", + "#6c3920","#778899","#3cb371","#bc8f8f","#b0737c","#008080","#b8860b","#bdb76b","#cd853f","#4682b4", + "#d2691e","#9acd32","#20b2aa","#008b7b","#82007e","#32cd32","#8fbc8f","#0ba47e","#b03060","#d2b48c", + "#66cdaa","#9932cc","#ff0000","#ffa500","#ffd700","#ffff00","#c71585","#0303ff","#7fff00","#00ff00", + "#ba55d3","#00ff7f","#4169e1","#e9967a","#dc143c","#00ffff","#00bfff","#9370db","#0000ff","#a020f0", + "#adff2f","#d8bfd8","#ff7f50","#ff00ff","#db7093","#f0e68c","#fa8072","#ffff54","#6495ed","#dda0dd", + "#87ceeb","#ff1493","#afeeee","#ee82ee","#2f4f4f","#7fffd4","#ff69b4","#ffe4c4","#ffb6c1","#DECBE4", + "#FED9A6","#FFFFCC","#E5D8BD","#191970","#F2F2F2","#B3E2CD","#FDCDAC","#CBD5E8","#F4CAE4","#E6F5C9", + "#FFF2AE","#F1E2CC","#CCCCCC","#E41A1C","#377EB8","#4DAF4A","#984EA3","#FFFF33","#A65628","#F781BF", + "#999999","#66C2A5","#FC8D62","#8DA0CB","#E78AC3","#A6D854","#FFD92F","#E5C494","#B3B3B3","#8DD3C7", + "#FFFFB3","#BEBADA","#FB8072","#80B1D3","#FDB462","#B3DE69","#FCCDE5","#D9D9D9","#BC80BD","#095F02", + "#E31A97","#A81AD2","#74ED33","#38F76B","#E607E1","#17D214","#E2E749","#5522DF","#1FF696","#1406E9", + "#6A43DE","#C2F309","#E92D57","#5004DE","#9C4AE9","#2EF613","#EDAA2D","#2FB5EA","#47FAB2","#CC9221", + "#5C4AFC","#E843D0","#FE3F64","#21DD3A","#1988D2","#BC11EF","#EEFB3D","#1B9E77","#D95F02","#7570B3", + "#88e99a","#ca2dc5","#bce333","#643176","#34f50e","#b22839","#4be8f9","#6c3920","#bfcd8e","#1642cd", + "#f4d403","#5310f0","#609111","#c697f4","#34466d","#b3d9fa","#155126","#fab5b5","#0ba47e","#ff0087", + "#3d99ce","#bf711e","#fa718e","#798872","#fe5900","#b0737c","#E31A97","#A81AD2","#74ED33","#38F76B", + "#DECBE4","#FED9A6","#FFFFCC","#E5D8BD","#FDDAEC","#F2F2F2","#B3E2CD","#FDCDAC","#CBD5E8","#F4CAE4", + "#E6F5C9","#FFF2AE","#F1E2CC","#CCCCCC","#E41A1C","#377EB8","#4DAF4A","#984EA3","#FFFF33","#A65628", + "#F781BF","#999999","#66C2A5","#FC8D62","#8DA0CB","#E78AC3","#A6D854","#FFD92F","#E5C494","#B3B3B3", + "#8DD3C7","#FFFFB3","#BEBADA","#FB8072","#80B1D3","#FDB462","#B3DE69","#FCCDE5","#D9D9D9","#BC80BD", + "#095F02","#E31A97","#A81AD2","#74ED33","#38F76B","#E607E1","#17D214","#E2E749","#5522DF","#1FF696", + "#1406E9","#6A43DE","#C2F309","#E92D57","#5004DE","#9C4AE9","#2EF613","#EDAA2D","#2FB5EA","#47FAB2", + "#CC9221","#5C4AFC","#E843D0","#FE3F64","#21DD3A","#1988D2","#BC11EF","#EEFB3D","#1B9E77","#D95F02", + "#7570B3","#88e99a","#ca2dc5","#bce333","#643176","#34f50e","#b22839","#4be8f9","#483d8b","#bfcd8e", + "#1642cd","#f4d403","#5310f0","#609111","#c697f4","#34466d","#b3d9fa","#155126","#fab5b5","#800080", + "#ff0087","#3d99ce","#bf711e","#fa718e","#798872","#fe5900","#663399","#E31A97","#A81AD2","#74ED33") + +# Images for help pages and banner +b64_1 <- base64enc::dataURI(file = "./www/Figures/Banner.png", mime = "image/png") +b64_2 <- base64enc::dataURI(file = "./www/Figures/Upload-1.png", mime = "image/png") +b64_3 <- base64enc::dataURI(file = "./www/Figures/Upload-2.png", mime = "image/png") +b64_4 <- base64enc::dataURI(file = "./www/Figures/Upload-3.png", mime = "image/png") +b64_5 <- base64enc::dataURI(file = "./www/Figures/Network_view_interactive.png", mime = "image/png") +b64_6 <- base64enc::dataURI(file = "./www/Figures/Community_detection_algorithms.png", mime = "image/png") +b64_7 <- base64enc::dataURI(file = "./www/Figures/Convex_Hulls.PNG", mime = "image/png") +b64_8 <- base64enc::dataURI(file = "./www/Figures/Pies.PNG", mime = "image/png") +b64_9 <- base64enc::dataURI(file = "./www/Figures/Expression_colors.png", mime = "image/png") +b64_10 <- base64enc::dataURI(file = "./www/Figures/Topology.PNG", mime = "image/png") +b64_11 <- base64enc::dataURI(file = "./www/Figures/Topology_comparisons.PNG", mime = "image/png") +b64_12 <- base64enc::dataURI(file = "./www/Figures/venn.png", mime = "image/png") +b64_3D_convex <- base64enc::dataURI(file = "./www/Figures/Convex_Hulls_3D.PNG", mime = "image/png") +b64_strategy1 <- base64enc::dataURI(file = "./www/Figures/Strategy_1.png", mime = "image/png") +b64_strategy2 <- base64enc::dataURI(file = "./www/Figures/Strategy_2.png", mime = "image/png") +b64_strategy3 <- base64enc::dataURI(file = "./www/Figures/Strategy_3.png", mime = "image/png") + + + +# Help pages examples for download +dros_net <- read.delim("./www/Examples/TAU/TAU_network_DEGs_NORMA.txt", header = T) +dros_annot <- read.delim("./www/Examples/TAU/TAU_KEGG_Annotation_NORMA.txt", header = F) +dros_louvain <- read.delim("./www/Examples/TAU/TAU_Louvain.txt", header = F) +dros_express <- read.delim("./www/Examples/TAU/TAU_expressions.txt", header = F) +string_net_tp53 <- read.delim("./www/Examples/TP53/string_interactions.txt", header = T) +string_annot <- read.delim("./www/Examples/TP53/string_interactions_groups_comma_duplicate.txt", header = F) +string_expr <- read.delim("./www/Examples/TP53/string_expression_colors.txt", header = F) +string_net_bcar3 <- read.delim("./www/Examples/BCAR3/BCAR3.txt", header = T) +string_bp <- read.delim("./www/Examples/BCAR3/BCAR3_GO_BP.txt", header = F) +string_mf <- read.delim("./www/Examples/BCAR3/BCAR3_GO_MF.txt", header = F) +string_kegg <- read.delim("./www/Examples/BCAR3/BCAR3_KEGG.txt", header = F) +co_express <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_NETWORK.txt", header = T) +co_express_bp <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_BP.txt", header = F) +co_express_mf <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_MF.txt", header = F) +co_express_cc <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_CC.txt", header = F) +co_express_kegg <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_KEGG.txt", header = F) +co_express_mcode <- read.delim("./www/Examples/Human_Coexpression/NORMA_Human_coexpression_Expression_MCODE.txt", header = F) +covid_19_net <- read.delim("./www/Examples/IntAct_COVID19/Intact-data_COVID19_no_self_loops.txt", header = T) +covid_19_interpro <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt", header = F) +covid_19_bp <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt", header = F) +covid_19_mf <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt", header = F) +covid_19_cc <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt", header = F) +covid_19_kegg <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt", header = F) +covid_19_smart <- read.delim("./www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_SMART_FILTERED.txt", header = F) +Gallus_gallus_net <- read.delim("./www/Examples/BioGrid_Chicken_Gallus/Biogrid_no_self_loops.txt", header = T) +Gallus_gallus_kegg <- read.delim("./www/Examples/BioGrid_Chicken_Gallus/BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt", header = F) + +R_script <- read.delim("./www/annotation_cleaner.R", header = F) + +# General functions #### +automated_annotation_choices <- function(igraph,automated_annotations_ui){ + if(length(automated_annotations_ui)==0) return(NULL) + results<-list() + + for(i in automated_annotations_ui){ + tmp<-unlist(strsplit(i,"\t",fixed=T)) + description<-tmp[1] + command<-tmp[2] + results[[description]] <- eval(parse(text=command)) + } + return(results[[description]]) +} + +# Colors if there is NOT "NA" in dataset (nodes) +group_pal_rows <- function(n){ + + qual_col_pals[1:300] + + if(n>=300){ + qual_col_pals<-c(qual_col_pals, rep(c("grey50"), times = (n-300) )) + nn<- qual_col_pals[1:n] + nnames<-c(nn) + } else { + nn<- qual_col_pals[1:n] + nnames<-c(nn) + } +} + +mapper <- function(value, istart, istop, ostart, ostop){ + return (ostart + (ostop - ostart) * ((value - istart) / (istop - istart))) +} + +# ui helper function +ui_dataTable_panel <- function(datasetName, pagination = TRUE) { + return(parse( + text = paste0( + "div(div(DT::dataTableOutput('", + datasetName, + "'), class='box-panel-padding'), class='box-panel')" + ) + )) +} + +EmptyDataset <- function(columns) { + dataset <- data.frame(V1 = integer()) + lapply(columns[-1], function(x) + dataset[, x] <<- integer()) + colnames(dataset) <- columns + return(dataset) +} + +# # DO ONCE +# css_colors <- group_pal_rows(300) +# css_generated <- "" +# for (i in 1: 300){ +# css_generated <- paste(css_generated, ".x", i, "{background-color: ", css_colors[i],";}","table.dataTable tr.selected td.x",i,"{background-color: ",css_colors[i], " !important;}", sep="") +# css_generated <- paste(css_generated," ", sep="\n") +# } +# print(css_generated) diff --git a/norma.config.hosted.json b/norma.config.hosted.json deleted file mode 100644 index 6169e10..0000000 --- a/norma.config.hosted.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "_comment": "Settings for the normaviz.org public server, behind an HTTPS reverse proxy (see deploy/apache-normaviz.conf). Copy to norma.config.json or pass with --config.", - "mode": "hosted", - "server": { - "host": "127.0.0.1", - "port": 8000, - "publicUrl": "https://normaviz.org/", - "trustProxy": true, - "hsts": true, - "accessLog": "anonymous", - "openBrowser": false - }, - "api": { "enabled": true, "ttlHours": 24, "maxMB": 50, "maxSessions": 500 }, - "relays": { "string": true, "arena3d": true, "databases": true }, - "site": { - "name": "NORMA", - "institution": "TODO: your institute, City, Country", - "contactName": "The NORMA team", - "contactEmail": "admin@normaviz.org", - "contactUrl": "https://github.com/PavlopoulosLab/NORMA/issues", - "sourceUrl": "https://github.com/PavlopoulosLab/NORMA", - "licenceName": "MIT License", - "licenceUrl": "https://opensource.org/licenses/MIT", - "privacyUrl": "", - "imprintUrl": "", - "maintainedUntil": "2031", - "notice": "", - "testedBrowsers": ["Chrome", "Firefox", "Safari", "Edge"] - }, - "app": { "maxNodes": 10000, "theme": "white", "startTab": "welcome", "cdnFallback": false } -} diff --git a/norma.config.json b/norma.config.json deleted file mode 100644 index 4dd8427..0000000 --- a/norma.config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "_comment": "Settings for running NORMA on this computer. See norma.config.hosted.json for a public server and README.md for every option.", - "mode": "local", - "server": { "host": "127.0.0.1", "port": 8000, "accessLog": "off" }, - "api": { "enabled": true, "ttlHours": 24, "maxMB": 50, "maxSessions": 500 }, - "relays": { "string": true, "arena3d": true, "databases": true }, - "app": { "maxNodes": 10000, "theme": "white", "startTab": "welcome", "cdnFallback": true } -} diff --git a/run_local.bat b/run_local.bat deleted file mode 100644 index b1115b0..0000000 --- a/run_local.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -REM Start NORMA on this computer and open it in the browser (Windows). -cd /d "%~dp0" -if not exist frontend\dist\norma.html ( - echo Building the page (needs Node 22 and npm)... - cd frontend && call npm install && call npm run build && cd .. -) -python backend\server.py --mode local %* -pause diff --git a/run_local.sh b/run_local.sh deleted file mode 100755 index 902c461..0000000 --- a/run_local.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# Start NORMA on this computer and open it in the browser (macOS, Linux). -cd "$(dirname "$0")" -if [ ! -f frontend/dist/norma.html ]; then - echo "Building the page (needs Node 22 and npm)..." - (cd frontend && npm install && npm run build) || exit 1 -fi -exec python3 backend/server.py --mode local "$@" diff --git a/server.R b/server.R new file mode 100644 index 0000000..b263007 --- /dev/null +++ b/server.R @@ -0,0 +1,1459 @@ +shinyServer(function(input, output, session) { + # source function files + source('./functions/upload.R', local=TRUE) + source('./functions/network.R', local=TRUE) + source('./functions/annotations.R', local=TRUE) + source('./functions/topology.R', local=TRUE) + source('./functions/helpDownloadHandlers.R', local=TRUE) + + set.seed(123) + + # ~~~GLOBAL VARS~~~ #### + colors <- randomColor(50) + area1<- NULL + area2<- NULL + v1<- NULL + v2<- NULL + + # reactiveValues #### + reactiveVars <- reactiveValues() + reactiveVars$StoredNetworks <- data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredNetworksIds <- c() + reactiveVars$StoredAnnotations <- data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredAnnotationIds <- c() + reactiveVars$StoredExpressions <- data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredExpressionIds <- c() + reactiveVars$StoredNetworks_just_network <- data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredNetworksIds_just_network <- c() + ### Duplicated upload for selctInput boxes of networks in annotation-tab (_annotations_tab)### + reactiveVars$StoredNetworks_annotations_tab <- + data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredNetworksIds_annotations_tab <- c() + ### Duplicated upload for annotations in annotation-tab ### + reactiveVars$StoredNetworks2_annotations_tab <- + data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredNetworksIds2_annotations_tab <- c() + ### Topology tab ### + reactiveVars$StoredNetworks_topology_tab <- + data.frame(id = character(), + name = character(), + stringsAsFactors = F) + reactiveVars$SelectedStoredNetworksIds_topology_tab <- c() + + # reactive #### + StoredNets <- reactive({ return(reactiveVars$StoredNetworks) }) + StoredNets_annotations_tab <- reactive({ return(reactiveVars$StoredNetworks_annotations_tab) }) + StoredNets2_annotations_tab <- reactive({ return(reactiveVars$StoredNetworks2_annotations_tab) }) + StoredNets_topology_tab <- reactive({ return(reactiveVars$StoredNetworks_topology_tab) }) + StoredAnnots <- reactive({ return(reactiveVars$StoredAnnotations) }) + StoredExpress <- reactive({ return(reactiveVars$StoredExpressions) }) + fetchFirstSelectedStoredDataset <- reactive({ + ssn <- SelectedStoredNets() + if (!is.null(ssn) && nrow(ssn) > 0) { + return(fetchDataset(ssn[1,]$id)) + } else { + return(NULL) + } + }) + fetchFirstSelectedStoredDataset2 <- reactive({ + sannots <- SelectedStoredAnnots() + if (!is.null(sannots) && nrow(sannots) > 0) { + return(fetchDataset(sannots[1,]$id)) + } else { + return(NULL) + } + }) + fetchFirstSelectedStoredDataset_annotations_tab <- reactive({ + ssn <- SelectedStoredNets_annotations_tab() + if (!is.null(ssn) && nrow(ssn) > 0) { + return(fetchDataset_annotations_tab(ssn[1,]$id)) + } else { + return(NULL) + } + }) + fetchFirstSelectedStoredGroups2_annotations_tab <- reactive({ + sannots <- SelectedStoredNets2_annotations_tab() + if (!is.null(sannots) && nrow(sannots) > 0) { + return(fetchDataset2_annotations_tab(sannots[1,]$id)) + } else { + return(NULL) + } + }) + fetchFirstSelectedStoredExpression <- reactive({ + sexpress <- SelectedStoredExpress() + if (!is.null(sexpress) && nrow(sexpress) > 0) { + return(fetchDatasetEx(sexpress[1,]$id)) + } else { + return(NULL) + } + }) + fetchFirstSelectedStoredDataset_topology_tab <- reactive({ + ssn <- SelectedStoredNets_topology_tab() + if (!is.null(ssn) && nrow(ssn) > 0) { + return(fetchDataset_topology_tab(ssn[1,]$id)) + } else { + return(NULL) + } + }) + datasetInput <- reactive({ + automated_annotations <- input$automated_annotations + + source("./functions/refreshing/modularity.R", local = T) + net <- fetchFirstSelectedStoredIgraph_just_network() + if (is.null(net)) + return() + modularity() + }) + # Scaling - Sliders + scaling_coordinates_convex <- reactive({ input$scaling_coordinates_convex }) + scaling_coordinates_pies <- reactive({ input$scaling_coordinates_pies }) + scaling_nodes_convex <- reactive({ input$scaling_nodes_convex }) + scaling_nodes_pies <- reactive({ input$scaling_nodes_pies }) + scaling_labels_convex <- reactive({ input$scaling_labels_convex }) + scaling_labels_pies <- reactive({ input$scaling_labels_pies }) + scaling_coordinates_convex_3D_X <- reactive({ input$scaling_coordinates_convex_3D_X }) + scaling_coordinates_convex_3D_Y <- reactive({ input$scaling_coordinates_convex_3D_Y }) + scaling_coordinates_convex_3D_Z <- reactive({ input$scaling_coordinates_convex_3D_Z }) + + # ~~~UPLOAD~~~ #### + # ~~Upload sidebar~~ #### + # observeEvents #### + doAddNetwork <- observeEvent(input$btnAddNetwork, { + tryCatch({ + addNetwork() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + doRemNetwork <- observeEvent(input$btnRemoveNetworks, { + tryCatch({ + remNetwork() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + doAddAnnotations <- observeEvent(input$btnAddNetwork2, { + tryCatch({ + addAnnotations() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + doRemAnnotations <- observeEvent(input$btnRemoveNetworks2, { + tryCatch({ + remAnnotations() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + doAddExpression <- observeEvent(input$btnAddExpression, { + tryCatch({ + addExpression() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + doRemExpression <- observeEvent(input$btnRemoveExpression, { + tryCatch({ + remExpression() + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + uploadTabSetSelectedNetwork <- observeEvent(input$storedGraphsOutputSelectUpload, { + tryCatch({ + reactiveVars$SelectedStoredNetworksIds <- c(input$storedGraphsOutputSelectUpload) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + # Add-Annotations button + uploadTabSetSelectedNetwork2 <- observeEvent(input$storedGraphsOutputSelectUpload2, { + tryCatch({ + reactiveVars$SelectedStoredAnnotationIds <- c(input$storedGraphsOutputSelectUpload2) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + # observes #### + # Change of Network Name based on input choices + dochangeNetworkName <- observe({ + tryCatch({ + updateTextInput(session, + inputId = "networkName", + value = (if (input$uiLoadGraphOptionsInput == "OF") { + paste("Network name") + } else if (input$uiLoadGraphOptionsInput == "oR_String_interactions"){ + paste("BCAR3 STRING Network") + } else if (input$uiLoadGraphOptionsInput == "oR_Drosophila"){ + paste("Drosophila TAU Network") + })) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # Change Annotation Name based on input choices + dochangeAnnotationName <- observe({ + tryCatch({ + updateTextInput(session, + inputId = "annotationName", + value = (if (input$uiLoadGraphOptionsInput_annotations == "OF") { + paste("Annotation name") + } else if (input$uiLoadGraphOptionsInput_annotations == "oR_String_Annotation_BP"){ + paste("BCAR3 GO Biological Process") + } else if (input$uiLoadGraphOptionsInput_annotations == "oR_String_Annotation_MF"){ + paste("BCAR3 GO Molecular Function") + } else if (input$uiLoadGraphOptionsInput_annotations == "oR_String_Annotation_KEGG"){ + paste("BCAR3 GO KEGG pathways") + } else if (input$uiLoadGraphOptionsInput_annotations == "oR_Drosophila_KEGG"){ + paste("Drosophila TAU KEGG pathways") + } else if (input$uiLoadGraphOptionsInput_annotations == "oR_Drosophila_Luvain"){ + paste("Drosophila TAU Louvain") + })) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # Change Expression Name based on input choices + dochangeExpressionName <- observe({ + tryCatch({ + updateTextInput(session, + inputId = "expressionName", + value = (if (input$uiLoadExpressionsInput == "oR_Expression_file_Drosophila"){ + paste("Drosophila TAU node - coloring file") + })) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # UI render outputs #### + output$uiLoadGraphOptionsOutput <- renderUI({ + tryCatch({ + if (is.null(input$uiLoadGraphOptionsInput)) return() + # Depending on input$input_type, we'll generate a different UI + # component and send it to the client. + if (input$uiLoadGraphOptionsInput == "oF") { + wellPanel(fileInput( + "file1", + "Choose file to upload", + accept = c( + "text/csv", + "text/comma-separated-values,text/plain", + ".csv" + ))#, + #div(list(div(wellPanel(checkboxInput(inputId = "weighted1", label = "Weighted", value = F)), class = "col-md-6")), class = "row") + ) + } + # else { + # div(list(div(wellPanel(checkboxInput(inputId = "weighted1", label = "Weighted", value = F)), class = "col-md-6")), class = "row") + # } + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + output$uiStoredGraphsOutputRadio <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices() + if (is.null(choices)) + return() + return(list( + br(), + wellPanel( + h4("Available networks"), + checkboxGroupInput( + "availableNetworks", + label = "", + choices = choices + ) + ), + div(div( + actionButton("btnRemoveNetworks", "REMOVE", icon = icon("minus")), + class = "col-md-5 centerBlock" + ), class = "row text-center") + )) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + output$uiLoadGraphOptionsOutput_annotations <- renderUI({ + tryCatch({ + if (is.null(input$uiLoadGraphOptionsInput_annotations)) return() + # Depending on input$input_type, we'll generate a different UI + # component and send it to the client. + if (input$uiLoadGraphOptionsInput_annotations == "oF") { + wellPanel(fileInput( + "file2", + "Choose file to upload", + accept = c( + "text/csv", + "text/comma-separated-values,text/plain", + ".csv" + ) + )) + } + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + output$uiLoadExpressionsOutput <- renderUI({ + tryCatch({ + if (is.null(input$uiLoadExpressionsInput)) return() + # Depending on input$input_type, we'll generate a different UI + # component and send it to the client. + if (input$uiLoadExpressionsInput == "oF") { + wellPanel(fileInput( + "file3", + "Choose file to upload", + accept = c( + "text/csv", + "text/comma-separated-values,text/plain", + ".csv" + ) + )) + } + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + output$uiStoredGraphsOutputRadioEx <- renderUI({ + tryCatch({ + input$btnAddExpression + input$btnRemoveExpression + choices <- getStoredExpressionChoices() + if (is.null(choices)) + return() + return(list( + br(), + wellPanel( + h4("Available Expressions"), + checkboxGroupInput( + "availableExpressions", + label = "", + choices = choices + ) + ), + div(div( + actionButton("btnRemoveExpression", "REMOVE", icon = icon("minus")), + class = "col-md-5 centerBlock" + ), class = "row text-center") + )) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # ~~Upload mainPanel~~ #### + # UI render outputs #### + + # select box for network edgelists + output$uiStoredGraphsOutputSelectUpload <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices() + if (is.null(choices)) return() + return(selectInput( + "storedGraphsOutputSelectUpload", + "Selected network", + choices + )) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # select box for annotations + output$uiStoredGraphsOutputSelectUpload2 <- renderUI({ + tryCatch({ + input$btnAddNetwork2 + input$btnRemoveNetworks2 + choices <- getStoredAnnotChoices() + if (is.null(choices)) return() + return( + selectInput( + "storedGraphsOutputSelectUpload2", + "Selected annotation", + choices + ) + ) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # Table format - No loops allowed (simplify) + output$datasettab1 <- DT::renderDataTable({ + tryCatch({ + dataset <- fetchFirstSelectedStoredDataset() + if (is.null(dataset)) + dataset <- EmptyDataset(c("Source", "Target", "Weight")) + if (nrow(dataset) == 0 || attr(dataset, 'weighted')){ + return(datatable(dataset, rownames = F, editable = F, options=list(deferRender = TRUE, scrollY = 500, scroller = TRUE,pageLength = 500) + ) %>% + formatStyle(colnames(dataset), fontSize = ui_options["ui_table_font_sz"]) %>% + formatRound("Weight") + ) + } else { + return(datatable(dataset, rownames = FALSE, options=list(deferRender = TRUE, + scrollY = 500, + scroller = TRUE,pageLength = 500, + columnDefs = list(list(visible=FALSE, targets=c(2))))) %>% + formatStyle(colnames(dataset), fontSize = ui_options["ui_table_font_sz"]) %>% + formatRound("Weight")) + } + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # Table view of Annotations + output$datasettab2 <- DT::renderDataTable({ + tryCatch({ + annotation <- fetchFirstSelectedStoredDataset2() + if (is.null(annotation)) annotation <- EmptyDataset(c("Annotations", "Nodes")) + annotation$Nodes <- gsub(',', ', ', annotation$Nodes) + datatable(annotation, rownames = FALSE, extensions = 'Responsive') %>% + formatStyle(colnames(annotation), fontSize = ui_options["ui_table_font_sz"]) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + output$uiStoredGraphsOutputRadio_annotations <- renderUI({ + tryCatch({ + input$btnAddNetwork2 + input$btnRemoveNetworks2 + choices <- getStoredAnnotChoices() + if (is.null(choices)) + return() + return(list( + br(), + wellPanel( + h4("Available Annotations"), + checkboxGroupInput( + "availableAnnotations", + label = "", + choices = choices + ) + ), + div(div( + actionButton("btnRemoveNetworks2", "REMOVE", icon = icon("minus")), + class = "col-md-5 centerBlock" + ), class = "row text-center") + )) + }, error = function(e) { + print(paste("Upload tab error: ", e)) + shinyalert("Error!", "Upload tab error.", type = "error") + }) + }) + + # ~~~NETWORK ~~~ #### + # ObserveEvents #### + uploadTabSetSelectedNetwork_just_network <- observeEvent(input$storedGraphsOutputSelectUpload_just_network, { + tryCatch({ + reactiveVars$SelectedStoredNetworksIds_just_network <- c(input$storedGraphsOutputSelectUpload_just_network) + }, error = function(e) { + print(paste("Network tab error: ", e)) + shinyalert("Error!", "Network tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + # UI render outputs #### + # Duplicated upload for networks in network-tab (just_network) + output$uiLoadGraphOptionsOutput_just_network <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices_just_network() + if (is.null(choices)) return() + return( selectInput( + "storedGraphsOutputSelectUpload_just_network", + "Selected network", choices) + ) + }, error = function(e) { + print(paste("Network tab error: ", e)) + shinyalert("Error!", "Network tab error.", type = "error") + }) + }) + + # Interactive visNetwork + output$tabVizIgraphSimple <- renderVisNetwork({ + tryCatch({ + set.seed(123) + g <- fetchFirstSelectedStoredIgraph_just_network() + if (is.null(g)) + return() + igraph_visn <- toVisNetworkData(g) + igraph_visn$edges$value <- E(g)$weight + + set.seed(123) + if(is.weighted(g)){ + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + set.seed(123) + visNetwork(igraph_visn$nodes, igraph_visn$edges) %>% + visNodes(size = 25, shape = "ellipse") %>% + visEdges(smooth = FALSE) %>% + visIgraphLayout(layout = "layout_with_fr") %>% + visOptions(highlightNearest = TRUE, + nodesIdSelection = TRUE) %>% + visPhysics(enabled = F) %>% + visInteraction( + keyboard = TRUE, + navigationButtons = TRUE, + zoomView = TRUE, + multiselect = TRUE, + dragView = TRUE + ) + }) + } else{ + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + visIgraph(as.undirected(g)) %>% + visNodes(size = 25, shape = "ellipse") %>% + visOptions(highlightNearest = TRUE, + nodesIdSelection = TRUE) %>% + visInteraction( + keyboard = TRUE, + navigationButtons = TRUE, + zoomView = TRUE, + multiselect = TRUE, + dragView = TRUE + ) + }) + } + }, error = function(e) { + print(paste("Network tab error: ", e)) + shinyalert("Error!", "Network tab error.", type = "error") + }) + }) + + output$modularity_plot <- renderPlot({ + tryCatch({ + # show_labels_algorithms_tab <- T + automated_annotations <- input$automated_annotations + net <- fetchFirstSelectedStoredIgraph_just_network() + if (is.null(net)) + return() + clp <- + automated_annotation_choices(net, automated_annotations) + + if (input$show_labels_algorithms_tab == F) { + # show_labels_algorithms_tab = F + set.seed(123) + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + plot( + clp, + net, + edge.color = 'grey50', + mark.col = qual_col_pals, + vertex.size = 5, + # vertex.label.color = "black", + vertex.color = 'grey50', + vertex.label = NA + ) + }) + } + + else{ + set.seed(123) + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + plot( + clp, + net, + edge.color = 'grey50', + mark.col = qual_col_pals, + vertex.size = 5, + vertex.label.color = "black", + vertex.color = 'grey50' + ) + }) + } + }, error = function(e) { + print(paste("Network tab error: ", e)) + shinyalert("Error!", "Network tab error.", type = "error") + }) + }) + + output$Modularity_table <- DT::renderDataTable({ + tryCatch({ + net <- fetchFirstSelectedStoredIgraph_just_network() + if (is.null(net)) { + df <- EmptyDataset(c("Annotations", "Nodes")) + } + else{ + automated_annotations <- input$automated_annotations + source("./functions/refreshing/modularity.R", local = T) + df <- modularity() + datatable( + df, + colnames = c("Annotations", "Nodes"), + rownames = FALSE, + extensions = 'Responsive' + ) %>% formatStyle(colnames(df), fontSize = ui_options["ui_table_font_sz"]) + } + }, error = function(e) { + print(paste("Network tab error: ", e)) + shinyalert("Error!", "Network tab error.", type = "error") + }) + }) + + # downloadHandlers #### + output$downloadData <- downloadHandler( + filename = function() { + automated_annotations <- input$automated_annotations + paste(file_name(), Sys.Date(), '.txt', sep = '') + }, + content = function(file) { + write.table( + datasetInput(), + file, + row.names = FALSE, + col.names = F, + quote = F, + sep = "\t" + ) + } + ) + + # ~~~ANNOTATIONS~~~ #### + # observeEvents #### + uploadTabSetSelectedNetwork_annotations_tab <- observeEvent(input$storedGraphsOutputSelectUpload_annotations_tab, { + tryCatch({ + reactiveVars$SelectedStoredNetworksIds_annotations_tab <- c(input$storedGraphsOutputSelectUpload_annotations_tab) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + uploadTabSetSelectedNetwork2_annotations_tab <- observeEvent(input$storedGraphsOutputSelectUpload2_annotations_tab, { + tryCatch({ + reactiveVars$SelectedStoredNetworksIds2_annotations_tab <- c(input$storedGraphsOutputSelectUpload2_annotations_tab) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + uploadTabSetSelectedExpression <- observeEvent(input$uiStoredGraphsOutputSelectUploadExpressions, { + tryCatch({ + reactiveVars$SelectedStoredExpressionIds <- c(input$uiStoredGraphsOutputSelectUploadExpressions) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + # observes #### + node1_choice<- observe({ + tryCatch({ + node1 <- input$node_1 + node2 <- input$node_2 + + venn<- vennDiagrams() + if(is.null(input$node1)) + df<- EmptyDataset(c("V1", "V2")) + + if(!is.null(input$node_1)){ + for(i in length(venn$id)){ + venn_1<-which(venn$id==input$node_1) + venn_2<-which(venn$id==input$node_2) + v2 <- unlist(stri_split(venn[venn_2,2],fixed=',')) + v1 <- unlist(stri_split(venn[venn_1,2],fixed=',')) + area1<-length(v1) + + df <- cbind("Node 1"=input$node_1,"Unique Groups"= v1[which((v1%in%v2)==F)]) + df<- as.data.frame(df) + } + } + output$venn_table1 <- DT::renderDataTable({ + datatable(df,rownames = FALSE, extensions = 'Responsive') %>% formatStyle(colnames(df), fontSize = ui_options["ui_table_font_sz"]) + }) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + node2_choice<- observe({ + tryCatch({ + node2 <- input$node_2 + node1 <- input$node_1 + + venn<- vennDiagrams() + if(is.null(input$node2)) + df<- EmptyDataset(c("V1", "V2")) + + if(!is.null(input$node_2)){ + for(i in length(venn$id)){ + venn_1<-which(venn$id==input$node_1) + venn_2<-which(venn$id==input$node_2) + v2 <- unlist(stri_split(venn[venn_2,2],fixed=',')) + v1 <- unlist(stri_split(venn[venn_1,2],fixed=',')) + area2<-length(v2) + + v1 %in% v2 + + df <- cbind("Node 2"=input$node_2, "Unique Groups"=v2[which((v2%in%v1)==F)]) + df<- as.data.frame(df) + } + } + output$venn_table2 <- DT::renderDataTable({ + datatable(df,rownames = FALSE, extensions = 'Responsive') %>% formatStyle(colnames(df), fontSize = ui_options["ui_table_font_sz"]) + }) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # UI render outputs #### + # 3 conditional panels + output$uiLoadGraphOptionsOutput_annotations_tab <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices_annotations_tab() + if (is.null(choices)) return() + return( + selectInput( + "storedGraphsOutputSelectUpload_annotations_tab", + "Selected network", + choices + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$uiLoadGraphOptionsOutput_annotations_annotations_tab <- renderUI({ + tryCatch({ + input$btnAddNetwork2 + input$btnRemoveNetworks2 + choices <- getStoredNetsChoices2_annotations_tab() + if (is.null(choices)) return() + return( + selectInput( + "storedGraphsOutputSelectUpload2_annotations_tab", + "Selected annotation", + choices + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$uiStoredGraphsOutputSelectUploadExpressions <- renderUI({ + tryCatch({ + input$btnAddExpression + input$btnRemoveExpression + choices <- getStoredExpressionChoices() + if (is.null(choices)) return() + return( + selectInput( + "uiStoredGraphsOutputSelectUploadExpressions", + "Selected Expression", + choices + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # Convex hulls + output$interactive_convex_hulls <- renderUI({ + tryCatch({ + g <- fetchFirstSelectedStoredIgraph_annotations_tab() + annotation_graph <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(g) | is.null(annotation_graph)) return(NULL) + + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + + start_time <- Sys.time() + convex_hulls(g, annotation_graph) + end_time <- Sys.time() + if (PRINT_TIMES) print(paste("### TIME:", input$convex_layout_strategy, ": ", end_time - start_time, " seconds.")) + + tags$iframe( + srcdoc = paste(readLines( + paste(USER_TEMP_FOLDER, "/output_convex_", session$token, ".html", sep = "") + ), collapse = '\n'), + width = "100%", + height = "850px" + ) + }) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$chooseGroups <- DT::renderDataTable({ + tryCatch({ + annotation <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(annotation)) + annotation <- EmptyDataset(c("Annotations", "Nodes")) + rowCallback_generated <- + "function(row, dat, displayNum, index){" + for (i in 1:length(rownames(annotation))) + { + rowCallback_generated <- + paste( + rowCallback_generated, + "if(dat[0]==", + i, + "){", + "$('td:eq(1)', row).addClass('x", + i, + "');}" , + sep = "" + ) + } + rowCallback_generated <- + paste(rowCallback_generated, "}", sep = "") + + css_colors <- group_pal_rows(length(rownames(annotation))) + + x <- length(rownames(annotation)) + tmp_css_colors <- c() + for (i in 1:x) + { + tmp_css_colors <- c(tmp_css_colors, css_colors[i]) + } + datatable( + annotation, + extensions = 'Scroller', + options = list( + rownames = T, + deferRender = TRUE, + scrollY = 200, + scroller = TRUE, + rowCallback = JS(rowCallback_generated) + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # PieCharts + output$tabVizPie_charts <- renderUI({ + tryCatch({ + g <- fetchFirstSelectedStoredIgraph_annotations_tab() + annotation_graph <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(g) | is.null(annotation_graph)) return(NULL) + + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + + pie_charts(g, annotation_graph) + tags$iframe( + srcdoc = paste(readLines( + paste(USER_TEMP_FOLDER, "/output_pies_", session$token, ".html", sep = "") + ), collapse = '\n'), + width = "100%", + height = "850px" + ) + }) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # PieCharts + output$chooseGroups2 <- DT::renderDataTable({ + tryCatch({ + annotation <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(annotation)) + annotation <- EmptyDataset(c("Annotations", "Nodes")) + rowCallback_generated <- + "function(row, dat, displayNum, index){" + for (i in 1:length(rownames(annotation))) + { + rowCallback_generated <- + paste( + rowCallback_generated, + "if(dat[0]==", + i, + "){", + "$('td:eq(1)', row).addClass('x", + i, + "');}" , + sep = "" + ) + } + rowCallback_generated <- + paste(rowCallback_generated, "}", sep = "") + + css_colors <- group_pal_rows(length(rownames(annotation))) + + x <- length(rownames(annotation)) + tmp_css_colors <- c() + for (i in 1:x) + { + tmp_css_colors <- c(tmp_css_colors, css_colors[i]) + } + datatable( + annotation, + extensions = 'Scroller', + options = list( + rownames = T, + deferRender = TRUE, + scrollY = 200, + scroller = TRUE, + rowCallback = JS(rowCallback_generated) + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # Convex hulls 3D + output$convex_hull_3D<- renderUI({ + tryCatch({ + s = input$chooseGroups_3D_rows_selected + + g <- fetchFirstSelectedStoredIgraph_annotations_tab() + annotation_graph <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(g) | is.null(annotation_graph)) + return(NULL) + + if (input$Dark == T) { + Dark_mode = T + } + else if (input$Dark == F) { + Dark_mode = F + } + if (input$show_labels_3D == T) { + show_labels_3D = T + } + else if (input$show_labels_3D == F) { + show_labels_3D = F + } + if (input$expressions_3D == T) { + expression_colors_3D = T + } + else if (input$expressions_3D == F) { + expression_colors_3D = F + } + # if (input$layouts_with_virtual_nodes_3D == T) { + # layouts_with_virtual_nodes_3D = T + # } + # else if (input$layouts_with_virtual_nodes_3D == F) { + # layouts_with_virtual_nodes_3D = F + # } + if (input$show_some_labels_3D == T) { + show_some_labels_3D = T + } + else if (input$show_some_labels_3D == F) { + show_some_labels_3D = F + } + + withProgress(min = 0, max = 1, { + incProgress(message = "Plotting", + detail = "This may take a while...", + amount = .1) + + lay <- input$layouts_3D + source("./functions/refreshing/convex_hulls_3D.R", local = T) + convex_hull_3D() + + tags$iframe( + srcdoc = paste(readLines( + paste(USER_TEMP_FOLDER, "/convex_3D_", session$token,".html", sep="") + ), collapse = '\n'), + width = "100%", + height = "850px" + ) + }) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$chooseGroups_3D <- DT::renderDataTable({ + tryCatch({ + annotation <- fetchFirstSelectedStoredGroups2_annotations_tab() + if (is.null(annotation)) + annotation <- EmptyDataset(c("Annotations", "Nodes")) + rowCallback_generated <- + "function(row, dat, displayNum, index){" + for (i in 1:length(rownames(annotation))) + { + rowCallback_generated <- + paste( + rowCallback_generated, + "if(dat[0]==", + i, + "){", + "$('td:eq(1)', row).addClass('x", + i, + "');}" , + sep = "" + ) + } + rowCallback_generated <- + paste(rowCallback_generated, "}", sep = "") + + css_colors <- group_pal_rows(length(rownames(annotation))) + + x <- length(rownames(annotation)) + tmp_css_colors <- c() + for (i in 1:x) + { + tmp_css_colors <- c(tmp_css_colors, css_colors[i]) + } + datatable( + annotation, + extensions = 'Scroller', + options = list( + rownames = T, + deferRender = TRUE, + scrollY = 200, + scroller = TRUE, + rowCallback = JS(rowCallback_generated) + ) + ) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # Venn Diagrams + output$vennDiagram1<- renderUI({ + tryCatch({ + venn <- vennDiagrams() + selectInput("node_1", label = "Node 1", choices = c("-",venn$id), selected = "-") + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$vennDiagram2<- renderUI({ + tryCatch({ + venn<-vennDiagrams() + selectInput("node_2", label = "Node 2", choices = c("-",venn$id), selected = "-") + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$vennDiagrams <- renderPlot({ + tryCatch({ + node1_choice<- observe({ node1 <- input$node_1 }) + + if(!is.null(input$node_1)){ + venn <- vennDiagrams() + for(i in length(venn$id)){ + venn_i<-which(venn$id==input$node_1) + v1 <- unlist(stri_split(venn[venn_i,2],fixed=',')) + area1<-length(v1) + } + } + + node2_choice<- observe({ node2 <- input$node_2 }) + + if(!is.null(input$node_2)){ + venn<- vennDiagrams() + for(i in length(venn$id)){ + venn_i<-which(venn$id==input$node_2) + v2 <- unlist(stri_split(venn[venn_i,2],fixed=',')) + area2<-length(v2) + } + } + + v12<-length(which(v1 %in% v2)) + + length_of_gaps<- c(input$node_1, input$node_2) + if(!is.null(length_of_gaps)){ + empty_length<- length(which(length_of_gaps %in% "-")) + + if(empty_length==0){ # 2 nodes + if( (input$node_1 != input$node_2) ) + { + draw.pairwise.venn(area1 = area1, + area2 = area2, + cross.area = v12, + category = c(input$node_1, input$node_2), + lty = "blank", + fill = c("skyblue", "pink1")) + } + } + } + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + output$venn_table_summ <- DT::renderDataTable({ + tryCatch({ + venn<- vennDiagrams() + length_of_gaps<- c(input$node_1, input$node_2, input$node_3) + empty_length<- length(which(length_of_gaps %in% "-")) + + if(is.null(input$node_1)|is.null(input$node_2)) + df<- EmptyDataset(c("Node 1", "Node 2", "Intersection")) + + for(i in length(venn$id)){ + venn_1<-which(venn$id==input$node_1) + venn_2<-which(venn$id==input$node_2) + v1 <- unlist(stri_split(venn[venn_1,2],fixed=',')) + v2 <- unlist(stri_split(venn[venn_2,2],fixed=',')) + area1<-length(v1) + area2<-length(v2) + v12<-length(which(v1 %in% v2)) + + df <- cbind("Node 1"=input$node_1, "Node 2"=input$node_2, "Intersection"=paste(v1[which(v1 %in% v2)], sep = "")) + df<- as.data.frame(df) + } + datatable(df,rownames = FALSE, extensions = 'Responsive') %>% formatStyle(colnames(df), fontSize = ui_options["ui_table_font_sz"]) + }, error = function(e) { + print(paste("Annotations tab error: ", e)) + shinyalert("Error!", "Annotations tab error.", type = "error") + }) + }) + + # downloadHandlers #### + output$HTML_convex <- downloadHandler( + filename = function() { + paste("Convex_hulls_2D_", session$token, ".html", sep = "") + }, + content = function(file) { + HTML_convex <- paste(readLines(paste(USER_TEMP_FOLDER, "/output_convex_", session$token, ".html", sep = "") + ), collapse = '\n') + write.table(HTML_convex, file, row.names = F,col.names = F, sep = "\t", quote = F) + } + ) + + output$HTML_pies <- downloadHandler( + filename = function() { + paste("Pie_charts_", session$token, ".html", sep = "") + }, + content = function(file) { + HTML_pies <- paste(readLines( + paste(USER_TEMP_FOLDER, "/output_pies_", session$token, ".html", sep = "") + ), collapse = '\n') + write.table(HTML_pies, file, row.names = F,col.names = F, sep = "\t", quote = F) + } + ) + + output$HTML_convex_3D <- downloadHandler( + filename = function() { + paste("Convex_hulls_3D_", session$token, ".html", sep = "") + }, + content = function(file) { + HTML_convex_3D <- paste(readLines( + paste(USER_TEMP_FOLDER, "/convex_3D_", session$token, ".html", sep="") + ), collapse = '\n') + write.table(HTML_convex_3D, file, row.names = F,col.names = F, sep = "\t", quote = F) + } + ) + + # ~~~TOPOLOGY~~~ #### + # ~~Topology sidebar~~ #### + # UI render outputs #### + output$uiStoredGraphsOutputSelectTopolopgy <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices_topology_tab() + if (is.null(choices)) return() + return(selectInput( + "storedGraphsOutputSelectTopolopgy", + "Selected network", + choices + )) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + output$uiStoredGraphsOutputMultipleSelectTopolopgy <- renderUI({ + tryCatch({ + input$btnAddNetwork + input$btnRemoveNetworks + choices <- getStoredNetsChoices_topology_tab() + if (is.null(choices)) + return() + return( + checkboxGroupInput( + "storedGraphsOutputMultipleSelectTopolopgy", + "Selected network(s)", + choices, + selected = getStoredNetsChoices_topology_tab()[1] + ) + ) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + # ~~Topology mainPanel~~ #### + # observeEvents #### + topologyTableViewSetSelectedNetwork <- observeEvent({ + input$storedGraphsOutputSelectTopolopgy + input$statisticsMethodsMainTabsetPanel + }, { + tryCatch({ + if (input$statisticsMethodsMainTabsetPanel == "tableView" && + !( + length(reactiveVars$SelectedStoredNetworksIds_topology_tab) == + 1 && + reactiveVars$SelectedStoredNetworksIds_topology_tab == input$storedGraphsOutputSelectTopolopgy + )) { + reactiveVars$SelectedStoredNetworksIds_topology_tab <- + c(input$storedGraphsOutputSelectTopolopgy) + } + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + topologyPlotViewSetSelectedNetwork <- observeEvent({ + input$storedGraphsOutputMultipleSelectTopolopgy + input$statisticsMethodsMainTabsetPanel}, { + tryCatch({ + if (input$statisticsMethodsMainTabsetPanel == "plotView" && + !( + length(reactiveVars$SelectedStoredNetworksIds_topology_tab) == + length(input$storedGraphsOutputMultipleSelectTopolopgy) && + all( + reactiveVars$SelectedStoredNetworksIds_topology_tab == input$storedGraphsOutputMultipleSelectTopolopgy + ) + )) { + reactiveVars$SelectedStoredNetworksIds_topology_tab <- + c(input$storedGraphsOutputMultipleSelectTopolopgy) + } + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }, ignoreNULL = FALSE) + + doStatSelectAll <- observeEvent(input$btnStatSelectAll, { + tryCatch({ + updateCheckboxGroupInput(session, "statistics2", selected = statistics) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + doStatSelectNone <- observeEvent(input$btnStatSelectNone, { + tryCatch({ + updateCheckboxGroupInput(session, "statistics2", selected = character(0)) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + doRefreshPalette <- observeEvent(input$btnRefreshPalette, { + tryCatch({ + ncolors <- length(colors) + colors <<- randomColor(ncolors) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + # UI render outputs #### + output$statisticsMethodsPlotRender <- renderUI({ + tryCatch({ + plotOutput("statisticsMethodsBarPlot", height = paste0(as.integer( + 480 + + 480 * (input$statisticsPlotPercentMagnify / 100) + ), "px")) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + output$statisticsMethodsBarPlot <- renderPlot({ + tryCatch({ + input$btnRefreshPalette + datasets <- fetchAllSelectedStoredDataset_topology_tab() + if (length(datasets) == 0) + return() + stat_res <- NULL + for (dataset_i in 1:length(datasets)) { + dataset <- datasets[[dataset_i]] + datasetName <- + getDatasetName(names(datasets)[dataset_i]) + dataset_stat_res <- stat_dataset(dataset, "") + if (is.null(dataset_stat_res) || + nrow(dataset_stat_res) == + 0) + next + if (is.null(stat_res)) { + stat_res <- dataset_stat_res + stat_res$network <- datasetName + } else { + dataset_stat_res$network <- datasetName + stat_res <- rbind(stat_res, dataset_stat_res) + } + } + if (is.null(stat_res)) + return() + colnames(stat_res) <- c("statistic", "value", "network") + stat_res$value <- as.numeric(as.character(stat_res$value)) + stat_res$network <- factor(stat_res$network) + ncolors <- length(levels(stat_res$network)) + if (length(colors) < ncolors) + colors <<- randomColor(ncolors) + plot.settings <- + list( + superpose.polygon = list(col = colors[1:ncolors], border = "transparent"), + strip.border = list(col = "black") + ) + barchart( + value ~ statistic, + group = network, + data = stat_res, + origin = 0, + horizontal = F, + auto.key = list( + space = "right", + points = F, + rectangles = T + ), + par.settings = plot.settings, + scales = list(x = list(rot = 45)), + panel = function(x, y, ...) { + panel.grid(h = -1, v = 0, col = "gray") + panel.barchart(x, y, ...) + } + ) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + output$statres <- DT::renderDataTable({ + tryCatch({ + dset <- fetchFirstSelectedStoredDataset_topology_tab() + DT::datatable( + stat_dataset2(dset, getDatasetName(attr(dset, "id"))), + options = list( + paging = FALSE, + dom = "t", + rowCallback = JS( + "function(row, data) {", + "var num = parseFloat(data[2]).toFixed(2);", + "if(isNaN(num)){num = data[2];}", + "$('td:eq(2)', row).html(num);", + "}" + ) + ) + ) + }, error = function(e) { + print(paste("Topology tab error: ", e)) + shinyalert("Error!", "Topology tab error.", type = "error") + }) + }) + + # onSessionEnded #### + session$onSessionEnded(function() { + snets <- isolate(StoredNets()) + if (nrow(snets) > 0) { + unlink(c(paste0(USER_TEMP_FOLDER, "/", "*.rda"), "*.zip")) + } + + sannots <- isolate(StoredAnnots()) + if (nrow(sannots) > 0) { + unlink(c(paste0(USER_TEMP_FOLDER, "/", "*.rda"), "*.zip")) + } + + sexpress <- isolate(StoredExpress()) + if (nrow(sexpress) > 0) { + unlink(c(paste0(USER_TEMP_FOLDER, "/", "*.rda"), "*.zip")) + } + }) + +}) diff --git a/ui.R b/ui.R new file mode 100644 index 0000000..9d6f66e --- /dev/null +++ b/ui.R @@ -0,0 +1,31 @@ +# libs and files #### +source("./views/welcome.R", local=TRUE) +source("./views/upload.R", local=TRUE) +source("./views/network.R", local=TRUE) +source("./views/annotations.R", local=TRUE) +source("./views/topology.R", local=TRUE) +source("./views/help.R", local=TRUE) +source("./views/about.R", local=TRUE) + +# UI fixedPage #### +fixedPage( + theme = shinytheme("sandstone"), + useShinyjs(), + useShinyalert(), + tags$head(tags$script(src = "intro.js")), + tags$head(tags$script(src = "introbutton.js")), + tags$head(tags$link(rel = "stylesheet", type = "text/css", href = "norma.css")), + tags$head(tags$link(rel = "stylesheet", type = "text/css", href = "intro.css")), + tags$head(tags$link(rel = "stylesheet", type = "text/css", href = "annot_table.css")), + tags$head(tags$link(rel = "shortcut icon", href = "favicon.ico")), + tags$img(src = b64_1), + navbarPage("NORMA: The NetwORk Makeup Artist", + tabPanel( "Welcome", welcomePage ), + tabPanel( "Upload", icon = icon("upload"), uploadPage ), + tabPanel( "Network", networkPage ), + tabPanel( "Annotations", annotationsPage), + tabPanel( "Topology", icon = icon("globe", lib = "glyphicon"), topologyPage ), + tabPanel( "Help", icon = icon("question"), helpPage ), + tabPanel( "About", icon = icon("users"), aboutPage ) + ) +) diff --git a/user_temp/folder_create b/user_temp/folder_create new file mode 100644 index 0000000..e69de29 diff --git a/views/about.R b/views/about.R new file mode 100644 index 0000000..5783208 --- /dev/null +++ b/views/about.R @@ -0,0 +1,20 @@ +aboutPage <- div(id = "about_div", + strong("The Team:"), + tags$ul( + tags$li("Mikaela Koutrouli - BSRC 'Alexander Fleming'"), + tags$li("Evangelos Karatzas - BSRC 'Alexander Fleming'"), + tags$li("Katerina Papanikolopoulou - BSRC 'Alexander Fleming'"), + tags$li("Yorgos Sofianatos - BSRC 'Alexander Fleming'"), + tags$li("Georgios A. Pavlopoulos - BSRC 'Alexander Fleming'") + ), + br(), + strong("Code:"), + helpText("Available at: https://github.com/PavlopoulosLab/NORMA"), + br(), + strong("Publications:"), + br(), + tags$a(target="_blank", href="https://www.biorxiv.org/content/10.1101/2022.03.02.482621v1.abstract", "The network makeup artist (NORMA-2.0): Distinguishing annotated groups in a network using innovative layout strategies"), + helpText("bioRxiv, 2022 March, doi: 10.1101/2022.03.02.482621"), + tags$a(target="_blank", href="https://pubmed.ncbi.nlm.nih.gov/34171457", "NORMA: The Network Makeup Artist - A Web Tool for Network Annotation Visualization"), + helpText("Genomics Proteomics Bioinformatics, 2021 Jun 22:S1672-0229(21)00130-3, doi: 10.1016/j.gpb.2021.02.005, Epub ahead of print, PMID: 34171457.") +) diff --git a/views/annotations.R b/views/annotations.R new file mode 100644 index 0000000..d42462b --- /dev/null +++ b/views/annotations.R @@ -0,0 +1,434 @@ +annotationsPage <- div(id = "annotations_div", + conditionalPanel( + condition = "input.availableNetworks == 0", + bsAlert("tabUploadMainAlert"), + uiOutput("uiLoadGraphOptionsOutput_annotations_tab") + ), + conditionalPanel( + condition = "input.availableAnnotations == 0", + bsAlert("tabUploadMainAlert"), + uiOutput("uiLoadGraphOptionsOutput_annotations_annotations_tab") + ), + conditionalPanel( + condition = "input.availableExpressions == 0", + bsAlert("tabUploadMainAlert3"), + uiOutput("uiStoredGraphsOutputSelectUploadExpressions") + ), + br(), + tabsetPanel( + tabPanel( + "Convex Hulls", + br(), + tags$div(class = "strategies", + selectInput( + "layouts", + "General layout:", + choices = layouts_ui, + selected = selected_layouts, + multiple = FALSE + ), + selectInput( + "local_layout", + "Local layout:", + choices = layouts_ui, + selected = selected_layouts, + multiple = FALSE + ), + br(), + prettyRadioButtons( + inputId = "convex_layout_strategy", + label = "Layout modification strategy:", + choices = c("Simple layout", "Virtual node per group", "Group gravity", "Supernodes per group"), + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info" + ), + sliderInput( + inputId = "repeling_force", + label = "Adjust force strength:", + min = 1, + max = 20, + value = 10 + ) + ), # strategies + br(), + prettyCheckbox( + inputId = "show_labels",outline = T,fill = T,bigger = T, + label = "Show Labels", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = T + ), + prettyCheckbox( + inputId = "expressions",outline = T,fill = T,bigger = T, + label = "Show node - coloring", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + prettyCheckbox( + inputId = "some_labels",outline = T,fill = T,bigger = T, + label = "Show only labels of selected annotation groups", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + hr(), + div(id="interactive_convex_hulls_loader", + shinycssloaders::withSpinner( + uiOutput("interactive_convex_hulls") + ) + ), + eval(ui_dataTable_panel("chooseGroups")), + tags$style( + HTML( + ".js-irs-3 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-3 .irs-bar-edge {border-color: #2C8160;} + .js-irs-3 .irs-single, .js-irs-3 .irs-bar-edge, .js-irs-3 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-4 .irs-bar {border-top-color: #2C8160;border-bottom-color: #2C8160;} .js-irs-4 .irs-bar-edge {border-color: #2C8160;} + .js-irs-4 .irs-single, .js-irs-4 .irs-bar-edge, .js-irs-4 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-5 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-5 .irs-bar-edge {border-color: #2C8160;} + .js-irs-5 .irs-single, .js-irs-5 .irs-bar-edge, .js-irs-5 .irs-bar {background: #2C8160;}" + ) + ), + + div( + style = "display:inline-block", + sliderInput( + inputId = "scaling_coordinates_convex", + label = "Scale the coordinates:", + min = 1, + max = 10, + value = 1 + ) + ), + div( + style = "display:inline-block", + sliderInput( + "scaling_nodes_convex", + "Adjust node size:", + min = 0.2, + max = 5, + value = 2, + step = 0.2 + ) + ), + div( + style = "display:inline-block", + sliderInput( + "scaling_labels_convex", + "Adjust label size:", + min = 0, + max = 30, + value = 10, + step = 2 + ) + ), + br(), + br(), + downloadBttn("HTML_convex", "Download HTML file", + style = "bordered", + color = "success", + size = "sm", + no_outline = T), + br(), + br() + ), + + tabPanel( + "Pie - Chart Nodes", + fluidRow( + tags$div(class = "strategies", + selectInput( + "layouts2", + "General layout:", + choices = layouts_ui, + selected = selected_layouts, + multiple = FALSE + ), + selectInput( + "local_layout2", + "Local layout:", + choices = layouts_ui, + selected = selected_layouts, + multiple = FALSE + ), + br(), + prettyRadioButtons( + inputId = "piechart_layout_strategy", + label = "Layout modification strategy:", + choices = c("Simple layout", "Virtual node per group", "Group gravity", "Supernodes per group"), + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info" + ), + sliderInput( + inputId = "repeling_force2", + label = "Adjust force strength:", + min = 1, + max = 20, + value = 10 + ) + ), # strategies pies + hr(), + prettyCheckbox( + inputId = "show_labels_pies",outline = T,fill = T,bigger = T, + label = "Show Labels", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = T + ), + prettyCheckbox( + inputId = "expressions_pies",outline = T,fill = T,bigger = T, + label = "Show node - coloring", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + prettyCheckbox( + inputId = "some_labels_pies",outline = T,fill = T,bigger = T, + label = "Show only labels of selected annotation groups", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + div(id="tabVizPie_charts_loader", + shinycssloaders::withSpinner( + uiOutput('tabVizPie_charts') + ) + ), + eval(ui_dataTable_panel("chooseGroups2")), + ), + tags$style( + HTML( + ".js-irs-0 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-0 .irs-bar-edge {border-color: #2C8160;} + .js-irs-0 .irs-single, .js-irs-0 .irs-bar-edge, .js-irs-0 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-1 .irs-bar {border-top-color: #2C8160;border-bottom-color: #2C8160;} .js-irs-1 .irs-bar-edge {border-color: #2C8160;} + .js-irs-1 .irs-single, .js-irs-1 .irs-bar-edge, .js-irs-1 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-2 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-2 .irs-bar-edge {border-color: #2C8160;} + .js-irs-2 .irs-single, .js-irs-2 .irs-bar-edge, .js-irs-2 .irs-bar {background: #2C8160;}" + ) + ), + + div( + style = "display:inline-block", + sliderInput( + "scaling_coordinates_pies", + "Scale the coordinates", + min = 1, + max = 10, + value = 1 + ) + ), + div( + style = "display:inline-block", + sliderInput( + "scaling_nodes_pies", + "Adjust node size:", + min = 10, + max = 30, + value = 15 + ) + ), + div( + style = "display:inline-block", + sliderInput( + "scaling_labels_pies", + "Adjust label size:", + min = 0, + max = 30, + value = 10, + step = 2 + ) + ), + + + hr(), + class = 'box-panel-padding', + class = 'box-panel', + + br(), + br(), + downloadBttn("HTML_pies", "Download HTML file", + style = "bordered", + color = "success", + size = "sm", + no_outline = T), + br(), + br() + + ), + + tabPanel( + "Convex Hulls - 3D", + helpText("Select the layout you want in the analysis."), + selectInput( + "layouts_3D", + "3D layout:", + choices = layouts_3D, + selected = selected_layouts_3d, + multiple = FALSE + ), + prettyCheckbox( + inputId = "Dark",outline = T,fill = T,bigger = T, + label = "Dark mode", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + prettyCheckbox( + inputId = "show_labels_3D",outline = T,fill = T,bigger = T, + label = "Show labels on network", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + prettyCheckbox( + inputId = "show_some_labels_3D",outline = T,fill = T,bigger = T, + label = "Show only labels of selected annotation groups", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + prettyCheckbox( + inputId = "expressions_3D",outline = T,fill = T,bigger = T, + label = "Show node - coloring", + thick = TRUE, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + div(id="convex_hull_3D_loader", + shinycssloaders::withSpinner( + uiOutput("convex_hull_3D") + ) + ), + eval(ui_dataTable_panel("chooseGroups_3D")), + + tags$style( + HTML( + ".js-irs-0 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-0 .irs-bar-edge {border-color: #2C8160;} + .js-irs-0 .irs-single, .js-irs-0 .irs-bar-edge, .js-irs-0 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-1 .irs-bar {border-top-color: #2C8160;border-bottom-color: #2C8160;} .js-irs-1 .irs-bar-edge {border-color: #2C8160;} + .js-irs-1 .irs-single, .js-irs-1 .irs-bar-edge, .js-irs-1 .irs-bar {background: #2C8160;}" + ) + ), + + tags$style( + HTML( + ".js-irs-2 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-2 .irs-bar-edge {border-color: #2C8160;} + .js-irs-2 .irs-single, .js-irs-2 .irs-bar-edge, .js-irs-2 .irs-bar {background: #2C8160;}" + ) + ), + div( + style = "display:inline-block", + sliderInput( + inputId = "scaling_coordinates_convex_3D_X", + label = "Scale x coordinates:", + min = 1, + max = 10, + value = 1 + ) + ), + div( + style = "display:inline-block", + sliderInput( + inputId = "scaling_coordinates_convex_3D_Y", + label = "Scale y coordinates:", + min = 1, + max = 10, + value = 1 + ) + ), + div( + style = "display:inline-block", + sliderInput( + inputId = "scaling_coordinates_convex_3D_Z", + label = "Scale z coordinates:", + min = 1, + max = 10, + value = 1 + ) + ), + + br(), + br(), + downloadBttn("HTML_convex_3D", "Download HTML file", + style = "bordered", + color = "success", + size = "sm", + no_outline = T), + br(), + br() + ), + + tabPanel("Venn Diagrams", + br(), + div(style = "display:inline-block",uiOutput("vennDiagram1")), + div(style = "display:inline-block",uiOutput("vennDiagram2")), + br(), + plotOutput("vennDiagrams",width = 600, height = 600), + div(style = "display:inline-block",eval(ui_dataTable_panel("venn_table1", FALSE))), + div(style = "display:inline-block",eval(ui_dataTable_panel("venn_table2", FALSE))), + div(style = "display:inline-block",eval(ui_dataTable_panel("venn_table_summ", FALSE))) + + ) + + ) #tabsetPanel + +) diff --git a/views/help.R b/views/help.R new file mode 100644 index 0000000..ff38b3f --- /dev/null +++ b/views/help.R @@ -0,0 +1,302 @@ +helpPage <- tabsetPanel( + tabPanel( "Input File", + br(), + strong("NORMA mainly accepts 3 different files as input:."), + br(), br(), + strong("The network file:"), + helpText( "It is an obligatory, 2-column (unweighted) or 3-column (weighted), tab-delimited file, containing all network connections of an undirected network. This file must contain headers, namely: 'Source' and 'Target' (and 'Weight' optionally). Notably, self-loops and multiple-edges are eliminated automatically." ), + strong("The annotation file:"), + helpText( " It is an obligatory, 2-column, tab-delimited file which contains information about the defined groups. The first column contains the group names whereas the second column contains the node names in a group separated by a comma (,) and without spaces. No headers are allowed."), + strong("The expression file:"), + helpText( "The expression file: It is an optional, 2-column, tab-delimited file which contains information about node coloring (e.g. gene expressions). The first column contains the node names and the second column the node colors (e.g. red, green, yellow, blue, orange, #00ff00, #ff0000, #ffff00). Nodes without color assignment will be colored gray. No headers are allowed in this file."), + helpText(" Examples are shown below:"), + pre( + " +Network File: Annotation File: Node-coloring file Warnings! + +Source Target Weight Group-2 BCL2L1,MDM4,MDM2,CHEK2 CDKN1A blue - Network file: Must have headers: Source - Target +CDKN1A TP53 5 Group-5 TP53,EP300 TP53 blue - Annotation file: no headers, no spaces only commas +TP53 MDM2 1 Group-1 CDKN2A,ATM,TP53BP2,MDM2 MDM4 #00ff00 (e.g. BCL2L1,MDM4,MDM2) +MDM4 TP53 3 Group-4 CHEK2,CREBBP,MDM2 BCL2L1 red +BCL2L1 TP53 4 Group-3 TP53,BCL2L1 CHEK2 red - Node-coloring file: no headers +CHEK2 ATM 2 Group-6 MDM4,MDM2 ATM red Colors can be either color names (e.g. blue, red) +TP53 EP300 1 TP53BP2 red or hex codes (#00ff00, #ff0000, #ffff00) +ATM TP53 4 CDKN2A blue +TP53 CREBBP 1 EP300 #ffff00 +MDM4 MDM2 1 CREBBP red +CHEK2 TP53 2 MDM2 blue +TP53BP2 TP53 8 +CDKN2A TP53 3 +CDKN2A MDM2 3 +ATM MDM2 1 +EP300 CREBBP 2 + . . . + . . . + . . . +" + ), + + strong("Usage:"), + helpText( "Users can upload as many network and annotation files as they like. Every time a network or an annotation file is uploaded, a name can be given first. + Once a network or an annotation file has been named and uploaded, it will appear as an option in any of the NORMA's dropdown selection lists. Users can remove indifferent annotations or networks at any time."), + br(), br(), + strong("Troubleshooting:"), + helpText("Nodes referenced in the annotation file must comply with nodes referenced in the network file. If a node in the annotation does not appear in the network file, NORMA won't be able to process the files and produce a proper visualization. To address this problem, we have implemented an R script which accepts a network file and an annotation file as inputs and generates a corrected annotation output file. In this output file, node names in annotations which do not appear in the network file are discarded. Notably, node names in both files must not contain any commas (,)."), + br(), + downloadLink('R_script', "Download R script here"), + br(), + helpText(tags$ul( + tags$li("Install R / Rstudio"), + tags$li("Open the file with Rstudio"), + tags$li("Run the file"), + tags$li("The output will be written in a new file named 'annotations_cleaned.txt'. ") + )), + br(), br() + ), #Tabpanel Input File + + tabPanel( + "EXAMPLES", + br(), + strong("STRING example 1 (TP53 interactors):"), + br(), + downloadLink('string_net_tp53', "STRING Network file"), + br(), + downloadLink('string_annot', "STRING Annotation file"), + br(), + downloadLink('string_expr', "STRING Expression file"), + br(), hr(), br(), + strong("STRING example 2 (BCAR3 interactors):"), + br(), + downloadLink('string_net_bcar3', "STRING Network file"), + br(), + downloadLink('string_bp', "GO Annotation - Biological Process"), + br(), + downloadLink('string_mf', "GO Annotation - Molecular Function"), + br(), + downloadLink('string_kegg', "KEGG pathways"), + br(), hr(), br(), + strong("Drosophila (Tau) Network (PMID:31488613, PMCID:PMC6794924, DOI:10.1523/JNEUROSCI.0391-19.2019):"), + br(), + downloadLink('dros_net', "Drosophila Network file"), + br(), + downloadLink('dros_annot', "Drosophila Kegg pathways"), + br(), + downloadLink('dros_louvain', "Drosophila Louvain automated annotation file"), + br(), + downloadLink('dros_express', "Drosophila Expression file"), + br(), hr(), br(), + strong("Human Gene Co-expression Network (PMID:19081792, PMCID:PMC2597745, DOI:10.1371/journal.pone.0003911):"), + helpText("http://bioinfow.dep.usal.es/coexpression/"), + br(), + downloadLink('co_express', "Gene Co-expression Network"), + br(), + downloadLink('co_express_bp', "GO Annotation - Biological Process"), + br(), + downloadLink('co_express_mf', "GO Annotation - Molecular Function"), + br(), + downloadLink('co_express_cc', "GO Annotation - Cellular Components"), + br(), + downloadLink('co_express_kegg', "KEGG pathways"), + br(), + downloadLink('co_express_mcode', "MCODE Node coloring"), + br(), hr(), br(), + strong("COVID-19:"), + helpText("Intact Database"), + br(), + downloadLink('covid_19_net', "COVID-19 Network"), + br(), + downloadLink('covid_19_interpro', "HomoSapiens Protein Domains - INTERPRO"), + br(), + downloadLink('covid_19_bp', "HomoSapiens GO Annotation - Biological Process"), + br(), + downloadLink('covid_19_mf', "HomoSapiens GO Annotation - Molecular Function"), + br(), + downloadLink('covid_19_cc', "HomoSapiens Protein Domains GO Annotation - Cellular Components"), + br(), + downloadLink('covid_19_kegg', "HomoSapiens Protein Domains KEGG pathways"), + br(), + downloadLink('covid_19_smart', "HomoSapiens Protein Domains SMART"), + br(), hr(), br(), + strong("Gallus gallus:"), + helpText("BioGrid Database"), + br(), + downloadLink('Gallus_gallus_net', "Gallus gallus Network"), + br(), + downloadLink('Gallus_gallus_kegg', "BioGrid Gallus gallus KEGG pathways"), + br(), br() + ), + + tabPanel( + "The Upload Tab", + br(), + helpText( + "Once one or more network and annotation files have been named and uploaded, they will appear as options in the dropdown selection lists. Users can select a network or an annotation file at a time and see its content as an interactive table. Notably, one can search by suffix in the table, using the Search field. " + ), + tags$img(src = b64_2), + tags$img(src = b64_3), + tags$img(src = b64_4), + br(), br() + ), #Tabpanel Upload + + tabPanel( + "The Network Tab", + br(), + strong( + "This Tab consists of two sub-tabs dedicated to network analysis and visualization. These are: (i) the Interactive Network and the (ii) the Automated Community Detection." + ), + br(), br(), + strong("Interactive Network:"), + helpText( + "This Tab offers a dynamic network visualization in its simplest form. Nodes are connected with undirected edges and their coordinates are calculated using a force-directed layout. The network is fully interactive as zooming, dragging and panning are allowed either by using the mouse or the navigation buttons. In addition, nodes can be selected and dragged anywhere on the plane, whereas the first neighbors of any node can be highlighted upon selection. Finally, the network view is automatically updated when a different network is selected." + ), + br(), + tags$img(src = b64_5), + br(), br(), + strong("Automated Community Detection:"), + helpText( + "This Tab is used for the automatic calculation of communities whereas the exported file(s) can be used as input annotation file(s). Users can assign nodes to communities (not necessarily uniquely), using various options. These are:" + ), + helpText(tags$ul( + # tags$li( "Fast-Greedy: This function tries to find densely connected subgraphs (also called communities) via directly optimizing a modularity score."), + tags$li( "Louvain: This function implements a multi-level modularity optimization algorithm for finding community structures and is based on the modularity measure and a hierarchical approach."), + tags$li( "Label-Propagation: This is a fast, nearly linear time algorithm for detecting community structures in a network by labeling the vertices with unique labels and then updating the labels by majority voting in the neighborhood of the vertex."), + tags$li( "Walktrap: This function tries to find densely connected subgraphs in a graph via random walks. The idea is that short random walks tend to stay in the same community."), + tags$li( "Betweenness: Many networks consist of modules which are densely connected between themselves but sparsely connected to other modules. Clustering is made by 'breaking' the bridges which connect densely connected regions."), + )), + br(), + tags$img(src = b64_6), + br(), + br(), + helpText( "Once a community detection method has been selected, users can see the results as interactive and searchable tables or as static plots for an at-a-glance view. In order for users to take advantage of NORMA's advanced interactive visualization capabilities, the automatically generated annotations must be first exported and then imported as annotation input files.") + + ), #Tabpanel Network + + tabPanel( + "The Annotations Tab", + br(), + strong( "This Tab is NORMA's strongest feature and is used to visualize annotated networks in an easy and user-friendly way. Annotated, are the networks with (pre-)defined clusters, communities, subgraphs, marked regions or neighborhoods. + Through the Annotation Tab, users can select between any of the uploaded networks or annotation files and visualize them in combination. Network and Annotation selections can be done by the offered dropdown selection lists. + The Annotation Tab consists of three sub-tabs. These are the: (i) Convex Hull, (ii) Pie-chart nodes and (iii) Venn diagrams." + ), + br(), br(), + strong("Convex Hulls:"), + helpText( "In this tab, the selected network is initially visualized after applying any of the offered layout algorithms and shaded convex hulls are then used to highlight communities in a Venn-diagram-like view. A node might belong to more than one group. In this case, NORMA tries to bring closer together the overlapping regions which share common nodes while simultaneously it tries to keep the distinct groups apart. Groups are highlighted using visually distinct colors, whereas transparency is used to efficiently highlight the overlapping regions."), + br(), + tags$img(src = b64_7), + br(), br(), + strong("Pie-chart nodes:"), + helpText( "In this Tab, the selected network is initially visualized after applying any of the offered layout algorithms and nodes are then visualized as pie-charts, divided into slices to illustrate the groups a node belongs to. If a node for example belongs to four groups, then the pie chart will consist of four equal slices colored with distinct colors. Nodes which do not belong to any group are marked gray."), + br(), + tags$img(src = b64_8), + br(), br(), + + strong("Convex Hulls 3D:"), + helpText( "Like in 2D Convex Hulls, in this tab, the selected network is initially visualized after applying any of the offered 3D layout algorithms and 3D shaded convex hulls are then used to highlight communities in a 3D Venn-diagram-like view. The visualization is fully interactive and a dark mode visualization is also supported."), + br(), + tags$img(src = b64_3D_convex), + br(), br(), + strong("Node coloring:"), + helpText( "Often, one might want to assign certain colors to nodes in order to encode certain information. In a gene expression network for example, one might want to highlight the up- and down-regulated genes. Once an expression file has been loaded (see Input file section), nodes in the Convex Hull will be filled with the color of interest whereas nodes in the Pie-Chart tab will appear with a colored border. As node coloring is an optional feature, one can enable or disable this functionality at any time (selection box)."), + br(), + tags$img(src = b64_9), + br(), br(), + strong("Layouts:"), + helpText( "Several layouts are offered for network visualization in both Convex Hull and Pie Chart sub-tabs. These are:"), + helpText( + tags$ul( + tags$li( "Fruchterman-Reingold: It places nodes on the plane using the force-directed layout algorithm developed by Fruchterman and Reingold."), + tags$li( "Random: This function places the vertices of the graph on a 2D plane uniformly using random coordinates."), + tags$li( "Circle: It places vertices on a circle, ordered by their vertex ids."), + tags$li( "Kamada-Kawai: This layout places the vertices on a 2D plane by simulating a physical model of springs."), + tags$li( "Reingold-Tilford: This is a tree-like layout and is suitable for trees or graphs without many cycles."), + tags$li( "LGL: A force directed layout suitable for larger graphs."), + tags$li( "Graphopt: A force-directed layout algorithm, which scales relatively well to large graphs."), + tags$li( "Gem: It places vertices on the plane using the GEM force-directed layout algorithm."), + tags$li( "Star: It places vertices of a graph on the plane, according to the simulated annealing algorithm by Davidson and Harel."), + tags$li("Grid: This layout places vertices on a rectangular 2D grid.") + ), + helpText("NOTE: NORMA provides the option to slightly modify the selected layout in order to make groups as distinct as possible, thus avoiding unecessary overlaps which may occur due to the original layout.") + ), + br(), + strong("Interactivity and Visualization:"), + helpText( + "NORMA gives a variety of options for the creation of optimal custom views. Network zoom in/out and panning functionalities are offered while users can interactively drag any node and place it anywhere on the plane. In addition to the visualized networks, groups are shown in an interactive table whose rows are colored accordingly. By selecting one or more groups, one can adjust the convex hulls as well as the pie-chart nodes accordingly. Colored groups (rows) in the table correspond to colored groups in the offered views and vice versa. In addition, users have the option to show and hide the labels or only keep the labels of the selected groups of interest while labels below a certain zoom level are hidden for clarity. Finally, sliders to adjust node and label sizes as well as a slider to scale the network size are offered." + ), + br(), + strong("Venn Diagrams:"), + helpText("Users are allowed to choose any pair of nodes and visualize the common groups they belong to as a Venn diagram. As it is not in the scope of NORMA to provide more complex Venn diagrams, users are encouraged to visit other online applications dedicated to this purpose. "), + tags$img(src = b64_12) + + ), #Tabpanel Annotations + + + + + tabPanel( + "Layout Strategies", + br(), + helpText( + "Users are allowed to choose between three different strategies to visually separate annotated areas of interest in a network in combination with established layout algorithms. + " + ), + br(), + br(), + strong("Strategy 1 - Virtual nodes:"), + helpText( + "As in the original version, NORMA-2.0 introduces one virtual node per group which behaves as a hub. Upon creation, edges with heavy weights are assigned to this node, linking it with all nodes from the same group. Then, any traditional layout algorithm can be utilized until it converges. The difference compared to directly applying a layout algorithm on the network is that the virtual nodes will attract the group-specific nodes as if they were parts of the network. After completing the layout execution, all virtual nodes are removed." + ), + tags$img(src = b64_strategy1, class="layout_strat_img"), + br(), + br(), + strong("Strategy 2 - Gravity:"), + helpText( + "Here, NORMA-2.0 introduces intra-group edges where necessary to generate clique-like subnetworks (all-vs-all connections). As a second step, the intra-group edge weights are significantly increased whereas the inter-group edge weights are simultaneously decreased. Then, any of the offered layouts can be applied to adjust node coordinates. The introduced edges and weights only exist for the calculation of the layout coordinates, and do not carry over to the final visualized network. + "), + tags$img(src = b64_strategy2, class="layout_strat_img"), + br(), + br(), + strong("Strategy 3 - Super nodes:"), + helpText( + "In this scenario, NORMA-2.0 introduces 'super-nodes' to represent each of the uploaded annotation groups. Then, it connects these groups with edges which correspond to the connections from the initial network. For example, if node A belonging to the annotation group 1 is connected to node B from group 2, then the group 1 super-node will also be connected to the group 2 super-node. Notably, the network becomes significantly smaller both in terms of node and connection numbers. In a second step, any of the available layouts can be applied on the 'super-network'. Upon layout convergence, all centroid coordinates of these super-nodes as well as the coordinates of no-group nodes can be further repelled according to a user-defined input. Then, all initial nodes will be placed around their respective super-nodes according to a second user-selected local layout choice. If a node belongs to more than one group, the average values for their (x, y) coordinates are used for the final visualization. + "), + tags$img(src = b64_strategy3, class="layout_strat_img"), + br(), br() + ), #Tabpanel Layout Strategies + + + tabPanel( + "The Topology Tab", + br(), + strong( + "This Tab is used for automated topological analysis and direct comparison of topological features between two or more networks." + ), + helpText("The topological features are: "), + helpText( + tags$ul( + tags$li( "Number of Edges: Shows the number of edges in the network. NORMA accepts networks with less than 10000 edges." ), + tags$li( "Number of Nodes: Shows the number of nodes in the network." ), + tags$li( "Density: The density of a graph is the ratio of the number of edges and the number of possible edges." ), + tags$li( "Average path length: The average number of steps needed to go from one node to another." ), + tags$li( "Clustering Coefficient: A metric which shows if the network has the tendency to form clusters. Values between 0 and 1." ), + tags$li( "Modularity: This function calculates how modular is a given division of a graph into subgraphs." ), + tags$li( "Average Eccentricity: The distance from a particular vertex to all other vertices in the graph is taken and among those distances, the eccentricity is the highest of distances." ), + tags$li( "Average number of Neighbors: It is the total number of neighbors per node divided by the number of nodes." ), + tags$li( "Centralization betweenness: It is an indicator of a node's centrality in a network. It is equal to the number of shortest paths from all vertices to all others that pass through that node. Betweenness centrality quantifies the number of times a node acts as a bridge along the shortest path between two other nodes." ), + tags$li( "Centralization degree: It is defined as the number of links incident upon a node." ) + ) + ), + br(), + strong( "The Topology Tab is divided into two sub-tabs. These are: (i) the Summaries and (ii) the Comparative Plots." ), + br(), + strong("Summaries:"), + helpText( "This Tab shows the aforementioned topological measures in a numerical form as a table view. Users can select one or more features of interest through the offered checkboxes and show them accordingly. Notably, this can be done for one network at a time upon selection (dropdown selection list)." ), + br(), + tags$img(src = b64_10), + br(), + strong("Comparative Plots:"), + helpText( "This Tab can be used to directly compare the topological features of two or more networks simultaneously. In contrast to the previous Tab, users are allowed to select one topological feature at a time (radio buttons) but as many networks as they like (check boxes). Once two or more networks and one topological feature have been selected, direct comparisons can be made by the generated bar charts. A slider to adjust the chart height is offered." ), + tags$img(src = b64_11), + br() + + ) #Tabpanel Topology +) #tabsetPanel diff --git a/views/network.R b/views/network.R new file mode 100644 index 0000000..87f56f4 --- /dev/null +++ b/views/network.R @@ -0,0 +1,45 @@ +networkPage <- div(id = "network_div", + conditionalPanel( + condition = "input.availableNetworks == 0", + bsAlert("tabUploadMainAlert"), + uiOutput("uiLoadGraphOptionsOutput_just_network") + ), + br(), br(), + tabsetPanel( + tabPanel( + "Interactive Network", + visNetworkOutput('tabVizIgraphSimple', width = 1000, height = 550), + class = 'box-panel-padding', + + class = 'box-panel', + ), + + tabPanel( + "Automated Community Detection", + br(), + selectInput( + "automated_annotations", + "Community Detection Algorithms:", + choices = automated_annotations_ui, + selected = selected_automated_annotations, + multiple = FALSE + ), + downloadButton("downloadData", "Export as annotation file", icon("download")), + br(), + br(), + prettyCheckbox( + inputId = "show_labels_algorithms_tab",outline = T,fill = T,bigger = T, + label = "Show Labels", + thick = T, + shape = "curve", + animation = "pulse", + status = "info", + inline = F, + value = F + ), + plotOutput("modularity_plot", width = 1000, height = 600), + eval(ui_dataTable_panel('Modularity_table')), + br() + ) + ) #tabsetPanel +) diff --git a/views/topology.R b/views/topology.R new file mode 100644 index 0000000..b7744dd --- /dev/null +++ b/views/topology.R @@ -0,0 +1,82 @@ +topologyPage <- div(id = "topology_div", + sidebarLayout( + sidebarPanel( + bsAlert("tabTopologySideAlert"), + conditionalPanel(condition = "input.statisticsMethodsMainTabsetPanel == 'tableView'", + uiOutput("uiStoredGraphsOutputSelectTopolopgy")), + conditionalPanel( + condition = "input.statisticsMethodsMainTabsetPanel == 'plotView'", + uiOutput("uiStoredGraphsOutputMultipleSelectTopolopgy") + ), + ), + conditionalPanel(condition = "input.availableNetworks == 0", + mainPanel( + bsAlert("tabTopologyMainAlert"), + tabsetPanel( + id = "statisticsMethodsMainTabsetPanel", + tabPanel( + "Summaries", + value = "tableView", + icon = icon("table"), + div( + div( + checkboxGroupInput( + "statistics2", + "The statistics:", + choices = statistics, + selected = selected_statistics + ), + eval(ui_dataTable_panel("statres", FALSE)), + class = 'box-panel-padding', + class = 'box-panel' + ), + div( + span( + actionButton("btnStatSelectAll", "Select all", style = "float: right;"), + class = "input-group-btn" + ), + span(actionButton("btnStatSelectNone", "Clear"), class = "input-group-btn"), + class = "input-group" + ) + ) + ), + tabPanel( + "Comparative Plots", + value = "plotView", + icon = icon("chart-bar"), + div( + div( + radioButtons( + "statistics", + "The statistics:", + choices = statistics, + selected = selected_statistics + ), + uiOutput("statisticsMethodsPlotRender"), + class = 'box-panel-padding' + ), + class = 'box-panel' + ), + tags$style( + HTML( + ".js-irs-6 .irs-bar {border-top-color: #2C8160; border-bottom-color: #2C8160;} .js-irs-6 .irs-bar-edge {border-color: #2C8160;} + .js-irs-6 .irs-single, .js-irs-6 .irs-bar-edge, .js-irs-6 .irs-bar {background: #2C8160;}" + ) + ), + + sliderInput( + "statisticsPlotPercentMagnify", + "Adjust plot height (% taller)", + min = 0, + max = 100, + value = 0 + ), + span( + actionButton("btnRefreshPalette", "Refresh palette", icon = icon("sync")) + ) + ) + + ) + )) + ) +) diff --git a/views/upload.R b/views/upload.R new file mode 100644 index 0000000..2c39804 --- /dev/null +++ b/views/upload.R @@ -0,0 +1,169 @@ +uploadPage <- div(id = "upload_div", + sidebarLayout( + + # sidebarPanel #### + sidebarPanel( + actionButton("introButton", "Guide Tutorial"), + br(), + hr(), + tags$div(class="network_div", + tags$h3('1. Networks'), + selectInput( + "uiLoadGraphOptionsInput", + "Choose Network(s)", + c( + "File upload" = "oF", + "Example STRING BCAR3 Network" = "oR_String_interactions", + "Example Drosophila TAU Network" = "oR_Drosophila" + ) + )%>% + shinyInput_label_embed( + shiny_iconlink() %>% + bs_embed_popover( + title = paste0("Please follow the steps below: +- Load your network file in tab delimited format +- Give it a name +- Hit the ADD button") + ) + ), + uiOutput("uiLoadGraphOptionsOutput"), + # checkboxInput("weighted",label = "Weight", value =F), + div( + span( + actionButton( + "btnAddNetwork", + "ADD", + icon = icon("plus"), + style = "color: #fff; background-color: #7F461B; border-color: #7F461B" + ), + class = "input-group-btn" + ), + tags$input( + id = "networkName", + type = "text", + class = "form-control", + placeholder = "Type network name ..", + value = "Network name" + ), + class = "input-group" + ), + uiOutput("uiStoredGraphsOutputRadio") + ), + hr(), + tags$div(class="annotations_div", + tags$h3('2. Annotations'), + ######2nd button-annotation + selectInput( + "uiLoadGraphOptionsInput_annotations", + "Choose Annotation(s)", + c( + "File upload" = "oF", + "Example BCAR3 STRING GO KEGG" = "oR_String_Annotation_KEGG", + "Example BCAR3 STRING GO MF" = "oR_String_Annotation_MF", + "Example BCAR3 STRING GO BP" = "oR_String_Annotation_BP", + "Example TAU Drosophila KEGG" = "oR_Drosophila_KEGG", + "Example TAU Drosophila Louvain" = "oR_Drosophila_Luvain" + ) + )%>% + shinyInput_label_embed( + shiny_iconlink() %>% + bs_embed_popover( + title = paste0("Please follow the steps below: +- Load your annotation file in tab delimited format +- Give it a name +- Hit the ADD button") + ) + ), + uiOutput("uiLoadGraphOptionsOutput_annotations"), + div( + span( + actionButton( + "btnAddNetwork2", + "ADD", + icon = icon("plus"), + style = "color: #fff; background-color: #8D4A43; border-color: #8D4A43" + ), + class = "input-group-btn" + ), + tags$input( + id = "annotationName", + type = "text", + class = "form-control", + placeholder = "Type annotation name ..", + value = "Annotation name" + ), + class = "input-group" + ), + uiOutput("uiStoredGraphsOutputRadio_annotations") + ), + hr(), + tags$div(class="expressions_div", + ######3rd button-expression + tags$h3('3. Node-coloring'), + selectInput( + "uiLoadExpressionsInput", + "Choose node - coloring file(s)", + c("File upload" = "oF", + # "Expression_file_STRING" = "oR_Expression_file_STRING", + "Example TAU Drosophila node - coloring file" = "oR_Expression_file_Drosophila" + ) + )%>% + shinyInput_label_embed( + shiny_iconlink() %>% + bs_embed_popover( + title = paste0("Please follow the steps below: +- Load your node - coloring file in tab delimited format +- Give it a name +- Hit the ADD button") + ) + ), + uiOutput("uiLoadExpressionsOutput"), + div( + span( + actionButton( + "btnAddExpression", + "ADD", + icon = icon("plus"), + style = "color: #fff; background-color: #B89778; border-color: #B89778" + ), + class = "input-group-btn" + ), + tags$input( + id = "expressionName", + type = "text", + class = "form-control", + placeholder = "Type expression name ..", + value = "Node - coloring file name" + ), + class = "input-group" + ), + uiOutput("uiStoredGraphsOutputRadioEx") + ) + ), #sidebarPanel + + # mainPanel #### + mainPanel( + conditionalPanel( + condition = "input.availableNetworks == 0", + uiOutput("uiStoredGraphsOutputSelectUpload") + ), + + conditionalPanel( + condition = "input.availableAnnotations == 0", + uiOutput("uiStoredGraphsOutputSelectUpload2") + ), + tabsetPanel( + tabPanel( + "Table View of Network(s)", + icon = icon("table"), + eval(ui_dataTable_panel('datasettab1')) + ), + tabPanel( + "Table View of Annotation(s)", + icon = icon("table"), + eval(ui_dataTable_panel('datasettab2')) + ) + ) #tabsetPanel + ) # mainPanel + ) # sidebarLayout +) diff --git a/views/welcome.R b/views/welcome.R new file mode 100644 index 0000000..4f69e7a --- /dev/null +++ b/views/welcome.R @@ -0,0 +1,24 @@ +welcomePage <- div(id = "welcome_div", + h1("Welcome to NORMA, the NetwORk Makeup Artist"), + strong("a tool that enables the visualization of annotation groups."), + br(), + br(), + helpText( + "NORMA is a handy tool for interactive network annotation, visualization and topological analysis, + able to handle multiple networks and annotations simultaneously. Annotations and/or node groups can + be precomputed or automatically calculated and users can combine several networks and groupings they + are interested in. Annotated networks can be visualized using both pie-chart nodes and shaded convex + hulls and can be shown using several layouts while users can isolate the groups of interest interactively. + In addition, NORMA is suitable for direct comparison of topological features between one or more networks. + In order for NORMA to run, two simple steps are required:" + ), + tags$ul( + tags$li("1. Please load your network file(s), name it and hit the add button"), + tags$li( + "2. Please load your annotation file(s), name it and hit the add button" + ) + ), + tags$ul( + "Input file instructions as well as downloadable examples can be found in the HELP/EXAMPLES page." + ) +) diff --git a/www/Examples/BCAR3/BCAR3.txt b/www/Examples/BCAR3/BCAR3.txt new file mode 100644 index 0000000..6d04212 --- /dev/null +++ b/www/Examples/BCAR3/BCAR3.txt @@ -0,0 +1,92 @@ +Source Target +VCP NPLOC4 +BCAR1 PXN +VCP DERL2 +NSFL1C VCP +CDC42 WASL +VCP DERL1 +NPLOC4 UFD1L +SYVN1 VCP +VCP UFD1L +CDC42 WAS +CDC42 PAK1 +CDC42 BAIAP2 +VCP FAF2 +CDC42 ARHGAP1 +VIMP VCP +CDC42 PARD6A +CDC42 TNK2 +CDC42 ITSN1 +CDC42 ARHGDIA +CDC42 PARD6B +SYVN1 DERL2 +CDC42 PAK2 +VIMP DERL1 +DERL1 DERL2 +WAS BAIAP2 +CDC42 PXN +FAF2 DERL2 +SYVN1 DERL1 +UFD1L FAF2 +NPLOC4 FAF2 +BCAR1 BCAR3 +ITSN1 WASL +BAIAP2 WASL +BCAR1 TNK2 +BCAR1 CDC42 +PAK1 PXN +PAK1 ARHGDIA +FAF2 DERL1 +NEDD9 PXN +PAK2 PAK1 +VIMP DERL2 +SYVN1 FAF2 +VIMP SYVN1 +CDC42 BCAR3 +UFD1L DERL1 +PAK2 PXN +NSFL1C UFD1L +NEDD9 BCAR3 +PARD6B PARD6A +BAIAP2 PXN +NSFL1C NPLOC4 +BCAR1 BAIAP2 +PAK2 PARD6A +SYVN1 UFD1L +WAS WASL +TULP4 SPSB1 +SYVN1 NPLOC4 +VIMP FAF2 +UFD1L DERL2 +NPLOC4 DERL1 +NPLOC4 DERL2 +ITSN1 WAS +VIMP NPLOC4 +TNK2 WAS +NSFL1C FAF2 +TULP4 KCNE4 +BCAR3 TULP4 +BCAR3 KCNE4 +NSFL1C DERL2 +SAT1 BCAR3 +NSFL1C DERL1 +BCAR3 VCP +PAK2 ARHGDIA +KCNE4 TULP1 +BCAR3 TULP1 +PXN WASL +VIMP UFD1L +WAS ARHGAP1 +WAS PXN +WAS PAK1 +BCAR3 SPSB1 +ARHGAP1 ARHGDIA +BCAR1 PARD6A +PAK1 WASL +BCAR1 WASL +ARHGDIA PXN +BCAR1 WAS +PAK2 WASL +WAS PAK2 +ARHGAP1 PXN +ARHGDIA WASL \ No newline at end of file diff --git a/www/Examples/BCAR3/BCAR3_GO_BP.txt b/www/Examples/BCAR3/BCAR3_GO_BP.txt new file mode 100644 index 0000000..a068ffe --- /dev/null +++ b/www/Examples/BCAR3/BCAR3_GO_BP.txt @@ -0,0 +1,99 @@ +cellular process ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SPSB1,SYVN1,TNK2,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL +biological regulation ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +regulation of biological process ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,KCNE4,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +regulation of cellular process ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +response to stimulus ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,NEDD9,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +localization ARHGAP1,BAIAP2,BCAR1,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,PAK2,PXN,SYVN1,TNK2,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL +cellular response to stimulus ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,ITSN1,NEDD9,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS,WASL +signal transduction ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NEDD9,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,VCP,VIMP,WAS,WASL +establishment of localization ARHGAP1,BAIAP2,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +positive regulation of biological process ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NSFL1C,PAK1,PAK2,PARD6A,PXN,TNK2,TULP1,VCP,WAS,WASL +regulation of response to stimulus ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL2,ITSN1,NPLOC4,PAK1,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP,WAS,WASL +transport ARHGAP1,BAIAP2,CDC42,DERL1,DERL2,FAF2,ITSN1,KCNE4,NPLOC4,PAK1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +positive regulation of cellular process ARHGAP1,ARHGDIA,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,ITSN1,NSFL1C,PAK1,PAK2,PXN,TNK2,TULP1,VCP,WAS,WASL +cellular component organization BAIAP2,BCAR1,CDC42,DERL1,FAF2,ITSN1,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PARD6B,PXN,TULP1,VCP,WAS,WASL +response to chemical BAIAP2,BCAR1,BCAR3,CDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS +cellular localization ARHGAP1,CDC42,DERL1,DERL2,FAF2,ITSN1,NPLOC4,NSFL1C,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WAS,WASL +response to organic substance BAIAP2,BCAR1,CDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS +negative regulation of biological process ARHGAP1,ARHGDIA,CDC42,DERL2,ITSN1,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,SYVN1,TNK2,UFD1L,VIMP,WAS,WASL +organonitrogen compound metabolic process CDC42,DERL1,DERL2,FAF2,NPLOC4,NSFL1C,PAK1,PAK2,SAT1,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP +cellular protein metabolic process CDC42,DERL1,DERL2,FAF2,NPLOC4,NSFL1C,PAK1,PAK2,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP +establishment of localization in cell ARHGAP1,CDC42,DERL1,DERL2,FAF2,ITSN1,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP,WAS,WASL +cellular response to organic substance BAIAP2,BCAR1,CDC42,DERL1,DERL2,PAK1,PAK2,PARD6A,PXN,SYVN1,UFD1L,VCP,VIMP,WAS +regulation of signal transduction ARHGAP1,ARHGDIA,BAIAP2,BCAR3,CDC42,ITSN1,NPLOC4,PAK1,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP +response to stress CDC42,DERL1,DERL2,FAF2,NPLOC4,PAK1,PAK2,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS +negative regulation of cellular process ARHGAP1,ARHGDIA,CDC42,DERL2,ITSN1,NPLOC4,PAK1,PAK2,PARD6A,SYVN1,UFD1L,VIMP,WAS,WASL +protein localization ARHGAP1,BAIAP2,DERL1,DERL2,FAF2,NPLOC4,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WASL +cell surface receptor signaling pathway ARHGDIA,BAIAP2,BCAR1,CDC42,ITSN1,NEDD9,PAK1,PAK2,PARD6A,PXN,TNK2,WAS,WASL +regulation of cellular component organization ARHGDIA,BAIAP2,BCAR1,CDC42,DERL2,NSFL1C,PAK1,PXN,TNK2,TULP1,VCP,WAS,WASL +regulation of localization ARHGAP1,BCAR1,DERL2,KCNE4,NSFL1C,PAK1,PARD6A,PARD6B,TNK2,TULP1,VIMP,WAS,WASL +regulation of molecular function ARHGAP1,ARHGDIA,BCAR3,DERL1,FAF2,ITSN1,NSFL1C,PAK1,PAK2,TNK2,VCP,WAS,WASL +response to organonitrogen compound BAIAP2,BCAR1,DERL1,DERL2,FAF2,NPLOC4,PAK1,PXN,SYVN1,UFD1L,VCP,VIMP +intracellular transport ARHGAP1,CDC42,DERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP,WAS,WASL +cellular protein modification process CDC42,DERL1,DERL2,PAK1,PAK2,SPSB1,SYVN1,TNK2,TULP4,UFD1L,VCP,VIMP +organelle organization BAIAP2,BCAR1,CDC42,FAF2,NEDD9,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,WAS,WASL +cellular protein localization DERL1,DERL2,FAF2,NPLOC4,SYVN1,TULP1,TULP4,UFD1L,VCP,VIMP,WASL +vesicle-mediated transport ARHGAP1,BAIAP2,CDC42,FAF2,ITSN1,PAK1,TNK2,TULP1,VCP,WAS,WASL +positive regulation of response to stimulus ARHGAP1,BAIAP2,BCAR1,BCAR3,CDC42,ITSN1,PAK1,PAK2,VCP,WAS,WASL +regulation of catalytic activity ARHGAP1,ARHGDIA,FAF2,ITSN1,NSFL1C,PAK1,PAK2,TNK2,VCP,WAS,WASL +cellular component assembly BAIAP2,CDC42,DERL1,NEDD9,NSFL1C,PARD6A,PARD6B,PXN,VCP,WAS,WASL +enzyme linked receptor protein signaling pathway BAIAP2,BCAR1,CDC42,ITSN1,PAK1,PAK2,PARD6A,PXN,TNK2,WASL +regulation of immune response BAIAP2,BCAR1,CDC42,NPLOC4,PAK1,PAK2,UFD1L,VIMP,WAS,WASL +cytoskeleton organization BAIAP2,BCAR1,CDC42,NEDD9,NSFL1C,PAK1,PAK2,PARD6A,WAS,WASL +organonitrogen compound catabolic process DERL1,DERL2,FAF2,NPLOC4,NSFL1C,SAT1,SYVN1,UFD1L,VCP,VIMP +positive regulation of cellular component organization ARHGDIA,BAIAP2,CDC42,NSFL1C,PAK1,PXN,TULP1,VCP,WAS,WASL +cellular response to stress DERL1,DERL2,FAF2,NPLOC4,PAK2,PXN,SYVN1,UFD1L,VCP,VIMP +cellular catabolic process DERL1,DERL2,FAF2,NPLOC4,NSFL1C,SAT1,SYVN1,UFD1L,VCP,VIMP +regulation of intracellular signal transduction ARHGAP1,ARHGDIA,CDC42,ITSN1,NPLOC4,PAK1,PAK2,SYVN1,UFD1L,VIMP +multi-organism process BAIAP2,CDC42,DERL1,DERL2,ITSN1,PAK2,PARD6A,VCP,VIMP,WASL +immune system process BAIAP2,BCAR1,CDC42,FAF2,PAK1,PAK2,TNK2,VCP,WAS,WASL +proteasome-mediated ubiquitin-dependent protein catabolic process DERL1,DERL2,FAF2,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP +transmembrane receptor protein tyrosine kinase signaling pathway BAIAP2,BCAR1,CDC42,ITSN1,PAK1,PAK2,PXN,TNK2,WASL +import into cell ARHGAP1,BAIAP2,CDC42,ITSN1,PAK1,TNK2,TULP1,WAS,WASL +protein transport ARHGAP1,DERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP +regulation of cellular protein metabolic process BCAR3,DERL1,DERL2,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP +retrograde protein transport, ER to cytosol DERL1,DERL2,FAF2,NPLOC4,SYVN1,UFD1L,VCP,VIMP +small GTPase mediated signal transduction ARHGAP1,BCAR3,CDC42,ITSN1,PAK1,PAK2,TNK2,WAS +actin cytoskeleton organization BAIAP2,BCAR1,CDC42,NEDD9,PAK1,PAK2,WAS,WASL +regulation of cytoskeleton organization ARHGDIA,BAIAP2,CDC42,NSFL1C,PAK1,PXN,WAS,WASL +endocytosis BAIAP2,CDC42,ITSN1,PAK1,TNK2,TULP1,WAS,WASL +protein modification by small protein conjugation or removal CDC42,DERL1,SPSB1,SYVN1,TULP4,UFD1L,VCP,VIMP +generation of neurons ARHGDIA,BAIAP2,CDC42,PAK1,PAK2,PARD6B,TULP1,WASL +regulation of apoptotic process ARHGDIA,BCAR1,ITSN1,PAK1,PAK2,SYVN1,VCP,VIMP +regulation of phosphate metabolic process BCAR3,ITSN1,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP +regulation of transport ARHGAP1,DERL2,KCNE4,PAK1,TNK2,TULP1,VIMP,WASL +regulation of protein modification process BCAR3,DERL1,NSFL1C,PAK1,PAK2,PARD6A,TNK2,VCP +anatomical structure morphogenesis BAIAP2,BCAR3,CDC42,PAK1,PARD6B,SAT1,TULP1,WASL +ubiquitin-dependent ERAD pathway DERL1,DERL2,FAF2,NPLOC4,SYVN1,VCP,VIMP +response to topologically incorrect protein DERL1,DERL2,FAF2,SYVN1,UFD1L,VCP,VIMP +positive regulation of cytoskeleton organization BAIAP2,CDC42,NSFL1C,PAK1,PXN,WAS,WASL +immune response-activating cell surface receptor signaling pathway BAIAP2,BCAR1,CDC42,PAK1,PAK2,WAS,WASL +regulation of actin cytoskeleton organization ARHGDIA,BAIAP2,CDC42,PAK1,PXN,WAS,WASL +positive regulation of cellular component biogenesis BAIAP2,CDC42,PAK1,PXN,VCP,WAS,WASL +neuron projection development BAIAP2,CDC42,PAK1,PAK2,PARD6B,TULP1,WASL +protein ubiquitination CDC42,DERL1,SPSB1,SYVN1,TULP4,VCP,VIMP +immune effector process BAIAP2,CDC42,FAF2,PAK1,VCP,WAS,WASL +regulation of response to stress DERL2,NPLOC4,PAK1,PAK2,SYVN1,UFD1L,VIMP +movement of cell or subcellular component BCAR1,CDC42,PAK2,PXN,TNK2,WAS,WASL +regulation of phosphorylation BCAR3,ITSN1,PAK1,PAK2,PARD6A,TNK2,VCP +positive regulation of signal transduction ARHGAP1,BAIAP2,BCAR3,ITSN1,PAK1,PAK2,VCP +Fc receptor signaling pathway BAIAP2,CDC42,PAK1,PAK2,WAS,WASL +cellular response to topologically incorrect protein DERL1,DERL2,SYVN1,UFD1L,VCP,VIMP +response to unfolded protein DERL1,DERL2,FAF2,SYVN1,VCP,VIMP +positive regulation of supramolecular fiber organization BAIAP2,CDC42,PAK1,PXN,WAS,WASL +phagocytosis BAIAP2,CDC42,PAK1,TULP1,WAS,WASL +actin filament organization BAIAP2,BCAR1,CDC42,NEDD9,WAS,WASL +regulation of actin filament organization BAIAP2,CDC42,PAK1,PXN,WAS,WASL +regulation of protein complex assembly BAIAP2,CDC42,PAK1,VCP,WAS,WASL +viral process CDC42,DERL1,ITSN1,PAK2,PARD6A,VCP +regulation of plasma membrane bounded cell projection organization ARHGDIA,BAIAP2,CDC42,PAK1,WAS,WASL +regulation of growth BCAR1,CDC42,DERL2,NEDD9,PAK1,PAK2 +positive regulation of phosphorylation BCAR3,ITSN1,PAK1,PAK2,TNK2,VCP +cellular response to endogenous stimulus BAIAP2,BCAR1,PAK1,PARD6A,PXN,VIMP +positive regulation of protein modification process BCAR3,DERL1,PAK1,PAK2,TNK2,VCP +regulation of hydrolase activity ARHGAP1,ARHGDIA,NSFL1C,PAK2,TNK2,VCP +vascular endothelial growth factor receptor signaling pathway BAIAP2,BCAR1,CDC42,PAK2,PXN +Fc-gamma receptor signaling pathway involved in phagocytosis BAIAP2,CDC42,PAK1,WAS,WASL +dendrite development BAIAP2,CDC42,PAK2,TULP1,WASL +endoplasmic reticulum unfolded protein response DERL1,DERL2,SYVN1,VCP,VIMP +establishment or maintenance of cell polarity CDC42,NSFL1C,PAK1,PARD6A,PARD6B diff --git a/www/Examples/BCAR3/BCAR3_GO_MF.txt b/www/Examples/BCAR3/BCAR3_GO_MF.txt new file mode 100644 index 0000000..1a7dc02 --- /dev/null +++ b/www/Examples/BCAR3/BCAR3_GO_MF.txt @@ -0,0 +1,20 @@ +protein binding ARHGAP1,BAIAP2,BCAR1,BCAR3,CDC42,DERL1,FAF2,ITSN1,KCNE4,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PXN,SAT1,SYVN1,TNK2,TULP1,UFD1L,VCP,VIMP,WAS,WASL +enzyme binding ARHGAP1,BCAR1,BCAR3,CDC42,DERL1,FAF2,ITSN1,NPLOC4,NSFL1C,PAK1,PAK2,PARD6A,PXN,SYVN1,TNK2,UFD1L,VCP,VIMP,WAS +molecular function regulator ARHGAP1,ARHGDIA,BCAR3,FAF2,ITSN1,NSFL1C,PAK2,TNK2,VCP,WAS,WASL +enzyme regulator activity ARHGAP1,ARHGDIA,FAF2,ITSN1,NSFL1C,PAK2,TNK2,VCP,WAS,WASL +identical protein binding BAIAP2,CDC42,PAK1,PAK2,SAT1,TNK2,VCP,WAS +protein kinase binding BCAR1,CDC42,PAK1,PAK2,PARD6A,PXN,WAS +GTPase binding ARHGAP1,BCAR3,ITSN1,PAK1,PAK2,PARD6A,WAS +protein domain specific binding ARHGAP1,BAIAP2,BCAR1,CDC42,TNK2,VCP,WAS +ubiquitin protein ligase binding DERL1,FAF2,NPLOC4,PXN,TNK2,VCP +Ras GTPase binding ARHGAP1,ITSN1,PAK1,PAK2,PARD6A,WAS +ATPase binding DERL1,NSFL1C,SYVN1,UFD1L,VIMP +Rho GTPase binding ITSN1,PAK1,PAK2,PARD6A,WAS +GTPase regulator activity ARHGAP1,ARHGDIA,TNK2,WAS,WASL +enzyme activator activity ARHGAP1,ARHGDIA,ITSN1,PAK2,VCP +ubiquitin-specific protease binding DERL1,SYVN1,VCP,VIMP +protein serine/threonine kinase activity CDC42,PAK1,PAK2,TNK2 +Rac GTPase binding PAK1,PAK2,WAS +ubiquitin binding FAF2,NPLOC4,NSFL1C +SH3 domain binding ARHGAP1,BCAR1,WAS +protein binding, bridging ARHGAP1,BAIAP2,BCAR3 diff --git a/www/Examples/BCAR3/BCAR3_KEGG.txt b/www/Examples/BCAR3/BCAR3_KEGG.txt new file mode 100644 index 0000000..d60cdab --- /dev/null +++ b/www/Examples/BCAR3/BCAR3_KEGG.txt @@ -0,0 +1,27 @@ +Protein processing in endoplasmic reticulum DERL1,DERL2,NPLOC4,NSFL1C,SYVN1,UFD1L,VCP,VIMP +Regulation of actin cytoskeleton BAIAP2,BCAR1,CDC42,PAK1,PAK2,PXN,WAS,WASL +Chemokine signaling pathway BCAR1,CDC42,PAK1,PXN,WAS,WASL +Bacterial invasion of epithelial cells BCAR1,CDC42,PXN,WAS,WASL +Tight junction CDC42,PARD6A,PARD6B,WAS,WASL +Axon guidance CDC42,PAK1,PAK2,PARD6A,PARD6B +Focal adhesion BCAR1,CDC42,PAK1,PAK2,PXN +Endocytosis CDC42,PARD6A,PARD6B,WAS,WASL +Adherens junction BAIAP2,CDC42,WAS,WASL +Fc gamma R-mediated phagocytosis CDC42,PAK1,WAS,WASL +Rap1 signaling pathway BCAR1,CDC42,PARD6A,PARD6B +Human papillomavirus infection CDC42,PARD6A,PARD6B,PXN +Pathogenic Escherichia coli infection CDC42,WAS,WASL +Shigellosis CDC42,WAS,WASL +Renal cell carcinoma CDC42,PAK1,PAK2 +Salmonella infection CDC42,WAS,WASL +T cell receptor signaling pathway CDC42,PAK1,PAK2 +Leukocyte transendothelial migration BCAR1,CDC42,PXN +Proteoglycans in cancer CDC42,PAK1,PXN +Ras signaling pathway CDC42,PAK1,PAK2 +MAPK signaling pathway CDC42,PAK1,PAK2 +VEGF signaling pathway CDC42,PXN +Epithelial cell signaling in Helicobacter pylori infection CDC42,PAK1 +ErbB signaling pathway PAK1,PAK2 +Choline metabolism in cancer WAS,WASL +Neurotrophin signaling pathway ARHGDIA,CDC42 +Hippo signaling pathway PARD6A,PARD6B \ No newline at end of file diff --git a/www/Examples/BioGrid_Chicken_Gallus/BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt b/www/Examples/BioGrid_Chicken_Gallus/BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt new file mode 100644 index 0000000..eef2edf --- /dev/null +++ b/www/Examples/BioGrid_Chicken_Gallus/BioGrid_Chicken_Gallus_Pathways_KEGG_PATHWAY_FILTERED.txt @@ -0,0 +1,13 @@ +ErbB signaling pathway Q00944,Q04929,P00523,P01109,Q90891,P13387,Q9W7C5,P05625,Q04982 +Carbon metabolism P00356,O57391,P00548,P07341,P51903,Q5ZLN1,Q5ZME2,P51913,P00940,P00508 +Biosynthesis of antibiotics P00356,O57391,P00548,P07341,O57535,P51903,Q5ZLN1,P00337,Q5ZME2,P51913,P00940,P00508,P00340,P38024 +Gap junction P00523,P09244,Q90891,P08070,P13387,P50147,P09207,P05625,P13863 +Ribosome Q98TF6,Q98TF8,P67883,P32429,P61355,P47826,Q5ZJ56,P47836,P50890,P18660,P22451 +Focal adhesion Q00944,Q04929,P00523,Q90623,P13387,P05094,P60706,P53478,P11799,P49024,P12003,Q04982,P62207,P05625 +Progesterone-mediated oocyte maturation Q04619,P07812,P18652,P11501,P50147,P05625,Q04982,P13863 +Vascular smooth muscle contraction Q8AYS8,P11799,Q90623,P05419,P63270,Q90891,P62207,P05625,Q04982 +Protein processing in endoplasmic reticulum Q04619,Q5ZKF5,Q90593,Q8JG64,O73885,P11501,P08110,Q5ZHY5,P81628,Q5ZK03,P08106 +Pyruvate metabolism P00548,P00337,Q5ZME2,P11029,P00340 +VEGF signaling pathway Q00944,P00523,P49024,Q90891,P05625,Q00649 +Insulin signaling pathway Q04929,P05419,Q90891,P11029,P62207,P05625,Q04982,O13016 +Tight junction P00523,P48463,P05094,P60706,P53478,Q01406 \ No newline at end of file diff --git a/www/Examples/BioGrid_Chicken_Gallus/Biogrid_no_self_loops.txt b/www/Examples/BioGrid_Chicken_Gallus/Biogrid_no_self_loops.txt new file mode 100644 index 0000000..8b23baf --- /dev/null +++ b/www/Examples/BioGrid_Chicken_Gallus/Biogrid_no_self_loops.txt @@ -0,0 +1,333 @@ +Source Target +P62760 P60706 +P62764 P60706 +O15392 P53352 +Q9IAY5 P49024 +Q9IAY5 P49024 +P49024 Q9IAY5 +Q13625 P46936 +O93512 O60542 +P41238 Q7T2T1 +O42414 Q9JI92 +O42414 Q9JI92 +P05556 Q00944 +P05556 P49024 +P27986 Q9DDT2 +P28497 P23297 +P49024 P12003 +P49024 Q00944 +Q4KWZ7 Q9DEA3 +Q9DEA3 Q4KWZ7 +P53478 Q9UUJ1 +P53478 P32390 +P53478 P78929 +P12003 P60010 +P79987 P56517 +P79987 P56519 +P79987 Q9W7I5 +P79987 Q3C1E9 +Q9W7I5 P79987 +P56517 P79987 +P56519 P79987 +Q5ZJY5 O93257 +Q5R1T0 P56517 +Q5R1T0 P56519 +Q5R1T0 P56517 +Q5R1T0 P56519 +Q5R1T0 P56520 +Q09472 P17678 +P23204 P17678 +P23204 P23824 +P23204 P23825 +Q92993 P70082 +Q92993 P0C1H3 +Q92993 P84247 +Q92993 P62801 +P56519 Q9HAZ2 +O18738 P31696 +Q8AYS7 Q1T7B8 +Q8AYS7 Q1T7C0 +Q8AYS7 Q1T7B7 +Q8AYS7 Q1T7C1 +Q8AYS7 Q1T7B9 +Q90ZF9 Q1T7B8 +Q90ZF9 Q1T7C0 +Q90ZF9 Q1T7B7 +Q90ZF9 Q1T7C1 +Q90ZF9 Q1T7B9 +Q76I90 Q76I89 +Q76I89 Q76I90 +Q8AYS8 P09572 +Q8AYS8 P11501 +Q8AYS8 Q90593 +Q8AYS8 P16053 +Q8AYS8 P08106 +Q8AYS8 O73885 +Q8AYS8 Q5ZHY5 +Q8AYS8 Q5ZL72 +Q8AYS8 P09207 +Q8AYS8 Q5ZLC5 +Q8AYS8 P05094 +Q8AYS8 P00508 +Q8AYS8 P54097 +Q8AYS8 P17153 +Q8AYS8 Q5ZMT0 +Q8AYS8 P28497 +Q8AYS8 P62207 +Q8AYS8 Q5ZKC9 +Q8AYS8 Q5F3W6 +Q8AYS8 P60878 +Q8AYS8 Q5ZM44 +Q8AYS8 P08250 +Q8AYS8 Q5ZMB2 +Q8AYS8 Q00649 +Q8AYS8 Q5ZKK4 +Q8AYS8 Q5ZIV5 +Q8AYS8 Q5ZMD1 +Q8AYS8 Q5ZLQ6 +Q8AYS8 P07090 +Q8AYS8 P81628 +Q8AYS8 P42324 +Q8AYS8 P62758 +Q8AYS8 P62764 +Q8AYS8 P08110 +Q8AYS8 O93510 +Q8AYS8 P13731 +Q8AYS8 Q5F425 +Q8AYS8 P18359 +Q8AYS8 P02789 +Q8AYS8 Q5ZL57 +Q8AYS8 P19121 +Q8AYS8 O57391 +Q8AYS8 Q9PTG6 +Q8AYS8 P63270 +Q8AYS8 P51913 +Q8AYS8 P05122 +Q8AYS8 Q5ZME2 +Q8AYS8 Q5ZHP5 +Q8AYS8 P48463 +Q8AYS8 P04354 +Q8AYS8 P50890 +Q8AYS8 P05419 +Q8AYS8 P80566 +Q8AYS8 P31395 +Q8AYS8 P00940 +Q8AYS8 Q5ZLN1 +Q8AYS8 Q5ZJF4 +Q8AYS8 P00340 +Q8AYS8 P09654 +Q8AYS8 O42163 +Q8AYS8 P16580 +Q8AYS8 P02112 +Q8AYS8 P80026 +Q8AYS8 Q5ZLG1 +Q8AYS8 O57535 +Q8AYS8 Q07212 +Q8AYS8 O13268 +Q8AYS8 P00337 +Q8AYS8 P00356 +Q8AYS8 P51903 +Q8AYS8 P07341 +Q8AYS8 P00548 +Q8AYS8 P14732 +Q8AYS8 P13648 +Q8AYS8 Q5F3W6 +Q8AYS8 P42324 +Q8AYS8 P60878 +Q8AYS8 P17153 +Q8AYS8 Q5ZL72 +Q8AYS8 Q5F425 +Q8AYS8 P80566 +Q8AYS8 Q01406 +Q8AYS8 P08106 +Q8AYS8 P31395 +Q8AYS8 Q90593 +Q8AYS8 Q04619 +Q8AYS8 P14731 +Q5F3W6 Q8AYS8 +P42324 Q8AYS8 +P60878 Q8AYS8 +P17153 Q8AYS8 +Q5ZL72 Q8AYS8 +Q5F425 Q8AYS8 +P80566 Q8AYS8 +Q01406 Q8AYS8 +P08106 Q8AYS8 +P31395 Q8AYS8 +Q90593 Q8AYS8 +Q04619 Q8AYS8 +P14731 Q8AYS8 +P84023 P49140 +P84023 P83038 +P38531 P04637 +P38531 P06876 +P38531 P01103 +F1N9Y5 P00548 +F1N9Y5 P09244 +F1N9Y5 O73885 +F1N9Y5 Q5ZL72 +F1N9Y5 P11501 +F1N9Y5 Q90705 +F1N9Y5 Q5ZLC5 +F1N9Y5 P51913 +F1N9Y5 Q5ZM98 +F1N9Y5 P38024 +F1N9Y5 P47826 +F1N9Y5 P08070 +F1N9Y5 Q90593 +F1N9Y5 P15771 +F1N9Y5 Q9IAY5 +F1N9Y5 P50890 +F1N9Y5 Q5ZJK8 +F1N9Y5 P51903 +F1N9Y5 P67883 +F1N9Y5 P00340 +F1N9Y5 Q6EE31 +F1N9Y5 P22451 +F1N9Y5 Q8UVD9 +F1N9Y5 Q5ZJU3 +F1N9Y5 O57535 +F1N9Y5 Q5ZLP8 +F1N9Y5 Q04619 +F1N9Y5 Q5ZIQ3 +F1N9Y5 P47836 +F1N9Y5 Q5ZKC9 +F1N9Y5 Q5ZK03 +F1N9Y5 Q8AYC9 +F1N9Y5 P00356 +F1N9Y5 Q5ZLN4 +F1N9Y5 Q5ZJ56 +F1N9Y5 P18660 +F1N9Y5 Q5ZMT0 +F1N9Y5 Q8JFP1 +F1N9Y5 P32429 +F1N9Y5 P16039 +F1N9Y5 P00337 +F1N9Y5 P08106 +F1N9Y5 Q5ZMS3 +F1N9Y5 P61355 +F1N9Y5 Q5ZJ54 +F1N9Y5 P63247 +F1N9Y5 Q5ZKA5 +F1N9Y5 P08629 +F1N9Y5 P16527 +F1N9Y5 Q5ZMN2 +F1N9Y5 Q5ZJZ5 +F1N9Y5 P18359 +F1N9Y5 Q5ZKF5 +F1N9Y5 Q5ZLN1 +F1N9Y5 Q9YGC1 +F1N9Y5 Q5ZLE6 +F1N9Y5 Q8UW59 +F1N9Y5 Q5ZKB9 +F1N9Y5 P13863 +F1N9Y5 Q90WU3 +F1N9Y5 Q5ZI72 +F1N9Y5 Q5ZIA5 +F1N9Y5 P11029 +F1N9Y5 Q98TF8 +F1N9Y5 Q5ZL42 +F1N9Y5 Q5ZKC1 +F1N9Y5 P17790 +F1N9Y5 Q5ZMN3 +F1N9Y5 Q98TF6 +F1N9Y5 Q5ZLC6 +F1N9Y5 P24367 +F1N9Y5 P11799 +F1N9Y5 Q5ZK62 +F1N9Y5 P26584 +F1N9Y5 P14315 +F1N9Y5 Q5ZKG5 +F1N9Y5 P42558 +F1N9Y5 P41239 +F1N9Y5 P05419 +F1N9Y5 P43347 +F1N9Y5 P63283 +F1N9Y5 Q5ZJN4 +F1N9Y5 Q5ZL57 +F1N9Y5 Q8JG64 +F1N9Y5 O42392 +F1N9Y5 Q5ZK01 +F1N9Y5 P19179 +F1N9Y5 O93256 +F1N9Y5 Q02391 +P28683 P62140 +Q6A078 P00698 +P68400 O13016 +P12931 O13016 +P07812 Q04619 +P07812 P08106 +P62207 Q90623 +Q90ZF9 Q76I89 +Q90ZF9 Q76I90 +Q90ZF9 Q76I89 +P00523 P19785 +P11501 Q5ZL72 +P11501 P08106 +Q04619 Q5ZL72 +Q04619 P08106 +P68399 P01109 +P67868 P01109 +Q12341 P02263 +Q12341 P62801 +Q5ZL72 P08106 +Q5ZL72 Q04619 +Q04619 P08106 +Q06592 P62801 +Q06592 P84247 +P39979 P62801 +Q3C1E9 P79987 +Q3C1E9 Q5R1S9 +Q3C1E9 P84247 +Q3C1E9 P62801 +P07812 Q15185 +P07812 P07900 +P07812 P34932 +P07812 P25685 +P07812 P31948 +P25293 P70082 +P25293 P0C1H3 +P25293 P84247 +P25293 P62801 +Q702N8 P68139 +A4UGR9 P68139 +P68139 P42639 +P42684 Q04929 +Q92831 P02263 +Q92831 P62801 +Q9DEA3 Q4KWZ7 +Q02842 Q5ZJL9 +Q15185 P11501 +P00523 P51913 +Q04982 Q5ZIK9 +Q04982 P48463 +Q04982 Q5ZM98 +Q04982 P67869 +Q04982 O73885 +Q04982 Q5ZMN3 +Q04982 P16039 +Q04982 P42558 +Q04982 Q6U7I1 +Q04982 P05625 +Q04982 Q5ZMD1 +Q04982 Q5ZLQ6 +Q04982 Q5ZMT0 +Q04982 Q5ZKC9 +Q04982 P50147 +Q04982 Q5F3W6 +Q04982 O57476 +Q04982 P11501 +Q04982 Q90828 +Q04982 Q90891 +Q04982 Q90593 +Q04982 P08110 +P18652 O95071 +O42414 P16092 +P16092 O42414 +P61088 Q9DEA3 +Q15819 Q9DEA3 +Q9W7C5 Q401C0 +Q401C0 Q9W7C5 +P11275 P13387 +Q7T0L4 Q9PU53 +Q07496 P32004 diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_BP.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_BP.txt new file mode 100644 index 0000000..2f7250b --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_BP.txt @@ -0,0 +1,147 @@ +protein biosynthesis RPS15,RPS15,RPL37,RPS5,RPL35,FAU,RPS11,RPS21,RPL18,RPS13,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,MRPS12,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL19,RPS16,RPS14,NARS,RPL7,RPS6,RPS2,U16,NACA,RPL18A,RPL13,RPL29,RPS15A,RPS20,RPS3A,RPS18,RPL14,MRPL34,RPL22,RPL34,RPS27A,EEF1D,RPL15,RPL9,RPL6,RPL26,RPL4,RPS4X,RPS24,RPS9,RPL13A,RPL17,EEF1A1,RPL5,RPL23,MDS1,RPS7,LOC402057,RPS25 +immune response HLA-DPB1,HLA-DPB1,TAPBP,IL2RG,HLA-DMA,HLA-DQA1,IL16,221651_x_at,IGLV3-10,ARL6IP2,HLA-B,CD164,HLA-DRA,HLA-DMB,TRIM22,HLA-DRB5,HLA-DRB1,IGL@,IGLC1,IGLV3-25,TRB@,HLA-DRB4,LTB,ARHGDIB,C6orf12,HLA-DQB1,HLA-DPA1,TRBV3-1,GPSM3,IGLC2,TRA@,214836_x_at,IGHV1-69,IGHA1,HLA-A,CD7,IGHG3,IGLJ3,IL7R,IL6ST,HLA-C,IGKC,IGHM +signal transduction AKT1,AKT1,STAT1,IL2RG,CXCR4,OPHN1,CCL5,PGF,INPP5D,PRKAR1A,CD164,RPS27,HLA-DRB5,CD74,HLA-DRB1,PRKAR2A,CD53,HLA-DRB4,LTB,HSP90AA1,OGT,ROCK1,IFNGR1,PTGES3,HMGB1,GTF2I,GNB2L1,CAP1,SPN,NPM1,IQGAP1,PAFAH1B1,EIF3S3,RAC2,COPS2,PRKACB,PDE4C +antigen presentation, endogenous antigen HLA-J,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,CD74,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM +antigen processing, endogenous antigen via ... CAST,CAST,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM +regulation of transcription, DNA-dependent NFE2L2,NFE2L2,STAT1,MGC2474,LYSMD4,ZMYND11,ZNF160,CDC5L,TRIM22,SFPQ,IFI16,HCLS1,ATRX,HMGB1,CSDE1,SCAND1,RPL6,LOC342346,SUB1 +electron transport MT1X,MT1X,MT1L,COX4I1,NCF1,SDHA,UQCRQ,UQCRC1,GPX4,UQCRFS1,NDUFV1,COX5B,NDUFS3,NDUFS8,COX5A,NDUFB4,CYC1,COX8A,RPA1 +protein amino acid phosphorylation AKT1,AKT1,RAVER2,GMFG,PRKAR1A,PRKD2,MYLK,PRKAR2A,ROCK1,TGFB1,PCM1,U16,LCK,CSNK1A1,HNRPDL,RPL4,PRKACB +antigen processing, exogenous antigen via M... HLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1 +antigen presentation, exogenous antigen HLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1 +cell adhesion ITGAV,ITGAV,LAMC1,CCL5,FNBP4,ADRM1,CD164,TGFB1I1,SELL,DGCR6,COL6A1,PLEKHC1,CD2,ITGB2 +intracellular signaling cascade STAT1,STAT1,RAVER2,INPP5D,PRKAR1A,PRKD2,NCF1,PRKAR2A,WSB1,HCLS1,HMHA1,ROCK1,LCK +transcription from RNA polymerase II promoter TARDBP,TARDBP,NFE2L2,STAT1,POLR2J,TGFB1I1,TCEA1,HMHA1,POLR2I,BTF3,COPS2,POLR2E +cell motility CALD1,CALD1,CAPZA1,CCL5,TPM1,MSN,ACTR3,ARPC1B,ARHGDIB,ARPC3,ARPC5,PAFAH1B1 +detection of pest, pathogen or parasite HLA-DPB1,HLA-DPB1,HLA-DMA,HLA-J,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-G +nuclear mRNA splicing, via spliceosome HNRPM,HNRPM,SFRS10,HNRPH3,SFPQ,HIATL1,RPL35A,SF3B1,HNRPA2B1,HNRPA1 +mitochondrial electron transport, NADH to u... NDUFC1,NDUFC1,NDUFB6,NDUFB2,NDUFV1,NDUFS7,NDUFS3,NDUFS8,NDUFB4 +development CECR1,CECR1,CLPTM1,CD164,ARHGDIB,COL6A3,MYH11,ITGB2,DDX1 +phosphate transport COL6A3,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2 +antigen presentation HLA-J,HLA-J,HLA-B,HLA-E,C6orf12,HLA-F,HLA-A,HLA-G,HLA-C +transport G3BP,G3BP,SLC25A16,UCP2,SLC25A11,TMED5,SLC25A3,ATP2A3 +cell proliferation RPS21,RPS21,PGF,ZMYND11,RPS27,CD74,IFI16,MIF,ISG20 +anti-apoptosis AKT1,AKT1,FAIM3,SON,HMGB1,HSP90B1,NPM1,TPT1,PRDX2 +cell surface receptor linked signal transdu... CD3D,CD3D,CD2,CD79A,BIRC2,MIF,CSNK1A1,IL7R,IL6ST +generation of precursor metabolites and energy ACO2,ACO2,ATPIF1,COX4I1,ECH1,UQCR,SLC25A3,COX8A +protein complex assembly TAPBP,TAPBP,CAPZA1,GPAA1,LAMC1,TCEB2,CD3D,CD74 +mRNA processing SFPQ,SFPQ,SRP46,DHX15,HNRPR,SFRS11,SFRS5,RNPC2 +positive regulation of cell proliferation SSR1,SSR1,CAPNS1,PGF,RPL31,HCLS1,TGFB1,NAP1L1 +RNA splicing HNRPH3,HNRPH3,SFPQ,DHX15,PPP2CA,ZNF638,SFRS11 +transcription LYSMD4,LYSMD4,POLR2J,ZNF160,SFPQ,POLR1B,PCM1 +regulation of translational initiation EIF4G1,EIF4G1,EIF4A2,EIF4G2,EIF5,EIF3S3,DDX1 +pathogenesis HLA-DPB1,HLA-DPB1,HLA-DRB5,HLA-DRB1,HLA-DRB4 +proton transport ATP5J2,ATP5J2,ATP6V0D1,UCP2,ATP6V0B,ATP6V0C +RNA processing HNRPU,HNRPU,RBPMS,DDX17,HNRPH1,HNRPDL,RNPC2 +regulation of transcription from RNA polyme... THRAP5,THRAP5,PRKAR1A,HMGB1,LOC342346,SUB1 +protein transport RAB1A,RAB1A,RAB6A,RAB6C,TLOC1,HSP90B1,COPB +small GTPase mediated signal transduction RAP1B,RAP1B,RGS19,RAB1A,RAB6A,RAB6C,RPL29 +response to virus CCL5,CCL5,IRF7,TRIM22,IFI16,IFNGR1,ISG20 +inflammatory response CCL5,CCL5,IRF7,MEFV,ALOX5AP,TGFB1,ITGB2 +actin cytoskeleton organization and biogenesis ARHGDIB,ARHGDIB,PLEKHC1,DST,ROCK1,ARPC5 +protein folding PPIB,PPIB,HSP90AA1,HSP90B1,EIF3S3,KTN1 +induction of apoptosis CD2,CD2,PPP2CA,BCLAF1,LCK,RPS3A,SCAND1 +skeletal development CNOT2,CNOT2,COL1A1,TGFB1,COL1A2,MYH11 +regulation of progression through cell cycle STAT1,STAT1,PGF,ERH,PPP2CA,LCK,SCAND1 +negative regulation of cell proliferation CD164,CD164,TGFB1I1,TGFB1,CDKN1B,NPM1 +cellular defense response HLA-J,HLA-J,CCL5,NCF1,FAIM3,SPN,HLA-G +translational elongation RPLP2,RPLP2,TUFM,EEF1B2,EEF1D,EEF1A1 +muscle development TAGLN,TAGLN,LYSMD4,SMTN,COL6A3,MBNL1 +ATP synthesis coupled proton transport ATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3 +proteolysis CAST,CAST,MASP1,YME1L1,MMP2,IGHG3 +microtubule-based movement 220725_x_at,220725_x_at,KNS2,KTN1 +metabolism ACO2,ACO2,ECH1,POPDC3,IGLC2,DIP2A +apoptosis AD7C-NTP,AD7C-NTP,FIS1,ITGB2,CROP +amino acid transport SLC38A2,SLC38A2,218041_x_at,RPL24 +response to unfolded protein HSP90AA1,HSP90AA1,HSP90B1,EIF3S3 +intracellular protein transport VDP,VDP,NAPA,CLTC,CD74,NPM1,COPE +transcription initiation from RNA polymeras... THRAP5,THRAP5,IRF7,THRAP1,GTF2I +response to oxidative stress CCL5,CCL5,GPX1,GPX4,CSDE1,PRDX2 +nucleobase, nucleoside, nucleotide and nucl... NDUFA10,NDUFA10,ERH,208246_x_at +carbohydrate metabolism LOC91316,LOC91316,MAN2B1,AKR7A2 +protein modification SUMO2,SUMO2,RPN1,MAN2B1,RPS27A +ubiquitin-dependent protein catabolism USP34,USP34,UBE4A,PSMB9,PSMB8 +defense response CD48,CD48,PTPRCAP,HLA-B,CD79A +DNA replication MCM3AP,MCM3AP,NARS,NAP1L1,SET +androgen receptor signaling pathway THRAP5,THRAP5,THRAP1,TGFB1I1 +protein targeting to mitochondrion TIMM8B,TIMM8B,TOMM20,TIMM13 +oxygen transport SLC38A2,SLC38A2,218041_x_at +G-protein coupled receptor protein signalin... AKT1,AKT1,RGS19,CXCR4,FNBP4 +nervous system development OPHN1,OPHN1,MBNL1,PAFAH1B1 +mitochondrial transport UCP2,UCP2,HSP90AA1,EIF3S3 +embryo implantation LOC283412,LOC283412,RPL29 +DNA repair SFPQ,SFPQ,ATRX,HMGB1,RPA1 +protein refolding HSP90AA1,HSP90AA1,EIF3S3 +positive regulation of nitric oxide biosynt... HSP90AA1,HSP90AA1,EIF3S3 +lipid metabolism PAFAH1B1,PAFAH1B1,ATP8B1 +cell cycle arrest EIF4G2,EIF4G2,DST,CDKN1B +cell cycle CUL4B,CUL4B,RPS27A,SEPT7 +protein ubiquitination UBE4A,UBE4A,PCNP,RPS27A +protein targeting TOMM20,TOMM20,SRP9,COPB +nucleosome assembly NAP1L1,NAP1L1,SET,H3F3A +negative regulation of cell adhesion CD164,CD164,ARHGDIB,SPN +cell-cell signaling CCL5,CCL5,PGF,LTB,ITGB2 +positive regulation of transcription TGFB1I1,TGFB1I1,RPS27A +negative regulation of transcription IRF7,IRF7,SUMO1,BCLAF1 +hemopoiesis CD164,CD164,ZNF160,LCK +cellular defense response (sensu Vertebrata) IGHA1,IGHA1,IGHG3,IGHM +retrograde vesicle-mediated transport, Golg... TAPBP,TAPBP,COPE,COPB +protein import into nucleus MCM3AP,MCM3AP,NDUFA13 +positive regulation of transcription, DNA-d... TGFB1I1,TGFB1I1,TGFB1 +negative regulation of cell growth PPP2CA,PPP2CA,NDUFA13 +integrin-mediated signaling pathway ITGAV,ITGAV,DST,ITGB2 +cotranslational protein targeting to membrane SSR1,SSR1,TLOC1,TRAM1 +Rho protein signal transduction ARHGDIB,ARHGDIB,ROCK1 +sensory perception of sound TIMM8B,TIMM8B,TIMM13 +regulation of translation PPP2CA,PPP2CA,EEF1A1 +regulation of transcription POLR2L,POLR2L,PPP2CA +prostaglandin biosynthesis CD74,CD74,PTGES3,MIF +mitosis TARDBP,TARDBP,CORO1A +insulin receptor signaling pathway PHIP,PHIP,AKT1,MYH11 +DNA recombination ATRX,ATRX,HMGB1,RPA1 +tricarboxylic acid cycle ACO2,ACO2,SDHA,RPA1 +negative regulation of transcription, DNA-d... RPS14,RPS14,NDUFA13 +intra-Golgi vesicle-mediated transport NAPA,NAPA,COPE,COPB +cell death EIF4G2,EIF4G2,TGFB1 +protein amino acid autophosphorylation RPS2,RPS2,U16,RPL4 +circulation RPL31,RPL31,COL3A1 +calcium ion transport ATP2A3,ATP2A3,TPT1 +axon guidance OPHN1,OPHN1,RPS27A +ER-associated protein catabolism RPL27,RPL27,RPS27A +regulation of actin filament polymerization ARPC3,ARPC3,ARPC5 +positive regulation of I-kappaB kinase/NF-k... BIRC2,BIRC2,EEF1D +organ morphogenesis DGCR6,DGCR6,TGFB1 +negative regulation of transcription from R... IRF7,IRF7,ZMYND11 +mRNA splice site selection SRP46,SRP46,SFRS5 +erythrocyte differentiation RPS19,RPS19,HCLS1 +cytokinesis SEPT2,SEPT2,SEPT7 +chromosome segregation ARL8B,ARL8B,RIOK3 +cell-matrix adhesion ITGAV,ITGAV,ITGB2 +activation of NF-kappaB transcription factor U16,U16,NPM1,RPL4 +T cell activation CD3D,CD3D,CD2,CD7 +ubiquitin cycle TPT1,TPT1,UBE2D2 +regulation of cell adhesion NPTN,NPTN,PPP2CA +cell growth TGFB1,TGFB1,DDX5 +angiogenesis CAST,CAST,ATPIF1 +translational initiation EIF5,EIF5,RPS3A +transforming growth factor beta receptor si... FNTA,FNTA,TGFB1 +response to drug RAB6C,RAB6C,LCK +regulation of apoptosis TPT1,TPT1,PRDX2 +chromosome organization and biogenesis (sen... ATRX,ATRX,H3F3A +caspase activation STAT1,STAT1,LCK +antimicrobial humoral response (sensu Verte... IL7R,IL7R,ITGB2 +I-kappaB kinase/NF-kappaB cascade PHIP,PHIP,STAT1 +transmembrane receptor protein tyrosine kin... CD7,CD7,COL1A2 +response to stress NPM1,NPM1,CROP +response to metal ion MT1X,MT1X,MT1L +calcium ion homeostasis CCL5,CCL5,TPT1 +regulation of macrophage activation CD74,CD74,MIF +nucleocytoplasmic transport NPM1,NPM1,SET +negative regulation of apoptosis CD74,CD74,MIF +muscle contraction DES,DES,CALD1 +establishment and/or maintenance of cell po... CAP1,CAP1,SPN +chemotaxis CCL5,CCL5,SPN +Ras protein signal transduction G3BP,G3BP,LCK +regulation of heart contraction DES,DES,TPM1 +activation of MAPKK activity U16,U16,RPL4 +JNK cascade U16,U16,RPL4 +cytoskeleton organization and biogenesis DES,DES,DST diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_CC.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_CC.txt new file mode 100644 index 0000000..4b57291 --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_CC.txt @@ -0,0 +1,75 @@ +integral to membrane SPCS2,SPCS2,ITGAV,CAST,REEP5,HLA-DPB1,SSR1,C11orf2,TAPBP,IL2RG,CXCR4,HLA-J,RPLP2,CD37,SLC25A16,221651_x_at,RPN1,IGLV3-10,FNBP4,UCP2,AD7C-NTP,ADRM1,IGHD,HLA-B,HLA-DMB,TLOC1,ATP6V0B,HLA-DRB5,SLC25A11,CD74,HLA-DRB1,IGL@,TMED5,IGLC1,HLA-E,SLC25A3,IGLV3-25,TRB@,ARL6IP5,HLA-DRB4,TRBC1,LAPTM5,CD2,C6orf12,IFNGR1,HLA-DQB1,FXYD5,POPDC3,ATP6V0C,TRBV3-1,IGLC2,214836_x_at,IGHV1-69,IGHA1,U16,HLA-F,HLA-A,CD7,CAV1,IGHG3,215176_x_at,IGLJ3,HLA-G,IL7R,ITGB2,IL6ST,RPL4,HLA-C,IGKC,IGHM,TRAM1,RPA1 +membrane HLA-DPB1,HLA-DPB1,SLC38A2,RAP1B,VDP,IL2RG,CXCR4,RAB1A,NPTN,RAB6A,HLA-DMA,HLA-DQA1,ADD3,SLC25A16,PGF,IL10RA,RAB6C,221651_x_at,MSN,IGLV3-10,RPL31,UCP2,IGHD,218041_x_at,HLA-DRA,HLA-DMB,YME1L1,SELL,ATP6V0B,HLA-DRB5,SLC25A11,HLA-DRB1,IGL@,IGLC1,SLC25A3,IGLV3-25,TRB@,HLA-DRB4,TRBC1,LTB,COL6A3,IFNGR1,HLA-DQB1,HLA-DPA1,CD79A,FXYD5,ATP6V0C,PPP2CA,TRBV3-1,IGLC2,ATP2A3,214836_x_at,IGHV1-69,IGHA1,IGHG3,215176_x_at,IGLJ3,IL7R,COPE,HLA-C,IGKC,IGHM,ATP5G3,RPA1,RPL24 +nucleus TARDBP,TARDBP,CNOT2,NFE2L2,ZFR,STAT1,SEPT2,MCM3AP,TINP1,LYSMD4,KPNB1,SFRS10,CBX3,THRAP5,G3BP,HNRPH3,IRF7,THRAP1,ZMYND11,POLR2J,ZNF160,MEFV,CDC5L,TRIM22,DDX17,IFI16,HCLS1,ATRX,HNRPH1,POLR1B,OGT,DHX15,ZNF611,MORF4L1,HNRPR,HMGB1,PPP2CA,CDKN1B,PCM1,BCLAF1,TIMM13,SRP9,RPS6,DDX5,ZNF638,RPL35A,SFRS11,PCNP,DDX50,ZNF552,RPS3A,NPM1,MBNL1,PAPOLA,AURKAIP1,FIBP,SH3BGRL,MATR3,RPS27A,SCAND1,DDX3X,SET,HNRPA2B1,RNPC2,SEPT7,H3F3A +cytoplasm AKT1,AKT1,MT1E,FNTA,STAT1,MT1F,SEPT2,VDP,EIF4G1,KPNB1,DDX42,IRF7,MSN,ARL8B,CD3D,PRKAR2A,HCLS1,ARHGDIB,COL6A3,COL1A1,COL6A1,DST,217718_s_at,RPL36A,NARS,COL4A2,COL4A1,CDKN1B,NDUFA13,PCM1,ZNF638,NACA,EXOSC4,COL3A1,COL1A2,ARPC5,EIF5,NPM1,MBNL1,PAPOLA,SH3BGRL,TPT1,PRDX2,COPS2,COPE,XPO1,DDX3X,EEF1A1,HNRPA1 +ribosome RPL37,RPL37,RPS11,RPS21,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,LOC440055,LOC388344,RPL27A,RPL28,LOC283412,RPL19,RPS16,RPL36A,RPS6,RPS2,U16,RPL18A,RPL13,RPL29,RPS3A,RPS18,RPL14,RPL22,RPL34,RPS23,RPL15,RPL9,RPL6,RPL4,RPS4X,RPS24,RPL5,RPL23,MDS1,RPS7 +intracellular RPL37,RPL37,RAP1B,MGC2474,RPS21,RPL21,RPLP2,G3BP,GMFG,RPS19,RAB6C,PRKD2,RPL31,ZNF160,TGFB1I1,MEFV,GLTSCR2,MRPS12,CD74,RPL28,LOC283412,RPL19,ROCK1,SON,RPS6,RPS2,U16,RPL18A,RPL29,RPS18,RPL14,RPL22,RPL34,RPL15,RPL4,RPS4X,RPS24,RPL13A,RPL5,MDS1,RPS7 +integral to plasma membrane HNRPM,HNRPM,IL2RG,HLA-DQA1,CD48,CD37,CLPTM1,ADRM1,HLA-B,CD164,HLA-DRA,SELL,HLA-DRB5,SLC25A11,HLA-DRB1,SLC25A3,HLA-DRB4,LAPTM5,CD2,C6orf12,IFNGR1,HLA-DPA1,ATP2A3,IGHA1,HLA-A,CAV1,SPN,IGHG3,CD52,IL6ST,HLA-C,IGHM,ATP8B1 +cytosolic small ribosomal subunit (sensu Eu... RPS15,RPS15,RPS5,FAU,RPS11,RPS21,RPS13,RPS19,LOC440055,RPS27,RPS10,RPS17,RPS16,RPS14,RPS6,RPS2,RPS15A,RPS20,RPS3A,RPS18,RPS27A,RPS23,RPS24,RPS9,RPS7,LOC402057,RPS25 +cytosolic large ribosomal subunit (sensu Eu... RPL35,RPL35,RPL18,RPL21,RPL36,RPLP2,RPL27A,RPL28,LOC283412,RPL7,U16,RPL18A,RPL29,RPL14,RPL22,RPL34,RPL6,RPL4,RPL5,MDS1 +plasma membrane ARF1,ARF1,CD48,PTPRCAP,CD37,IL10RA,MSN,CD164,HLA-DRA,SELL,PRKAR2A,CD53,TRB@,TRBV3-1,TRA@,LCK,CD7,IL6ST +mitochondrion ACO2,ACO2,ATPIF1,MSRB2,SDHA,UCP2,TUFM,LOC440055,GPX4,SLC25A3,PPP2CA,NDUFA13,FIBP,DBT,NDUFB4,CYC1,RPA1 +cytosol CAST,CAST,NCF1,NPEPPS,KNS2,PSMB9,PSMB8,HSP90AA1,OGT,PPP2CA,HSP90B1,EIF5,RPS3A,EIF3S3,SPG21,COPB +membrane fraction RGS19,RGS19,ADRM1,HLA-B,PRKAR2A,LMOD1,COL6A3,IGHV1-69,IGHA1,U16,CD7,IGHG3,CD52,FIBP,RPL4,IGHM +endoplasmic reticulum CAST,CAST,P4HB,SSR1,TAPBP,RPN1,PPIB,TLOC1,NAPA,HSP90B1,CAV1,KTN1,SET,TRAM1 +cytoskeleton CALD1,CALD1,RAVER2,ACTA2,TPM1,MSN,NPEPPS,ARHGDIB,LMOD1,ACTG2,ARPC3,ARPC5 +ribonucleoprotein complex HNRPM,HNRPM,SRP72,HNRPH3,HNRPU,HNRPK,HNRPR,HNRPA3P1,SIAHBP1,HNRPA1 +extracellular region CAST,CAST,P4HB,CNOT2,MASP1,IGL@,FBLN1,IFI30,COL4A1,MIF,IGHG3,IGKC +MHC class I protein complex HLA-J,HLA-J,RPLP2,HLA-B,HLA-E,C6orf12,HLA-F,HLA-A,HLA-G,HLA-C +heterogeneous nuclear ribonucleoprotein com... HNRPH3,HNRPH3,HNRPU,HNRPH1,HNRPR,HNRPDL,HNRPA2B1,HNRPA1 +mitochondrial inner membrane NDUFA7,NDUFA7,SLC25A16,UCP2,SLC25A11,SLC25A3,NDUFV1 +extracellular space CNOT2,CNOT2,IL16,AD7C-NTP,FBLN1,TGFB1,MMP2,SPN,TPT1 +nucleoplasm IFI16,IFI16,NDUFA13,ZNF638,XPO1,ISG20,RNPC2,HNRPA1 +collagen COL1A1,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2 +nucleolus RPL35,RPL35,RPS19,IFI16,NCL,EXOSC4,NPM1,RPS7 +spliceosome complex HNRPM,HNRPM,HIATL1,SF3B1,HNRPA2B1,HNRPA1 +proton-transporting two-sector ATPase complex ATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3 +extracellular matrix (sensu Metazoa) LAMC1,LAMC1,FBLN1,DGCR6,COL6A3,TGFB1 +perinuclear region 217718_s_at,217718_s_at,HSP90B1,SET +DNA-directed RNA polymerase II, core complex POLR2J,POLR2J,HCLS1,HMHA1,POLR2E +actin cytoskeleton MARCKS,MARCKS,FNBP4,CORO1A,SMTN +cAMP-dependent protein kinase complex PRKAR1A,PRKAR1A,PRKAR2A,PRKACB +Arp2/3 protein complex ACTR3,ACTR3,ARPC1B,ARPC3,ARPC5 +lysosome HLA-DRA,HLA-DRA,LAPTM5,IFI30 +large ribosomal subunit RPL7,RPL7,RPL26,RPL13A,RPL17 +Golgi apparatus RGS19,RGS19,RAB6A,RAB6C,NAPA +soluble fraction NCF1,NCF1,FBLN1,NARS,PPP2CA +eukaryotic translation initiation factor 4F... EIF4G1,EIF4G1,EIF4A2,EIF4G2 +proton-transporting ATP synthase complex (s... ATP5J2,ATP5J2,ATP5D,ATP5G3 +microsome SPCS2,SPCS2,TAPBP,HSP90B1 +cytosolic ribosome (sensu Eukaryota) LOC388344,LOC388344,RPL13 +Golgi membrane TAPBP,TAPBP,VDP,CAV1,COPB +transcription factor complex LOC342346,LOC342346,SUB1 +endoplasmic reticulum membrane TAPBP,TAPBP,HSP90B1,KTN1 +actin filament ACTA2,ACTA2,ACTG2,IQGAP1 +proteasome complex (sensu Eukaryota) PSMB3,PSMB3,PSMB9,PSMB8 +nuclear envelope PAFAH1B1,PAFAH1B1,XPO1 +endoplasmic reticulum lumen CAST,CAST,PPIB,HSP90B1 +stress fiber PLEKHC1,PLEKHC1,SEPT7 +small ribosomal subunit RPS5,RPS5,MRPS12,RPS2 +mitochondrial electron transport chain UQCRC1,UQCRC1,NDUFA13 +mitochondrial ribosome MRPS12,MRPS12,MRPL34 +mitochondrial inner membrane presequence tr... TIMM8B,TIMM8B,TIMM13 +mediator complex THRAP5,THRAP5,THRAP1 +collagen type VI COL6A3,COL6A3,COL6A1 +ER-Golgi intermediate compartment P4HB,P4HB,TMED5,KTN1 +respiratory chain complex III (sensu Eukary... UQCRQ,UQCRQ,UQCRFS1 +eukaryotic translation elongation factor 1 ... EEF1B2,EEF1B2,EEF1D +cytoplasmic membrane-bound vesicle ARHGDIB,ARHGDIB,DST +microtubule associated complex MEFV,MEFV,PAFAH1B1 +ubiquitin ligase complex RNF11,RNF11,UBE4A +protein complex PSMB9,PSMB9,PSMB8 +proteasome core complex (sensu Eukaryota) PSMB9,PSMB9,PSMB8 +nuclear pore KPNB1,KPNB1,RGPD5 +integrin complex ITGAV,ITGAV,ITGB2 +signal recognition particle (sensu Eukaryota) SRP72,SRP72,SRP9 +cell surface P4HB,P4HB,ATPIF1 +myosin MYH9,MYH9,MYH11 +mitochondrial envelope UCRC,UCRC,COX5B +centrosome NPM1,NPM1,CEP27 +basement membrane LAMC1,LAMC1,DST +mitochondrial matrix MDH2,MDH2,ETFB +COPI vesicle coat COPE,COPE,COPB +pericentriolar material PCM1,PCM1,LCK +intermediate filament DES,DES,RPL38 +lipid raft LCK,LCK,CAV1 diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_MF.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_MF.txt new file mode 100644 index 0000000..9c3e94a --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_GO_MF.txt @@ -0,0 +1,114 @@ +protein binding TARDBP,TARDBP,ITGAV,CAST,RPS15,PHIP,NFE2L2,OAZ1,STAT1,SEPT2,TXNL2,ORC6L,IL2RG,RGS19,EIF4G1,RPS13,C1orf160,RAB6A,HLA-DMA,ZNHIT1,GPAA1,CUL4B,HLA-J,KPNB1,ARF1,IL16,CD48,CBX3,G3BP,SUMO2,ACTA2,TCEB2,EIF3S6,MT1H,RPS19,INPP5D,RAB6C,RNF11,PRKAR1A,NCF1,ZMYND11,HNRPK,SUMO1,TGFB1I1,CDC5L,KNS2,RBPMS,CAB39,SFPQ,CD3D,NAPA,ACTR3,CLTC,TTC3,EIF4A2,IFI16,ARL6IP5,EEF1B2,HCLS1,PSMB8,HMHA1,HSP90AA1,HNRPH1,SRP46,EIF4G2,FIS1,PLEKHC1,CD2,OGT,TAF7,217718_s_at,NARS,C6orf12,TGFB1,CDKN1B,NDUFA13,GPSM3,BCLAF1,HSP90B1,SERBP1,GTF2I,RPS6,U16,BIRC2,RPL35A,HLA-A,RPS15A,EXOSC4,RPS20,RPL38,ACTG2,CAV1,ACTR2,SKP1A,ACTR10,MIF,PCNP,RPS3A,RPL14,HLA-G,IL7R,AURKAIP1,FIBP,EIF3S3,HNRPDL,RPS27A,RAC2,ITGB2,COPS2,XPO1,SIAHBP1,POLR2E,NAP1L1,RPL4,SFRS5,HNRPA2B1,EEF1A1,UBE2D2,SEPT7,COPB,ATP5G3,RPL5,RPS7,RPA1,HNRPA1,RPL24 +RNA binding TARDBP,TARDBP,RPS5,HNRPM,LOC56902,EIF4G1,RPL18,RPL21,SFRS10,RPLP2,G3BP,HNRPH3,HNRPU,DDX42,RPS19,HNRPK,RBPMS,LOC440055,RPS27,LOC388344,SFPQ,DDX17,RPL27A,RPS10,RPS17,EIF4A2,RPL28,LOC283412,RPS14,HNRPH1,SRP46,EIF4G2,HNRPR,NCL,RPL7,HSP90B1,SRP9,HNRPA3P1,RPS2,U16,DDX5,ZNF638,RPL18A,RPL13,RPL29,SFRS11,DDX50,NPM1,RPS18,RPL14,PAPOLA,RPL22,MATR3,HNRPDL,RPL34,SCAND1,RPL9,RPL6,SIAHBP1,RPL4,RPS4X,SFRS5,SFRS3,HNRPA2B1,RPS9,RNPC2,RPL5,MDS1,LOC402057,HNRPA1 +structural constituent of ribosome RPS15,RPS15,RPL37,RPS5,RPL35,FAU,RPS11,RPS21,RPL18,RPS13,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL31,MRPS12,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL19,RPS16,RPS14,RPL7,RPS6,RPS2,U16,RPL18A,RPL13,RPL29,RPS15A,RPS20,RPS3A,RPS18,RPL14,MRPL34,RPL22,RPL34,RPS27A,RPS23,RPL15,RPL9,RPL6,RPL26,RPL4,RPS4X,RPS24,RPS9,RPL13A,RPL17,RPL5,RPL23,MDS1,RPS7,LOC402057,RPS25 +nucleotide binding TARDBP,TARDBP,HNRPM,RAP1B,MCM3AP,RAB1A,RAB6A,RAVER2,SFRS10,G3BP,HNRPH3,ACTA2,DDX42,RAB6C,PRKD2,RBPMS,YME1L1,SFPQ,DDX17,EIF4A2,HSP90AA1,HNRPH1,SRP46,NARS,HNRPR,NCL,PCM1,HSP90B1,HNRPA3P1,U16,DDX5,ACTG2,SFRS11,DDX50,CSNK1A1,EIF3S3,MATR3,HNRPDL,DDX3X,SIAHBP1,RPL4,SFRS5,HNRPA2B1,EEF1A1,RNPC2,SEPT7,PRKACB,HNRPA1 +nucleic acid binding TARDBP,TARDBP,ZFR,HNRPM,LOC56902,MGC2474,RPS11,SFRS10,G3BP,HNRPH3,ZNF9,DDX42,HNRPK,ZNF160,MRPS12,SFPQ,DDX17,R3HCC1,EIF4A2,IFI16,HNRPH1,SRP46,NARS,ZNF611,SON,HNRPR,DDX5,SFRS11,DDX50,ZNF552,NPM1,MBNL1,MATR3,HNRPDL,DDX3X,SIAHBP1,SFRS5 +ATP binding NDUFA10,NDUFA10,AKT1,RAVER2,ACTA2,DDX42,PRKD2,MYH9,220725_x_at,YME1L1,DDX17,EIF4A2,HSP90AA1,208246_x_at,DHX15,NARS,ROCK1,NDUFA13,PCM1,HSP90B1,ATP2A3,RPS2,U16,DDX5,LCK,MYH11,ACTG2,DDX50,CSNK1A1,EIF3S3,DDX3X,RPL4,PRKACB +receptor activity ITGAV,ITGAV,HNRPM,IL2RG,CXCR4,THRAP5,PTPRCAP,RPL27,IL10RA,THRAP1,TLOC1,HLA-DRB5,HLA-DRB1,HLA-DRB4,CD2,IFNGR1,TRBV3-1,IGHA1,U16,CD7,IGHG3,IL7R,IL6ST,RPL4,EEF1A1,IGHM,TRAM1 +MHC class I receptor activity HLA-J,HLA-J,RPLP2,221651_x_at,IGLV3-10,IGHD,HLA-B,IGL@,IGLC1,HLA-E,IGLV3-25,C6orf12,IGLC2,214836_x_at,IGHV1-69,IGHA1,HLA-F,HLA-A,IGHG3,IGLJ3,HLA-G,HLA-C,IGKC,IGHM +zinc ion binding CAST,CAST,ZFR,MT1E,MT1F,KPNB1,MSRB2,TIMM8B,ZNF9,DDX42,RNF11,PRKD2,SDHA,ZMYND11,ZNF160,MEFV,RPS27,TTC3,ZNF611,TIMM13,POLR2I,MMP2,ZNF552,MBNL1,MATR3 +binding SRP72,SRP72,VDP,CAPZA1,TTC19,KPNB1,SLC25A16,MSN,UCP2,KNS2,CAB39,RGPD5,NAPA,SLC25A11,TTC3,SLC25A3,ALOX5AP,HIATL1,OGT,CHCHD2,SF3B1,COPS2,COPE,COPB +DNA binding MCM3AP,MCM3AP,HNRPU,RNF11,IRF7,SDHA,POLR2L,POLR2J,CDC5L,SFPQ,POLR1B,SON,HMGB1,PCM1,BCLAF1,CSDE1,RPL6,LOC342346,SUB1,LOC58486,H3F3A +NADH dehydrogenase (ubiquinone) activity NDUFC1,NDUFC1,NDUFA10,NDUFA7,NDUFB6,NDUFB2,NDUFA2,NDUFV1,NDUFA3,NDUFA13,NDUFS7,NDUFS3,NDUFS8,NDUFB4,NDUFAB1 +MHC class II receptor activity HLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-DRB4,HLA-DQB1,HLA-DPA1,HLA-C +electron carrier activity P4HB,P4HB,MT1X,MT1L,NCF1,UQCR,CYBA,FLJ14346,NDUFS3,NDUFS8,COX5A,ETFB,KTN1,AKR7A2,CYC1 +GTPase activity SEPT2,SEPT2,RAB6A,ARF1,RAB6C,NCF1,ARL6IP2,ARF5,ARL8B,RPL29,EIF5,RAC2,EEF1A1 +antigen binding IGL@,IGL@,IGLC1,IGLV3-25,IGLC2,IGHV1-69,IGHA1,IGHG3,IGLJ3,IL7R,IGKC,IGHM +transferase activity FNTA,FNTA,PRKD2,POLR2J,POLR1B,COL6A3,NARS,RPS2,U16,CSNK1A1,RPL4,PRKACB +transcription factor activity TARDBP,TARDBP,NFE2L2,STAT1,LYSMD4,MSRB2,TRIM22,HCLS1,GTF2I,SCAND1,MDS1 +metal ion binding MT1X,MT1X,ZFR,MT1M,MT1L,PRKD2,ZNF160,TTC3,ZNF611,ZNF552,MATR3,NDUFS8 +structural molecule activity DES,DES,FNBP4,CORO1A,NPEPPS,CLTC,COL6A3,RPL38,CAV1,MATR3,SEPT7 +GTP binding RAP1B,RAP1B,RAB1A,RAB6A,RAB6C,NCF1,ARL6IP2,ARL8B,EEF1A1,SEPT7 +extracellular matrix structural constituent LAMC1,LAMC1,FBLN1,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2 +structural constituent of cytoskeleton DES,DES,ADD3,ACTA2,TPM1,MSN,ARPC1B,DST,ACTG2,ARPC3,ARPC5 +hydrolase activity DDX42,DDX42,PPP1CA,DDX17,EIF4A2,PPP2CA,DDX5,DDX50,DDX3X +unfolded protein binding TAPBP,TAPBP,PPIB,HSP90AA1,PTGES3,HSP90B1,NPM1,EIF3S3 +transporter activity G3BP,G3BP,UCP2,ATP6V0B,SLC25A11,SLC25A3,ATP5D,ATP5G3 +kinase activity GUK1,GUK1,MYLK,PRKAR2A,WSB1,208246_x_at,ROCK1,CDKN1B +iron ion binding ACO2,ACO2,MT1X,MT1L,PPP1CA,NDUFV1,PPP2CA,NDUFS8,CYC1 +DNA-directed RNA polymerase activity POLR2L,POLR2L,POLR2J,HMHA1,POLR1B,PCM1,POLR2I,POLR2E +ATP-dependent helicase activity DDX42,DDX42,DDX17,EIF4A2,DHX15,DDX5,DDX50,DDX3X,DDX1 +protein serine/threonine kinase activity AKT1,AKT1,PRKD2,ROCK1,RPS2,U16,CSNK1A1,RPL4,PRKACB +calcium ion binding MASP1,MASP1,SPARCL1,STAT1,FBLN1,HSP90B1,IGHG3,TPT1 +actin binding CNN1,CNN1,CALD1,CAPZA1,GMFG,SMTN,MEFV,NPEPPS,FXYD5 +protein homodimerization activity ATPIF1,ATPIF1,HSP90AA1,U16,NPM1,EIF3S3,RPL4 +signal transducer activity STAT1,STAT1,RGS19,BIRC2,HNRPDL,EEF1D,COPS2 +oxidoreductase activity NDUFA10,NDUFA10,GPX1,MDH2,GPX4,IGLC2,PRDX2 +transcription coactivator activity TGFB1I1,TGFB1I1,TAF7,NPM1,LOC342346,SUB1 +translation initiation factor activity EIF4G1,EIF4G1,EIF4A2,EIF4G2,EIF5,EIF3S3 +protein-tyrosine kinase activity RAVER2,RAVER2,PCM1,U16,LCK,HNRPDL,RPL4 +catalytic activity KIAA1840,KIAA1840,LOC91316,DIP2A,PDE4C +hydrogen-transporting ATPase activity, rota... ATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3 +hydrogen-transporting ATP synthase activity... ATP6V0B,ATP6V0B,ATP6V0C,ATP5D,ATP5G3 +hematopoietin/interferon-class (D200-domain... STAT1,STAT1,IL2RG,IL10RA,IFNGR1,IL7R +oxidoreductase activity, acting on NADH or ... NDUFV1,NDUFV1,NDUFS7,NDUFS3,NDUFS8 +single-stranded DNA binding HNRPDL,HNRPDL,LOC342346,SUB1,RPA1 +amino acid-polyamine transporter activity SLC38A2,SLC38A2,218041_x_at,RPL24 +receptor binding CCL5,CCL5,ARPP-19,MSN,LTB,GNB2L1 +calmodulin binding CALD1,CALD1,MARCKS,ATPIF1,IQGAP1 +cytochrome-c oxidase activity COX4I1,COX4I1,COX5B,COX5A,COX8A +ubiquinol-cytochrome-c reductase activity UCRC,UCRC,UQCRQ,UQCRC1,UQCRFS1 +protein transporter activity VDP,VDP,KPNB1,TLOC1,AP2S1,COPE +translation elongation factor activity TUFM,TUFM,EEF1B2,EEF1D,EEF1A1 +peptide antigen binding TAPBP,TAPBP,TRB@,TRBV3-1,TRA@ +microtubule motor activity 220725_x_at,220725_x_at,KNS2 +phosphotransferase activity, alcohol group ... NDUFA10,NDUFA10,208246_x_at +oxygen transporter activity SLC38A2,SLC38A2,218041_x_at +RNA helicase activity DDX17,DDX17,DHX15,DDX5,DDX1 +growth factor activity CECR1,CECR1,GMFG,PGF,TGFB1 +RNA splicing factor activity, transesterifi... SFRS10,SFRS10,HIATL1,SF3B1 +heparin binding LOC283412,LOC283412,RPL29 +double-stranded DNA binding IFI16,IFI16,ZNF638,HNRPDL +RNA polymerase II transcription mediator ac... CNOT2,CNOT2,THRAP5,THRAP1 +nitric-oxide synthase regulator activity HSP90AA1,HSP90AA1,EIF3S3 +isomerase activity P4HB,P4HB,ECH1,PPIB,KTN1 +TPR domain binding HSP90AA1,HSP90AA1,EIF3S3 +ubiquitin-protein ligase activity UBE4A,UBE4A,TPT1,UBE2D2 +protein domain specific binding YWHAZ,YWHAZ,217718_s_at +cAMP-dependent protein kinase regulator act... PRKAR1A,PRKAR1A,PRKAR2A +transcriptional repressor activity IRF7,IRF7,IFI16,BCLAF1 +protein kinase binding U16,U16,LCK,ITGB2,RPL4 +general RNA polymerase II transcription fac... TCEA1,TCEA1,TAF7,GTF2I +MHC protein binding TRB@,TRB@,TRBV3-1,TRA@ +ATP-dependent RNA helicase activity G3BP,G3BP,RPL35A,DDX3X +protein heterodimerization activity CD3D,CD3D,PPP2CA,NPM1 +peptidase activity MASP1,MASP1,ZNF9,CLPP +identical protein binding AKT1,AKT1,CD74,SCAND1 +vitamin D receptor binding THRAP5,THRAP5,THRAP1 +thyroid hormone receptor binding THRAP5,THRAP5,THRAP1 +serine-type endopeptidase inhibitor activity NFE2L2,NFE2L2,COL6A3 +sequence-specific DNA binding NFE2L2,NFE2L2,LYSMD4 +protein dimerization activity NFE2L2,NFE2L2,POLR2J +motor activity MYH9,MYH9,KNS2,MYH11 +copper ion binding MT1E,MT1E,MT1F,DDX42 +cadmium ion binding MT1E,MT1E,MT1F,DDX42 +transmembrane receptor activity CD3D,CD3D,CD79A,SPN +translation factor activity, nucleic acid b... EIF3S3,EIF3S3,EEF1D +transcriptional activator activity THRAP5,THRAP5,TGFB1 +3'-5'-exoribonuclease activity EXOSC4,EXOSC4,ISG20 +tropomyosin binding CALD1,CALD1,LMOD1 +transcription factor binding YWHAZ,YWHAZ,HMGB1 +threonine endopeptidase activity PSMB9,PSMB9,PSMB8 +serine-type endopeptidase activity MASP1,MASP1,IGHG3 +peroxidase activity CSDE1,CSDE1,PRDX2 +enzyme activator activity GMFG,GMFG,ALOX5AP +antioxidant activity RPL27,RPL27,PRDX2 +actin filament binding MARCKS,MARCKS,DST +acetylglucosaminyltransferase activity COL6A3,COL6A3,OGT +ATPase binding ATPIF1,ATPIF1,LCK +transcription regulator activity RPL7,RPL7,RPS27A +sugar binding MASP1,MASP1,SELL +ligase activity NARS,NARS,UBE2D2 +cytokine binding CD74,CD74,IFNGR1 +aminopeptidase activity CAST,CAST,NPEPPS +protein phosphatase inhibitor activity PSMB9,PSMB9,SET +succinate dehydrogenase activity SDHA,SDHA,RPA1 +structural constituent of muscle TPM1,TPM1,SMTN +protein disulfide oxidoreductase activity P4HB,P4HB,KTN1 +protein disulfide isomerase activity P4HB,P4HB,KTN1 +ornithine decarboxylase inhibitor activity OAZ2,OAZ2,OAZ1 +glutathione peroxidase activity GPX1,GPX1,GPX4 +double-stranded RNA binding SON,SON,MBNL1 +CD4 receptor binding LCK,LCK,SPG21 +MAP kinase kinase kinase activity U16,U16,RPL4 +protein C-terminus binding DST,DST,LCK diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_KEGG.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_KEGG.txt new file mode 100644 index 0000000..1d2de7e --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Annotation_KEGG.txt @@ -0,0 +1,56 @@ +Ribosome RPS5,RPS5,RPL35,FAU,RPL18,RPL21,RPL36,RPL12,RPLP2,RPS19,RPL27,RPL10A,LOC440055,RPS27,LOC388344,RPL27A,RPS10,RPS17,RPL28,LOC283412,RPL23A,RPL36A,RPL7,RPS2,U16,RPL35A,RPL18A,RPL13,RPL29,RPS15A,RPS3A,RPL14,RPL22,RPL34,RPS27A,RPL9,RPL6,RPL4,RPS4X,RPS9,RPL13A,RPL17,RPL5,MDS1,LOC402057 +Oxidative phosphorylation NDUFC1,NDUFC1,ATP5J2,NDUFA10,NDUFA7,COX4I1,ATP6V0D1,NDUFB6,UCRC,SDHA,ATP6V0B,UQCRC1,NDUFB2,UQCRFS1,NDUFA2,NDUFV1,ATP6V0C,NDUFA3,NDUFA13,NDUFS7,COX5B,ATP5D,NDUFS3,NDUFS8,COX5A,NDUFB4,NDUFAB1,ATP5G3,CYC1,COX8A,RPA1 +Antigen processing and presentation HLA-DPB1,HLA-DPB1,TAPBP,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,HLA-DRB5,CD74,HLA-DRB1,HLA-E,HLA-DRB4,HSP90AA1,C6orf12,HLA-DQB1,HLA-DPA1,IFI30,HLA-A,HLA-G,EIF3S3,HLA-C +Cell adhesion molecules (CAMs) ITGAV,ITGAV,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,SELL,HLA-DRB5,HLA-DRB1,HLA-E,HLA-DRB4,CD2,C6orf12,HLA-DQB1,HLA-DPA1,HLA-A,SPN,HLA-G,ITGB2,HLA-C +Type I diabetes mellitus HLA-DPB1,HLA-DPB1,HLA-DMA,HLA-DQA1,HLA-J,HLA-B,HLA-DRA,HLA-DMB,HLA-DRB5,HLA-DRB1,HLA-E,HLA-DRB4,C6orf12,HLA-DQB1,HLA-DPA1,HLA-A,HLA-G,HLA-C +Focal adhesion ITGAV,ITGAV,AKT1,RAP1B,LAMC1,PGF,MYLK,COL6A3,COL1A1,COL6A1,ROCK1,COL4A2,COL4A1,BIRC2,COL3A1,COL1A2,CAV1,RAC2 +Natural killer cell mediated cytotoxicity HLA-J,HLA-J,CD48,HLA-B,HLA-E,C6orf12,IFNGR1,HLA-A,LCK,HLA-G,RAC2,ITGB2,HLA-C +Regulation of actin cytoskeleton ITGAV,ITGAV,MSN,MYH9,MYLK,ARPC1B,ROCK1,ARPC3,ARPC5,IQGAP1,RAC2,ITGB2 +Cell Communication DES,DES,LAMC1,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2,RPL38 +Pyrimidine metabolism POLR2L,POLR2L,POLR2J,HMHA1,POLR1B,208246_x_at,POLR2I,RPS15A,POLR2E +ECM-receptor interaction ITGAV,ITGAV,LAMC1,COL6A3,COL1A1,COL6A1,COL4A2,COL4A1,COL3A1,COL1A2 +Purine metabolism GUK1,GUK1,POLR2L,POLR2J,HMHA1,POLR1B,POLR2I,RPS15A,POLR2E,PDE4C +Hematopoietic cell lineage CD37,CD37,HLA-DRA,CD3D,HLA-DRB5,HLA-DRB1,HLA-DRB4,CD2,CD7,IL7R +Cytokine-cytokine receptor interaction IL2RG,IL2RG,CXCR4,CCL5,IL10RA,LTB,IFNGR1,TGFB1,IL7R,IL6ST +Leukocyte transendothelial migration RAP1B,RAP1B,CXCR4,NCF1,MSN,CYBA,ROCK1,MMP2,RAC2,ITGB2 +Jak-STAT signaling pathway AKT1,AKT1,STAT1,IL2RG,RAVER2,IL10RA,IFNGR1,IL7R,IL6ST +Cell cycle ORC6L,ORC6L,YWHAZ,217718_s_at,TGFB1,CDKN1B,SKP1A +RNA polymerase POLR2L,POLR2L,POLR2J,HMHA1,POLR1B,POLR2I,POLR2E +Apoptosis AKT1,AKT1,PRKAR1A,ENDOG,PRKAR2A,BIRC2,PRKACB +Cholera - Infection SEC61A1,SEC61A1,ATP6V0D1,ATP6V0B,ATP6V0C +Pathogenic Escherichia coli infection - EPEC HCLS1,HCLS1,YWHAZ,ROCK1,NCL,RPL38,ARPC5 +Pathogenic Escherichia coli infection - EHEC HCLS1,HCLS1,YWHAZ,ROCK1,NCL,RPL38,ARPC5 +Epithelial cell signaling in Helicobacter pylori infection ATP6V0D1,ATP6V0D1,CCL5,ATP6V0B,ATP6V0C +Wnt signaling pathway ROCK1,ROCK1,SKP1A,CSNK1A1,RAC2,PRKACB +Pancreatic cancer AKT1,AKT1,STAT1,RAVER2,PGF,TGFB1,RAC2 +MAPK signaling pathway AKT1,AKT1,RAP1B,TGFB1,RAC2,PRKACB +Insulin signaling pathway AKT1,AKT1,PRKAR1A,PRKAR2A,PRKACB +Protein export SEC61A1,SEC61A1,SRP72,SRP9 +Ubiquitin mediated proteolysis TCEB2,TCEB2,SKP1A,UBE2D2 +Citrate cycle (TCA cycle) ACO2,ACO2,SDHA,MDH2,RPA1 +TGF-beta signaling pathway ROCK1,ROCK1,TGFB1,SKP1A +Calcium signaling pathway MYLK,MYLK,ATP2A3,PRKACB +Hedgehog signaling pathway CSNK1A1,CSNK1A1,PRKACB +Chronic myeloid leukemia AKT1,AKT1,TGFB1,CDKN1B +Axon guidance CXCR4,CXCR4,ROCK1,RAC2 +Toll-like receptor signaling pathway AKT1,AKT1,STAT1,CCL5 +Tight junction AKT1,AKT1,MYH9,HCLS1 +Glycan structures - degradation POPDC3,POPDC3,MAN2B1 +Colorectal cancer AKT1,AKT1,TGFB1,RAC2 +B cell receptor signaling pathway AKT1,AKT1,CD79A,RAC2 +Prion disease NFE2L2,NFE2L2,LAMC1 +T cell receptor signaling pathway AKT1,AKT1,CD3D,LCK +Long-term potentiation RAP1B,RAP1B,PRKACB +Adherens junction IQGAP1,IQGAP1,RAC2 +Glycan structures - biosynthesis 2 COL6A3,COL6A3,OGT +GnRH signaling pathway MMP2,MMP2,PRKACB +Adipocytokine signaling pathway AKT1,AKT1,RAVER2 +Basal transcription factors TAF7,TAF7,GTF2I +VEGF signaling pathway AKT1,AKT1,RAC2 +Reductive carboxylate cycle (CO2 fixation) ACO2,ACO2,MDH2 +Glyoxylate and dicarboxylate metabolism ACO2,ACO2,MDH2 +Glutathione metabolism GPX1,GPX1,GPX4 +Fc epsilon RI signaling pathway AKT1,AKT1,RAC2 +Arachidonic acid metabolism GPX1,GPX1,GPX4 +mTOR signaling pathway AKT1,AKT1,PGF +Tyrosine metabolism ECH1,ECH1,MIF diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Expression_MCODE.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Expression_MCODE.txt new file mode 100644 index 0000000..b663b07 --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_Expression_MCODE.txt @@ -0,0 +1,557 @@ +207730_x_at green +214836_x_at green +215176_x_at green +215182_x_at green +216342_x_at green +216524_x_at green +217281_x_at green +220725_x_at green +221651_x_at green +ACTA2 green +ACTR2 green +ACTR3 green +AD7C-NTP green +AP2S1 green +ARF5 green +ARHGDIB green +ARL6IP2 green +ARPC3 green +ATP8B1 green +ATRX green +AURKAIP1 green +BIRC2 green +C6orf12 green +CAV1 green +CD52 green +CD74 green +CDC5L green +CEP27 green +CHCHD2 green +COL1A1 green +COL1A2 green +COL3A1 green +COL6A1 green +COPE green +COX5A green +COX5B green +COX8A green +CROP green +CTA-246H3.1 green +CYBA green +DBT green +DDT green +DDX3X green +DDX5 green +EIF3S3 green +EIF3S6 green +EIF4G2 green +FAU green +FBLN1 green +FBXW12 green +FLJ11021 green +FLJ12151 green +FLJ20294 green +FLJ42393 green +GNB2L1 green +H3F3A green +HIATL1 green +HLA-A green +HLA-B green +HLA-C green +HLA-DMA green +HLA-DPA1 green +HLA-DQB1 green +HLA-DRB1 green +HLA-DRB4 green +HLA-DRB5 green +HLA-E green +HLA-F green +HLA-G green +HLA-J green +HNRPDL green +HNRPH3 green +IFI30 green +IGHA1 green +IGHG3 green +IGHM green +IGHV1-69 green +IGKC green +IGKV1-5 green +IGKV1D-13 green +IGKV1OR15-118 green +IGKV1OR2-108 green +IGL@ green +IGLC1 green +IGLC2 green +IGLJ3 green +IGLV3-25 green +IQGAP1 green +KNS2 green +LAPTM5 green +LCK green +LMOD1 green +LOC152719 green +LOC388344 green +LOC440055 green +LOC91316 green +MAN2B1 green +MATR3 green +MBNL1 green +MCM3AP green +MDS1 green +MGEA5 green +MORF4L1 green +MRPL34 green +MT1E green +MT1F green +MT1G green +MT1H green +MT1L green +MT1M green +MT1X green +MT2A green +MYH11 green +NACA green +NAP1L1 green +NARS green +NDUFA2 green +NDUFA3 green +NDUFAB1 green +NDUFB11 green +NDUFB2 green +NDUFB4 green +NDUFS3 green +NDUFS7 green +NPM1 green +PAPOLA green +PCNP green +PDE4C green +PFAAP5 green +PGF green +PNRC2 green +POLR2J green +PRG1 green +PRR11 green +PTGES3 green +RAC2 green +RIOK3 green +ROCK1 green +RPL11 green +RPL13 green +RPL13A green +RPL17 green +RPL18 green +RPL21 green +RPL22 green +RPL23 green +RPL24 green +RPL27 green +RPL27A green +RPL28 green +RPL29 green +RPL30 green +RPL34 green +RPL35A green +RPL36 green +RPL36A green +RPL38 green +RPL4 green +RPL5 green +RPL6 green +RPL7 green +RPL9 green +RPLP2 green +RPS14 green +RPS17 green +RPS18 green +RPS19 green +RPS2 green +RPS23 green +RPS24 green +RPS25 green +RPS27A green +RPS3A green +RPS4X green +RPS5 green +RPS6 green +RPS7 green +RPS9 green +SEPT2 green +SERBP1 green +SFRS11 green +SH3BGRL green +SKP1A green +SLC35E1 green +SON green +SUMO2 green +TAF7 green +TLOC1 green +TRA@ green +TRB@ green +TUG1 green +U16 green +UQCR green +UQCRC1 green +UQCRFS1 green +USP34 green +XPO1 green +YTHDC1 green +ZNF160 green +ZNF611 green +ZNF638 green +ZNHIT1 green +208246_x_at yellow +211637_x_at yellow +211645_x_at yellow +ATP5G3 yellow +ATP6V0B yellow +ATP6V0D1 yellow +BCLAF1 yellow +CAPZA1 yellow +COL6A3 yellow +CXCR4 yellow +HLA-DPB1 yellow +HNRPH1 yellow +IFI16 yellow +LOC342346 yellow +LOC56902 yellow +LOC58486 yellow +LOC645745 yellow +MEFV yellow +MRPS12 yellow +NDUFS8 yellow +PTRF yellow +RPL31 yellow +RPL35 yellow +RPS11 yellow +TAGLN yellow +TGFB1 yellow +TRBC1 yellow +208120_x_at red +208238_x_at red +211639_x_at red +211641_x_at red +211650_x_at red +211908_x_at red +212498_at red +216412_x_at red +216858_x_at red +217052_x_at red +217258_x_at red +217679_x_at red +217718_s_at red +218041_x_at red +AASDHPPT red +ACO2 red +ACTG2 red +ACTR10 red +ADD3 red +ADRM1 red +AKR7A2 red +AKT1 red +ALOX5AP red +ARF1 red +ARL6IP5 red +ARL8B red +ARPC1B red +ARPC5 red +ARPP-19 red +ATP2A3 red +ATP5D red +ATP5J2 red +ATP6AP2 red +ATP6V0C red +ATPIF1 red +BTF3 red +C11orf2 red +C11orf58 red +C12orf10 red +C12orf38 red +C16orf24 red +C17orf62 red +C1orf160 red +C1orf63 red +C21orf33 red +C3orf60 red +CAB39 red +CALD1 red +CAP1 red +CAPNS1 red +CAST red +CBX3 red +CCL5 red +CCNL1 red +CD164 red +CD2 red +CD37 red +CD3D red +CD48 red +CD53 red +CD7 red +CD79A red +CDKN1B red +CECR1 red +CHMP2A red +CLPP red +CLPTM1 red +CLTC red +CNN1 red +CNOT2 red +COL4A1 red +COL4A2 red +COPB red +COPS2 red +COQ9 red +CORO1A red +COX4I1 red +CSDE1 red +CSNK1A1 red +CUL4B red +CXorf9 red +CYC1 red +DDX1 red +DDX17 red +DDX42 red +DDX50 red +DES red +DEXI red +DGCR6 red +DHX15 red +DIP2A red +DKFZP566N034 red +DST red +DYNLT3 red +ECH1 red +EEF1A1 red +EEF1B2 red +EEF1D red +EIF4A2 red +EIF4G1 red +EIF5 red +ENDOG red +ERH red +ETFB red +EXOSC4 red +FAIM3 red +FAM96B red +FIBP red +FIS1 red +FLJ10154 red +FLJ14346 red +FNBP4 red +FNTA red +FXYD5 red +G3BP red +GBL red +GLTSCR2 red +GMFG red +GOLGA8A red +GOLGA8B red +GPAA1 red +GPSM3 red +GPX1 red +GPX4 red +GTF2I red +GUK1 red +HCLS1 red +HLA-DMB red +HLA-DQA1 red +HLA-DRA red +HMGB1 red +HMHA1 red +HNRPA1 red +HNRPA2B1 red +HNRPA3P1 red +HNRPK red +HNRPM red +HNRPR red +HNRPU red +HSP90AA1 red +HSP90B1 red +IFNGR1 red +IGHD red +IGLV3-10 red +IL10RA red +IL16 red +IL2RG red +IL6ST red +IL7R red +INPP5D red +IRF7 red +ISG20 red +ITGAV red +ITGB2 red +KIAA0907 red +KIAA1840 red +KIDINS220 red +KPNB1 red +KTN1 red +LAMC1 red +LOC283412 red +LOC402057 red +LRRFIP1 red +LTB red +LYSMD4 red +MARCKS red +MASP1 red +MDH2 red +MGC2474 red +MIF red +MMP2 red +MRP63 red +MSN red +MSRB2 red +MYH9 red +MYLK red +NAPA red +NBPF1 red +NBPF12 red +NCF1 red +NCL red +NDUFA10 red +NDUFA13 red +NDUFA7 red +NDUFB6 red +NDUFC1 red +NDUFV1 red +NFE2L2 red +NPEPPS red +NPTN red +OAZ1 red +OAZ2 red +OGT red +OPHN1 red +ORC6L red +OSBPL8 red +P4HB red +PAFAH1B1 red +PCM1 red +PDCD10 red +PGLS red +PHIP red +PLEKHC1 red +POLR1B red +POLR2E red +POLR2I red +POLR2L red +POPDC3 red +PPIB red +PPP1CA red +PPP2CA red +PRDX2 red +PRKACB red +PRKAR1A red +PRKAR2A red +PRKD2 red +PSMB3 red +PSMB8 red +PSMB9 red +PTPRCAP red +PUM2 red +R3HCC1 red +RAB1A red +RAB6A red +RAB6C red +RAP1B red +RAVER2 red +RBPMS red +REEP5 red +RGPD5 red +RGS19 red +RNF11 red +RNPC2 red +RPA1 red +RPL10A red +RPL12 red +RPL14 red +RPL15 red +RPL18A red +RPL19 red +RPL23A red +RPL26 red +RPL32 red +RPL37 red +RPL39 red +RPL41 red +RPN1 red +RPS10 red +RPS13 red +RPS15 red +RPS15A red +RPS16 red +RPS20 red +RPS21 red +RPS27 red +SCAND1 red +SDHA red +SEC61A1 red +SELL red +SEPT7 red +SET red +SF3B1 red +SFPQ red +SFRS10 red +SFRS3 red +SFRS5 red +SIAHBP1 red +SLC25A11 red +SLC25A16 red +SLC25A3 red +SLC38A2 red +SMTN red +SPARCL1 red +SPCS2 red +SPG21 red +SPN red +SRP46 red +SRP72 red +SRP9 red +SSR1 red +STAT1 red +SUB1 red +SUMO1 red +TAPBP red +TARDBP red +TCEA1 red +TCEB2 red +TGFB1I1 red +THRAP1 red +THRAP5 red +TIMM13 red +TIMM8B red +TINP1 red +TMED5 red +TMEM123 red +TMEM66 red +TMEM93 red +TOMM20 red +TPM1 red +TPT1 red +TRAM1 red +TRBV3-1 red +TRIM22 red +TTC19 red +TTC3 red +TUFM red +TXNL2 red +UBE2D2 red +UBE4A red +UCP2 red +UCRC red +UQCRQ red +USP47 red +VDP red +WAC red +WDR45 red +WIPI2 red +WSB1 red +YIPF3 red +YME1L1 red +YWHAZ red +ZC3H11A red +ZFR red +ZMYND11 red +ZNF403 red +ZNF552 red +ZNF9 red +hfl-B5 red diff --git a/www/Examples/Human_Coexpression/NORMA_Human_coexpression_NETWORK.txt b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_NETWORK.txt new file mode 100644 index 0000000..4aa6aaf --- /dev/null +++ b/www/Examples/Human_Coexpression/NORMA_Human_coexpression_NETWORK.txt @@ -0,0 +1,2144 @@ +Source Target +RPL24 RPL41 +RPL24 RPL26 +RPL24 RPL30 +RPL24 TPT1 +RPL24 RPL9 +RPL24 RPS25 +RPL24 RPS23 +RPL24 RPL31 +RPL24 RPS27A +RPL24 U16 +RPL24 RPS4X +RPL24 RPL7 +RPL24 RPL4 +RPL24 RPL5 +RPL24 RPS3A +RPL24 RPS7 +RPL24 RPL34 +RPL24 RPL35A +RPL24 RPS24 +RPL24 RPL6 +RPL41 RPL5 +RPL41 RPS3A +RPL41 TPT1 +RPL41 RPS23 +RPL41 RPS4X +HNRPA1 RPS25 +HNRPA1 RPS24 +HNRPA1 RPS4X +HNRPA1 RPS6 +HNRPA1 RPL22 +HNRPA1 NCL +HNRPA1 RPL24 +HNRPA1 NPM1 +HNRPA1 RPL4 +HNRPA1 MDS1 +HNRPA1 U16 +HNRPA1 RPS23 +HNRPA1 RPS7 +HNRPA1 SF3B1 +HNRPA1 NAP1L1 +HNRPA1 RPL17 +HNRPA1 RPL11 +HNRPA1 RPL6 +HNRPA1 RPL34 +HNRPA1 LOC440055 +HNRPA1 RPS3A +HNRPA1 RPL7 +RPS25 RPS4X +RPS25 RPS3A +RPS25 RPS6 +RPS25 U16 +RPS25 RPS7 +RPS25 RPS27A +H3F3A RPA1 +H3F3A RAP1B +H3F3A RPLP2 +H3F3A RPS20 +H3F3A RPL27 +H3F3A RPS11 +H3F3A RPL27A +COX8A CYC1 +COX8A NDUFS8 +COX8A SLC25A11 +COX8A SLC25A3 +COX8A NDUFAB1 +COX8A NDUFB11 +COX8A MRPS12 +CYC1 ETFB +CYC1 UQCRC1 +ATP8B1 PDE4C +ATP8B1 FLJ42393 +ATP8B1 DKFZP566N034 +ATP8B1 CEP27 +ATP8B1 KNS2 +ATP8B1 RIOK3 +ATP8B1 DBT +ATP8B1 PGF +ATP8B1 ZNF611 +ATP8B1 SLC35E1 +ATP8B1 LOC152719 +ATP8B1 FBXW12 +ATP8B1 ZNF160 +PDE4C SPG21 +PDE4C UBE2D2 +PDE4C PRKAR2A +PDE4C ZNF611 +PDE4C PRR11 +PDE4C PGF +PDE4C ZNF160 +PDE4C RIOK3 +PDE4C SLC35E1 +LOC58486 SUB1 +LOC58486 MGEA5 +LOC58486 RNPC2 +LOC58486 PFAAP5 +LOC58486 WAC +LOC58486 MATR3 +LOC58486 PNRC2 +LOC58486 PAPOLA +LOC58486 ZNF638 +LOC58486 SF3B1 +LOC58486 USP34 +LOC402057 RPS7 +LOC402057 RPS15A +LOC402057 RPL34 +LOC402057 RPS4X +LOC402057 RPS6 +LOC402057 RPL31 +LOC402057 RPL35A +LOC402057 RPL37 +LOC402057 RPL22 +LOC402057 RPL4 +LOC402057 RPL9 +LOC402057 RPS23 +LOC402057 RPL11 +LOC402057 LOC440055 +LOC402057 RPL30 +LOC402057 RPL23 +LOC402057 RPL36A +LOC402057 RPL39 +LOC402057 MDS1 +LOC402057 RPL32 +LOC402057 RPS17 +RPS7 SFRS5 +RPS7 SF3B1 +RPS7 U16 +MDS1 RPL23 +MDS1 RNPC2 +MDS1 RPL17 +MDS1 RPL5 +MDS1 RPS3A +MDS1 RPL41 +MDS1 RPL4 +MDS1 TPT1 +MDS1 NPM1 +MDS1 RPS6 +MDS1 RPL7 +MDS1 RPL24 +MDS1 RPL34 +MDS1 RPL26 +MDS1 RPL22 +MDS1 RPS27A +MDS1 RPL6 +MDS1 RPS25 +MDS1 RPS15A +MDS1 U16 +MDS1 RPS7 +MDS1 RPS24 +MDS1 RPL36A +MDS1 RPS4X +MDS1 RPL21 +MDS1 RPL31 +MDS1 RPL35A +MDS1 RPL30 +MDS1 RPS17 +MDS1 RPL9 +MDS1 RPL11 +MDS1 RPS23 +RPL23 RPL7 +RPL23 RPL26 +RPL23 RPL5 +RPL23 RPL24 +RPL23 RPS27 +RPL23 RPS15A +RPL23 RPS27A +RPL23 RPL6 +RPL23 RPL36A +RPL23 U16 +RPL23 RPS25 +RPL23 RPS17 +RPL23 RPS4X +RPL23 RPS7 +RPL23 RPS3A +RPL23 RPS23 +RPL23 RPL30 +RPL23 RPS24 +RPL23 RPL9 +RPL23 RPL31 +RPL23 RPS6 +RPL23 RPL35A +RPL23 RPL4 +RPL23 RPL34 +RPL5 RPS6 +RPL5 RPS23 +RPL5 TPT1 +RPL5 RPS25 +RPL5 RPS3A +RPL5 RPS4X +RPL5 RPL9 +RPL5 RPS27A +RPL5 RPL7 +RPL5 RPS7 +RPL5 U16 +RPL5 RPS24 +RPL5 RPL6 +ATP5G3 COX8A +ATP5G3 NDUFB11 +ATP5G3 SLC25A3 +ATP5G3 UQCRC1 +ATP5G3 UQCRQ +ATP5G3 COX5A +ATP5G3 COX4I1 +ATP5G3 NDUFAB1 +ATP5G3 UQCRFS1 +ATP5G3 COX5B +ATP5G3 MDH2 +COPB TRAM1 +COPB LOC58486 +COPB RNPC2 +COPB HSP90B1 +COPB YME1L1 +COPB PAPOLA +PRKACB SEPT7 +SEPT7 SH3BGRL +SEPT7 ZFR +UBE2D2 ZNF160 +RNPC2 TAF7 +RNPC2 TCEA1 +RNPC2 ZNF638 +RNPC2 RPS3A +RNPC2 SF3B1 +RNPC2 RPL5 +RNPC2 RPL6 +RNPC2 RPL17 +RNPC2 SRP72 +IGHM IGKC +IGHM IGLC2 +IGHM IGKV1-5 +IGHM LOC91316 +IGHM IGLV3-10 +IGHM IGL@ +IGHM IGHV1-69 +IGHM IGKV1D-13 +IGHM IGLV3-25 +IGHM IGLC1 +IGHM IGKV1OR2-108 +IGKC IGLJ3 +IGKC IGLC2 +IGKC IGKV1D-13 +IGKC IGLV3-25 +IGKC IGKV1OR2-108 +IGKC IGKV1-5 +IGKC IGLV3-10 +IGKC IGL@ +IGKC LOC91316 +IGKC IGLC1 +IGKC IGKV1OR15-118 +LOC342346 YTHDC1 +LOC342346 XPO1 +LOC342346 SON +LOC342346 TAF7 +LOC342346 PAPOLA +LOC342346 SH3BGRL +EEF1A1 RPL17 +EEF1A1 MDS1 +EEF1A1 RPS24 +EEF1A1 RPS3A +EEF1A1 RPL22 +EEF1A1 NPM1 +EEF1A1 RPL11 +EEF1A1 RPS7 +EEF1A1 RPS27A +EEF1A1 TPT1 +EEF1A1 RPS6 +EEF1A1 RPL7 +EEF1A1 RPS4X +EEF1A1 RPL23 +RPL17 SFRS5 +RPL17 RPS25 +RPL17 RPL9 +RPL17 TPT1 +RPL17 RPL30 +RPL17 RPL26 +RPL17 RPL7 +RPL17 RPL36A +RPL17 RPL22 +RPL17 RPS6 +RPL17 SF3B1 +RPL17 RPS4X +RPL17 TINP1 +RPL17 RPL35A +RPL17 RPS23 +RPL17 U16 +RPL17 RPS3A +RPL17 RPL24 +RPL17 RPS7 +RPL17 RPL23 +RPL17 RPL31 +RPL17 RPS27A +RPL17 RPL21 +RPL17 RPL4 +RPL17 RPL5 +RPL17 RPL34 +RPL17 RPL6 +RPL17 RPS24 +RPL13A RPS9 +RPL13A RPS18 +RPL13A RPL18A +RPL13A RPL27A +RPL13A RPL14 +RPL13A RPS16 +RPL13A RPS14 +RPL13A RPL15 +RPL13A RPLP2 +RPL13A RPS21 +RPL13A RPL29 +RPL13A RPS19 +RPL13A RPL36 +RPL13A RPS5 +NDUFAB1 NDUFB4 +NDUFAB1 NDUFS3 +NDUFAB1 UQCRC1 +NDUFAB1 NDUFV1 +NDUFAB1 UQCR +NDUFAB1 NDUFB2 +NDUFAB1 UQCRQ +NDUFAB1 NDUFS7 +NDUFAB1 NDUFB11 +NDUFAB1 NDUFC1 +NDUFAB1 POLR2I +NDUFAB1 NDUFS8 +NDUFAB1 UQCRFS1 +NDUFB4 NDUFS3 +NDUFB4 NDUFS8 +NDUFB4 UQCRFS1 +NDUFB4 UCRC +AKR7A2 CLPP +AKR7A2 TIMM13 +AKR7A2 NDUFV1 +AKR7A2 MRPL34 +AKR7A2 C21orf33 +AKR7A2 NDUFS8 +CLPP TUFM +CLPP MRPL34 +hfl-B5 HNRPA2B1 +hfl-B5 HIATL1 +hfl-B5 HNRPH1 +hfl-B5 HNRPA1 +HNRPA2B1 XPO1 +HNRPA2B1 SRP46 +SET SFRS3 +SET SRP46 +SET SON +SET SFRS10 +HLA-C ISG20 +HLA-C MAN2B1 +HLA-C HLA-G +HLA-C HLA-F +HLA-C HLA-E +HLA-C TAPBP +HLA-C HLA-J +ISG20 RAC2 +ISG20 PSMB9 +ISG20 UCP2 +RPS24 TPT1 +RPS24 TCEA1 +RPS24 SF3B1 +RPS24 RPS25 +RPS24 RPS3A +RPS24 RPS6 +RPS24 RPS4X +RPS24 U16 +RPS24 RPS27A +RPS24 RPS7 +DDX1 KTN1 +KTN1 USP47 +RPS4X U16 +RPS4X TPT1 +RPS4X RPS7 +RPS4X RPS6 +CCNL1 RPL4 +CCNL1 ZNF638 +CCNL1 NACA +CCNL1 RPL5 +CCNL1 RPS27A +CCNL1 RPL17 +CCNL1 RNPC2 +CCNL1 RPL21 +CCNL1 RPL6 +CCNL1 EIF3S6 +RPL4 TPT1 +RPL4 RPL7 +RPL4 SFRS5 +RPL4 RPS15A +RPL4 RPL5 +RPL4 RPS17 +RPL4 RPS25 +RPL4 RPL9 +RPL4 RPS7 +RPL4 RPS27A +RPL4 RPL6 +RPL4 RPS23 +RPL4 RPS4X +RPL4 RPS3A +RPL4 RPS24 +RPL4 U16 +RPL4 RPS6 +FLJ11021 NAP1L1 +FLJ11021 ZNF403 +FLJ11021 HIATL1 +FLJ11021 KIAA0907 +FLJ11021 WAC +FLJ11021 TUG1 +FLJ11021 PAPOLA +FLJ11021 RNPC2 +FLJ11021 XPO1 +FLJ11021 LOC58486 +FLJ11021 NPM1 +NAP1L1 RPL17 +NAP1L1 YTHDC1 +NAP1L1 PAPOLA +NAP1L1 ZNF9 +NAP1L1 XPO1 +C21orf33 ETFB +C21orf33 COX5B +C21orf33 NDUFS7 +C21orf33 NDUFS8 +ETFB NDUFS8 +ETFB UQCRQ +COX5A NDUFS8 +COX5A NDUFB11 +COX5A NDUFS7 +COX5A MDH2 +COX5A COX5B +COX5A NDUFAB1 +COX5A UQCRFS1 +COX5A UQCRC1 +NDUFS8 SLC25A3 +NDUFS8 POLR2I +NDUFS8 POLR2L +NDUFS8 TIMM13 +NDUFS8 UQCRFS1 +NDUFS8 TMEM93 +NDUFS8 UQCRQ +CROP IL6ST +CROP ZNF638 +CROP HNRPA3P1 +CROP RNPC2 +CROP MATR3 +CROP SFRS5 +CROP XPO1 +CROP SFPQ +CROP LOC58486 +CROP HNRPDL +CROP TUG1 +CROP SFRS11 +CROP ZC3H11A +CROP PFAAP5 +CROP NPM1 +CROP FLJ10154 +CROP GOLGA8A +IL6ST SFRS11 +POLR2E SIAHBP1 +RPL26 RPS3A +RPL26 RPL4 +RPL26 RPL7 +RPL26 RPS7 +RPL26 RPS6 +RPL26 RPS27A +RPL26 RPL34 +RPL26 RPS24 +RPL26 RPS25 +RPL26 RPL5 +RPL26 RPL6 +216858_x_at CEP27 +216858_x_at ZNF611 +216858_x_at FLJ14346 +216858_x_at LOC152719 +CEP27 PRR11 +CEP27 PGF +CEP27 PRKAR2A +CEP27 LOC152719 +CEP27 PDE4C +CEP27 RIOK3 +CEP27 ZNF611 +CEP27 KNS2 +CEP27 FBXW12 +CEP27 DBT +CEP27 ZNF160 +CEP27 SLC35E1 +CEP27 FLJ12151 +RPL6 RPL9 +RPL6 SFRS5 +RPL6 TPT1 +RPL6 RPL7 +RPL6 RPS23 +RPL6 SF3B1 +RPL6 RPS4X +RPL6 RPS25 +RPL6 RPS3A +RPL6 RPS6 +RPL6 U16 +RPL6 RPS7 +RPL6 RPS27A +RPL6 RPS24 +RPL9 RPS27A +RPL9 RPS24 +RPL9 U16 +RPL9 RPS25 +RPL9 RPS4X +RPL9 RPS23 +RPL9 RPS7 +RPL9 RPS3A +RPL9 RPS17 +RPL9 RPS6 +RPL9 RPS27 +RPL9 RPS15A +DDX3X XPO1 +DDX3X NAP1L1 +DDX3X PAPOLA +DDX3X SON +DDX3X HNRPR +DDX3X THRAP1 +DDX3X SH3BGRL +DDX3X HNRPH1 +DDX3X MGEA5 +DDX3X HIATL1 +DDX3X DDX5 +DDX3X LOC342346 +DDX3X EIF4G2 +DDX3X SF3B1 +DDX3X YTHDC1 +COPE SCAND1 +COPE MRPS12 +COPE NAPA +COPE FIBP +SCAND1 THRAP5 +COPS2 RPL15 +COPS2 HSP90B1 +COPS2 HNRPH3 +COPS2 ZNF638 +COPS2 KIDINS220 +COPS2 VDP +COPS2 TLOC1 +COPS2 CSDE1 +RPL15 RPL35 +RPL15 RPS16 +ITGB2 RAC2 +ITGB2 RGS19 +RAC2 TRA@ +RAC2 TRB@ +DBT PRR11 +DBT DIP2A +DBT FLJ42393 +DBT ZNF611 +DBT FLJ12151 +DBT PGF +DBT LOC152719 +DBT RIOK3 +DBT PDE4C +DBT WDR45 +DBT ZNF160 +DBT FBXW12 +DBT KNS2 +DBT SLC35E1 +PRR11 ZNF611 +PRR11 SLC35E1 +PRR11 ZNF160 +207730_x_at 216858_x_at +207730_x_at UBE2D2 +207730_x_at FLJ42393 +207730_x_at 208246_x_at +207730_x_at PRKAR2A +207730_x_at 217679_x_at +207730_x_at PDE4C +207730_x_at PRR11 +207730_x_at PGF +207730_x_at RIOK3 +207730_x_at 208238_x_at +207730_x_at KNS2 +207730_x_at 216524_x_at +207730_x_at ATP8B1 +207730_x_at FLJ12151 +207730_x_at LOC152719 +207730_x_at DBT +207730_x_at ZNF611 +207730_x_at SLC35E1 +207730_x_at ZNF160 +207730_x_at CEP27 +207730_x_at FBXW12 +EEF1D RPS9 +EEF1D GLTSCR2 +EEF1D RPL38 +PRDX2 RIOK3 +RIOK3 ZNF611 +RIOK3 ZNF160 +RIOK3 SLC35E1 +RPS23 RPS27A +RPS23 U16 +RPS23 RPS7 +RPS23 RPS24 +RPS23 RPS25 +RPS23 RPS6 +RPS23 RPS4X +RPS23 RPS3A +RPS27A SFRS5 +RPS27A RPS4X +RPS27A U16 +RPS27A TINP1 +RPS27A RPS3A +RPS27A RPS7 +RPS27A RPS6 +RPL34 TPT1 +RPL34 SF3B1 +RPL34 RPL7 +RPL34 RPS25 +RPL34 RPL36A +RPL34 RPL5 +RPL34 RPS27A +RPL34 RPS4X +RPL34 RPS3A +RPL34 RPS6 +RPL34 RPS7 +RPL34 RPL4 +RPL34 RPL9 +RPL34 RPS23 +RPL34 RPL6 +RPL34 RPL35A +RPL34 RPS24 +HNRPDL MATR3 +HNRPDL SON +HNRPDL NAP1L1 +HNRPDL NPM1 +HNRPDL SF3B1 +HNRPDL IL6ST +HNRPDL SERBP1 +HNRPDL SFRS11 +MATR3 WAC +MATR3 SERBP1 +MATR3 NARS +MATR3 XPO1 +MATR3 YTHDC1 +MATR3 SFRS11 +MATR3 SON +MATR3 MGEA5 +MATR3 PAPOLA +MATR3 SH3BGRL +MATR3 SEPT7 +MATR3 SEPT2 +MATR3 TAF7 +MATR3 NPM1 +MATR3 PCNP +MATR3 RNPC2 +MATR3 TLOC1 +EIF3S3 MATR3 +EIF3S3 HSP90AA1 +EIF3S3 TTC3 +EIF3S3 NARS +EIF3S3 FNTA +EIF3S3 SEPT2 +WAC ZNF403 +RPL22 RPL5 +RPL22 TPT1 +RPL22 RPS15A +RPL22 RPS23 +RPL22 RPS27A +RPL22 RPS7 +RPL22 RPL30 +RPL22 RPS3A +RPL22 RPL6 +RPL22 RPL34 +RPL22 RPL26 +RPL22 RPL9 +RPL22 RPL24 +RPL22 RPS25 +RPL22 RPL35A +RPL22 RPS17 +RPL22 RPS24 +RPL22 RPL4 +RPL22 RPL41 +RPL22 RPL7 +RPL22 RPS6 +RPL22 RPL36A +RPL22 RPL23 +RPL22 RPS4X +RPL22 RPL31 +SH3BGRL YTHDC1 +SH3BGRL TAF7 +SH3BGRL SON +MRPL34 NDUFS3 +MRPL34 UQCRFS1 +MRPL34 UQCRC1 +MRPL34 NDUFV1 +MRPL34 POLR2I +NDUFS3 POLR2I +NDUFS3 NDUFS8 +NDUFS3 UQCRFS1 +FIBP SIAHBP1 +FIBP WIPI2 +FIBP POLR2E +AURKAIP1 COX5A +AURKAIP1 C12orf10 +AURKAIP1 MRPL34 +AURKAIP1 GPX4 +AURKAIP1 NDUFS3 +AURKAIP1 TUFM +AURKAIP1 NDUFS8 +AURKAIP1 NDUFS7 +AURKAIP1 NDUFA2 +AURKAIP1 C3orf60 +AURKAIP1 NDUFA3 +AURKAIP1 UQCRFS1 +AURKAIP1 TMEM93 +AURKAIP1 UQCRC1 +AURKAIP1 FIBP +AURKAIP1 EXOSC4 +DIP2A PGF +CD52 IL7R +CD52 TRBV3-1 +CD52 FAIM3 +CD52 LCK +CD52 ISG20 +CD52 TRA@ +CD52 TRB@ +CD52 RAC2 +CD52 TRBC1 +CSNK1A1 PAPOLA +CSNK1A1 EIF4G2 +PAPOLA TAF7 +PAPOLA PUM2 +PAPOLA SON +PAPOLA ZNF9 +PAPOLA SSR1 +PAPOLA SRP72 +PAPOLA TRAM1 +PAPOLA THRAP1 +PAPOLA TMEM123 +PAPOLA USP34 +PAPOLA RNPC2 +PAPOLA PNRC2 +PAPOLA YTHDC1 +PAPOLA SF3B1 +HLA-G MAN2B1 +HLA-G TGFB1 +HLA-G ISG20 +HLA-G IFI30 +HLA-G HLA-J +MAN2B1 TGFB1 +ATP5D COX5B +ATP5D CYC1 +COX5B FLJ14346 +COX5B NDUFS3 +COX5B NDUFB2 +COX5B NDUFA3 +COX5B MRPS12 +COX5B NDUFS8 +COX5B UQCRC1 +COX5B CYC1 +COX5B MSRB2 +COX5B UQCRQ +COX5B NDUFB11 +COX5B MDH2 +COX5B NDUFV1 +COX5B UQCRFS1 +COX5B NDUFS7 +COX5B NDUFAB1 +COX5B COX8A +COX5B UQCR +IGLJ3 IGLV3-25 +IGLJ3 LOC91316 +215176_x_at IGHG3 +215176_x_at 217258_x_at +215176_x_at IGHV1-69 +215176_x_at 217281_x_at +215176_x_at IGLC2 +215176_x_at IGHA1 +215176_x_at IGKC +215176_x_at IGHM +215176_x_at IGLV3-25 +215176_x_at IGKV1OR2-108 +215176_x_at IGLJ3 +215176_x_at IGKV1-5 +215176_x_at IGKV1OR15-118 +215176_x_at IGLC1 +215176_x_at IGL@ +215176_x_at LOC91316 +215176_x_at IGKV1D-13 +215176_x_at 221651_x_at +215176_x_at CTA-246H3.1 +IGHG3 IGHM +IGHG3 IGHV1-69 +IGHG3 IGKV1-5 +IGHG3 IGLJ3 +IGHG3 IGKV1D-13 +IGHG3 IGKC +IGHG3 IGKV1OR2-108 +IGHG3 IGLC2 +IGHG3 IGLC1 +IGHG3 LOC91316 +217258_x_at IGLC2 +217258_x_at IGKC +217258_x_at IGL@ +217258_x_at CTA-246H3.1 +217258_x_at IGLJ3 +217258_x_at IGKV1-5 +217258_x_at IGLV3-25 +RPL14 RPL17 +RPL14 RPS4X +RPL14 RPS3A +RPL14 RPL5 +RPL14 RPS7 +RPL14 U16 +RPL14 RPL21 +RPL14 RPL4 +RPL14 RPL6 +RPL14 RPL35A +RPL14 RPL39 +RPL14 RPL23 +DYNLT3 PAFAH1B1 +PAFAH1B1 PRKACB +PAFAH1B1 POPDC3 +PAFAH1B1 REEP5 +PAFAH1B1 ZFR +PAFAH1B1 PCNP +PAFAH1B1 SEPT7 +RPS18 RPS19 +SF3B1 WAC +SF3B1 SON +SF3B1 TUG1 +SF3B1 SFRS5 +SF3B1 YTHDC1 +SF3B1 TCEA1 +IQGAP1 MBNL1 +IQGAP1 ROCK1 +MBNL1 OSBPL8 +MBNL1 ROCK1 +MBNL1 PNRC2 +MBNL1 PUM2 +NPM1 RPS3A +NPM1 SFRS5 +NPM1 SFRS11 +NPM1 RPS7 +NPM1 RPS6 +NPM1 RPL7 +NPM1 RPL34 +NPM1 TCEA1 +NPM1 TUG1 +NPM1 RPL5 +NPM1 RPL17 +NPM1 RPL24 +NPM1 SF3B1 +NPM1 TINP1 +NPM1 RPL6 +NPM1 RPS24 +NPM1 RPL4 +NPM1 RPS27A +NPM1 RNPC2 +RPS3A SF3B1 +RPS3A TPT1 +RPS3A U16 +RPS3A RPS4X +RPS3A RPS7 +RPS3A RPS6 +SPN ZNF552 +DDX50 RNPC2 +EIF5 PCNP +EIF5 PAPOLA +EIF5 TAF7 +EIF5 MATR3 +EIF5 HSP90AA1 +PCNP SEPT2 +PCNP TLOC1 +PCNP RNPC2 +217281_x_at IGHM +217281_x_at IGHV1-69 +217281_x_at IGLJ3 +217281_x_at IGKC +217281_x_at IGLC2 +217281_x_at IGHA1 +217281_x_at IGHG3 +217281_x_at IGKV1OR2-108 +CHCHD2 MIF +CHCHD2 NDUFAB1 +CHCHD2 COX5B +CHCHD2 MRPS12 +CHCHD2 MRP63 +CHCHD2 COX8A +CHCHD2 SLC25A3 +CHCHD2 NDUFB11 +MIF TIMM13 +ARPC5 CAP1 +ARPC5 PRG1 +SFRS11 TUG1 +ACTR10 SKP1A +ACTR10 PRKAR1A +SKP1A SUMO2 +ACTR2 ARPC3 +ACTR2 ACTR3 +ACTR2 CAPZA1 +ACTR2 PDCD10 +ARPC3 CAPZA1 +CAV1 MMP2 +CAV1 COL6A1 +CAV1 FBLN1 +CAV1 COL1A2 +CAV1 PTRF +CAV1 LAMC1 +ACTG2 MYH11 +MYH11 MYLK +MYH11 TPM1 +MYH11 TAGLN +RPL38 RPS20 +RPL38 RPLP2 +RPL38 RPS19 +RPS20 RPS27 +CSDE1 FLJ11021 +CSDE1 WAC +CSDE1 NFE2L2 +COL1A2 COL3A1 +COL3A1 COL6A3 +CD7 LCK +CD7 HMHA1 +LCK RAC2 +LCK TRA@ +LCK TRB@ +LCK TRBC1 +LCK TRBV3-1 +NDUFB11 NDUFS3 +NDUFB11 UQCRQ +NDUFB11 UQCR +NDUFB11 NDUFB4 +NDUFB11 UQCRFS1 +NDUFB11 SLC25A3 +NDUFB11 NDUFS8 +NDUFB11 POLR2L +C3orf60 EXOSC4 +C3orf60 POLR2I +C3orf60 MRPL34 +C3orf60 GPX4 +211639_x_at IGKC +RPL11 RPS15A +RPL11 RPL5 +RPL11 RPS24 +RPL11 RPL17 +RPL11 RPL7 +RPL11 U16 +RPL11 RPL24 +RPL11 RPL21 +RPL11 RPL35A +RPL11 RPS25 +RPL11 RPL14 +RPL11 RPS6 +RPL11 RPL22 +RPL11 RPL34 +RPL11 RPL4 +RPL11 RPS4X +RPL11 RPS3A +RPL11 RPS7 +RPL11 RPL39 +RPL11 RPL31 +RPL11 RPL36A +RPL11 RPL23 +RPL11 RPS17 +RPL11 RPL30 +RPL11 RPS23 +RPS15A RPS6 +RPS15A RPS17 +RPS15A RPS7 +RPS15A RPS3A +RPS15A RPS20 +RPS15A RPS27 +RPS15A RPS23 +GNB2L1 RPL13A +GNB2L1 RPS19 +GNB2L1 RPL29 +GNB2L1 RPS16 +RPL29 RPS9 +RPL29 RPS16 +RPL29 RPL35 +RPL13 RPL18A +RPL13 RPL28 +RPL13 RPS2 +RPL13 RPL18 +RPL13 RPLP2 +RPL13 RPS15 +RPL18A RPS2 +RPL18A RPS19 +RPL18A RPLP2 +RPL18A RPL28 +HLA-A HLA-F +HLA-A MAN2B1 +HLA-A HLA-E +HLA-A HLA-G +HLA-A HLA-B +HLA-A HLA-J +HLA-A HLA-C +HLA-F ISG20 +HLA-F HLA-G +HLA-F IFI30 +HLA-F MAN2B1 +HLA-F HLA-J +BTF3 MDS1 +BTF3 RPS27 +BTF3 RPS17 +BTF3 RPS15A +BTF3 RPL30 +BTF3 LOC402057 +BTF3 RPL22 +NACA RPL35A +NACA RPS3A +NACA RPL5 +NACA NAP1L1 +NACA SFRS5 +NACA RPS7 +NACA RPL24 +NACA U16 +NACA RPL21 +NACA RPL9 +NACA NPM1 +NACA RPS23 +NACA RPL4 +NACA RPS27A +NACA RPS24 +NACA RPL17 +NACA RPL6 +NACA TINP1 +RPL35A RPL7 +RPL35A RPS17 +RPL35A RPS27A +RPL35A RPS20 +RPL35A RPS27 +RPL35A RPS15A +RPL35A RPS3A +RPL35A RPS25 +RPL35A RPL36A +RPL35A RPL39 +RPL35A RPL6 +RPL35A RPS6 +RPL35A RPS4X +RPL35A RPL4 +RPL35A RPL5 +RPL35A RPS23 +RPL35A RPS24 +RPL35A U16 +RPL35A RPL9 +RPL35A RPS7 +BIRC2 DDX5 +BIRC2 ZNF403 +BIRC2 LOC58486 +BIRC2 THRAP1 +BIRC2 SERBP1 +BIRC2 PAPOLA +BIRC2 USP34 +BIRC2 C1orf63 +DDX5 SERBP1 +DDX5 SRP46 +DDX5 hfl-B5 +DDX5 TUG1 +DDX5 NAP1L1 +DDX5 HNRPH1 +DDX5 NBPF1 +DDX5 SFRS5 +DDX5 LOC342346 +DDX5 NBPF12 +DDX5 MGEA5 +DDX5 HNRPA2B1 +DDX5 SON +DDX5 FLJ10154 +DDX5 USP34 +DDX5 NPM1 +DDX5 TARDBP +DDX5 EIF4G2 +DDX5 PAPOLA +DDX5 HIATL1 +DDX5 YTHDC1 +DDX5 SF3B1 +RPL30 U16 +RPL30 RPS7 +RPL30 RPS25 +RPL30 RPS13 +RPL30 RPS3A +RPL30 RPS4X +RPL30 RPL4 +RPL30 RPS6 +RPL30 RPL37 +RPL30 RPL31 +RPL30 RPL36A +RPL30 RPL35A +RPL30 RPS27 +RPL30 RPS17 +RPL30 RPS15A +RPL30 RPL9 +RPL30 RPS20 +RPL30 RPS23 +RPL30 RPL32 +RPL30 RPL39 +TMEM123 TRAM1 +IGHA1 IGHV1-69 +IGHA1 IGKC +IGHA1 IGLJ3 +IGHA1 IGLV3-25 +IGHA1 IGHM +IGHA1 IGKV1OR15-118 +IGHA1 IGLC2 +IGHA1 IGL@ +IGHA1 IGKV1-5 +IGHA1 IGKV1D-13 +IGHA1 IGHG3 +IGHA1 IGKV1OR2-108 +IGHA1 LOC91316 +IGHA1 IGLC1 +IGHV1-69 IGLC2 +IGHV1-69 IGKC +IGHV1-69 IGLC1 +IGHV1-69 IGKV1OR2-108 +NDUFS7 POLR2I +NDUFS7 NDUFV1 +NDUFS7 NDUFS8 +POLR2I UQCRFS1 +214836_x_at 217258_x_at +214836_x_at 217281_x_at +214836_x_at IGLC2 +214836_x_at IGLV3-25 +214836_x_at IGL@ +214836_x_at IGKV1OR2-108 +214836_x_at IGKV1OR15-118 +214836_x_at IGLJ3 +214836_x_at IGHG3 +214836_x_at IGKC +214836_x_at LOC91316 +214836_x_at IGKV1-5 +214836_x_at IGKV1D-13 +214836_x_at IGLC1 +214836_x_at IGHA1 +214836_x_at 221651_x_at +214836_x_at CTA-246H3.1 +214836_x_at 215176_x_at +RPS6 TPT1 +RPS6 RPS7 +RPS6 U16 +GTF2I SRP9 +GTF2I TMEM66 +SRP9 SUMO1 +ATP2A3 TRA@ +ATP2A3 TRB@ +ATP2A3 TRBC1 +TRA@ TRBC1 +TRA@ TRB@ +IGLC2 IGLJ3 +IGLC2 LOC91316 +IGLC2 IGLV3-25 +IGLC2 IGLV3-10 +HSP90B1 ZNF638 +HSP90B1 RNPC2 +HSP90B1 VDP +RPL7 TPT1 +RPL7 RPS3A +RPL7 RPS6 +RPL7 RPS24 +RPL7 TCEA1 +RPL7 SF3B1 +RPL7 RPS7 +RPL7 RPS25 +RPL7 RPL9 +RPL7 RPS23 +RPL7 RPS27A +RPL7 RPS4X +MGEA5 NAP1L1 +MGEA5 PAPOLA +MGEA5 XPO1 +MGEA5 SF3B1 +MGEA5 RNPC2 +MGEA5 YTHDC1 +MGEA5 PNRC2 +BCLAF1 PCM1 +BCLAF1 RNPC2 +BCLAF1 PUM2 +BCLAF1 EIF3S6 +BCLAF1 USP47 +BCLAF1 SF3B1 +BCLAF1 SLC38A2 +BCLAF1 FLJ11021 +BCLAF1 COPS2 +BCLAF1 PCNP +BCLAF1 CSDE1 +BCLAF1 MATR3 +BCLAF1 WAC +BCLAF1 CROP +BCLAF1 SRP72 +BCLAF1 LOC58486 +BCLAF1 PHIP +BCLAF1 ZNF638 +BCLAF1 VDP +BCLAF1 NPM1 +BCLAF1 HSP90B1 +BCLAF1 TLOC1 +PCM1 VDP +CXorf9 GPSM3 +CXorf9 TRA@ +CXorf9 IL2RG +CXorf9 IL16 +216342_x_at RPL7 +216342_x_at TPT1 +216342_x_at RPL14 +216342_x_at RPL9 +216342_x_at RPS27A +216342_x_at RPL17 +216342_x_at RPS17 +216342_x_at RPL5 +216342_x_at U16 +216342_x_at RPL22 +216342_x_at RPL36A +216342_x_at RPS25 +216342_x_at LOC402057 +216342_x_at RPS7 +216342_x_at RPL21 +216342_x_at RPL24 +216342_x_at RPL41 +216342_x_at RPS24 +216342_x_at MDS1 +216342_x_at RPL6 +216342_x_at LOC440055 +216342_x_at RPL4 +216342_x_at RPL34 +216342_x_at RPS3A +216342_x_at RPL11 +216342_x_at RPS6 +216342_x_at RPL23 +216342_x_at RPL30 +216342_x_at RPL31 +216342_x_at RPS23 +216342_x_at RPL35A +216342_x_at RPS4X +NDUFA13 NDUFA3 +NDUFA13 NDUFB2 +NDUFA3 NDUFAB1 +NDUFA3 UCRC +NDUFA3 NDUFB2 +NDUFA3 NDUFB11 +NDUFA3 UQCR +NDUFA3 UQCRC1 +NDUFA3 NDUFB4 +CDKN1B PPP2CA +CDKN1B MARCKS +HMGB1 HNRPR +HMGB1 HNRPA2B1 +HNRPR SRP9 +HNRPR SON +HNRPR SFRS10 +AP2S1 ATP6V0C +AP2S1 POLR2E +AP2S1 GPX1 +AP2S1 GUK1 +AP2S1 NAPA +AP2S1 COPE +AP2S1 ATP6V0B +AP2S1 MRPS12 +211637_x_at IGKC +211637_x_at IGHA1 +211637_x_at IGLC2 +211637_x_at 211639_x_at +211637_x_at IGHM +211637_x_at 211908_x_at +211637_x_at IGHV1-69 +211637_x_at CTA-246H3.1 +211637_x_at 211641_x_at +211637_x_at 215176_x_at +211637_x_at 211645_x_at +211637_x_at IGHG3 +211637_x_at 217281_x_at +211637_x_at LOC91316 +COL4A1 COL4A2 +COL4A1 FBLN1 +COL4A2 RBPMS +FLJ42393 PDE4C +FLJ42393 ZNF160 +FLJ42393 POLR1B +FLJ42393 KNS2 +FLJ42393 SLC35E1 +FLJ42393 LOC152719 +MORF4L1 PTGES3 +MORF4L1 SKP1A +PTGES3 SUMO2 +SON XPO1 +SON SRP46 +SON YTHDC1 +IFI30 TGFB1 +IFI30 NCF1 +AASDHPPT POPDC3 +AASDHPPT PAFAH1B1 +POPDC3 RAB6C +POPDC3 PRKACB +FXYD5 ITGB2 +FXYD5 PSMB9 +CD79A IGKC +CD79A IGHA1 +CD79A IGLC2 +CD79A IGLJ3 +FLJ14346 ZNF611 +FLJ14346 GPAA1 +FLJ14346 MRPS12 +FLJ14346 LOC152719 +FLJ14346 SLC35E1 +HLA-DPA1 HLA-DQB1 +HLA-DPA1 HLA-DQA1 +HLA-DPA1 HLA-DRA +HLA-DPA1 HLA-DRB5 +HLA-DPA1 HLA-DRB4 +HLA-DPA1 HLA-DRB1 +HLA-DPA1 HLA-DPB1 +HLA-DQB1 HLA-DRB4 +HLA-DQB1 HLA-DRB5 +HLA-DQB1 HLA-DRB1 +IFNGR1 ROCK1 +C6orf12 HLA-F +C6orf12 HLA-G +C6orf12 HLA-B +C6orf12 HLA-C +C6orf12 HLA-J +C6orf12 HLA-A +NARS TLOC1 +NARS TTC3 +NARS SRP9 +RPL36A RPL6 +RPL36A RPS7 +RPL36A RPS24 +RPL36A RPS3A +RPL36A RPL4 +RPL36A RPS4X +RPL36A RPS25 +RPL36A RPS15A +RPL36A RPS6 +RPL36A RPS17 +RPL36A RPS23 +RPL36A RPL39 +217718_s_at YWHAZ +217718_s_at CAB39 +RPL23A RPS20 +TAF7 UBE4A +TAF7 YTHDC1 +TAF7 XPO1 +DHX15 OGT +DHX15 KIAA0907 +DHX15 NPM1 +CD2 TRA@ +CD2 CD52 +CD2 IL7R +DST PLEKHC1 +DST SPARCL1 +COL6A1 COL6A3 +COL6A1 FBLN1 +COL6A1 PTRF +FIS1 NDUFA13 +EIF4G2 SRP46 +EIF4G2 HSP90AA1 +EIF4G2 SERBP1 +EIF4G2 SON +EIF4G2 EIF5 +EIF4G2 NPM1 +EIF4G2 PUM2 +EIF4G2 SET +EIF4G2 PCNP +EIF4G2 LOC342346 +EIF4G2 HNRPA2B1 +EIF4G2 YTHDC1 +EIF4G2 XPO1 +EIF4G2 MATR3 +EIF4G2 PAPOLA +EIF4G2 THRAP1 +EIF4G2 SF3B1 +EIF4G2 HIATL1 +SRP46 XPO1 +COL1A1 COL6A3 +COL1A1 COL1A2 +COL1A1 COL3A1 +C12orf10 FIBP +C12orf10 C3orf60 +LMOD1 MYH11 +208246_x_at POLR1B +208246_x_at ZNF611 +208246_x_at PGF +208246_x_at ATP8B1 +208246_x_at RIOK3 +208246_x_at KNS2 +208246_x_at DBT +208246_x_at LOC152719 +208246_x_at FBXW12 +208246_x_at PDE4C +208246_x_at SLC35E1 +208246_x_at ZNF160 +208246_x_at 216524_x_at +HNRPH1 SON +HNRPH1 SF3B1 +HNRPH1 YTHDC1 +RPS14 RPS16 +RPS14 RPS5 +RPS14 RPS9 +RPS16 RPS19 +RPS16 RPS9 +RPS16 RPS5 +ARHGDIB LAPTM5 +ARHGDIB PRG1 +ARHGDIB UCP2 +ARHGDIB RAC2 +ARHGDIB CXCR4 +ARHGDIB CECR1 +LAPTM5 RAC2 +LAPTM5 PRG1 +ATRX COPS2 +ATRX PCNP +ATRX PUM2 +ATRX MATR3 +ATRX SH3BGRL +ATRX SEPT7 +ATRX BCLAF1 +ATRX TLOC1 +HIATL1 SFRS5 +HIATL1 RPL6 +HIATL1 RPL17 +HIATL1 TUG1 +HIATL1 HNRPA2B1 +HIATL1 RAVER2 +HIATL1 RNPC2 +HIATL1 HNRPH1 +HIATL1 YTHDC1 +HIATL1 TCEA1 +HIATL1 NPM1 +HIATL1 LOC58486 +HIATL1 THRAP1 +HIATL1 PAPOLA +HIATL1 SF3B1 +FAIM3 TRA@ +FAIM3 HCLS1 +FAIM3 RAC2 +IGKV1D-13 IGLJ3 +IGKV1D-13 IGLV3-25 +IGKV1D-13 IGLC2 +IGKV1D-13 LOC91316 +IGKV1D-13 IGL@ +IGKV1D-13 IGLC1 +IGKV1D-13 IGKV1OR15-118 +HSP90AA1 MATR3 +HSP90AA1 SRP9 +HSP90AA1 SERBP1 +HSP90AA1 SEPT2 +HSP90AA1 NARS +RPL19 RPS20 +LOC283412 RPL29 +LOC283412 RPS9 +DGCR6 POLR2I +LTB TRBC1 +LTB TRB@ +HMHA1 PSMB8 +HMHA1 TRBC1 +HMHA1 RAC2 +PSMB8 TAPBP +PSMB8 PSMB9 +IGKV1-5 IGL@ +IGKV1-5 IGKV1OR2-108 +IGKV1-5 IGLC2 +IGKV1-5 IGLJ3 +IGKV1-5 IGLV3-25 +IGKV1-5 IGLC1 +IGKV1-5 IGKV1D-13 +IGKV1-5 IGKV1OR15-118 +RPL28 RPS10 +RPL28 RPS2 +RPL28 RPS19 +RPL28 RPS16 +RPL28 RPLP2 +HCLS1 ITGB2 +HCLS1 SELL +HCLS1 RAC2 +HCLS1 TRA@ +FBLN1 MMP2 +FBLN1 PTRF +NDUFA2 UQCRFS1 +NDUFA2 UQCR +NDUFA2 NDUFS8 +NDUFA2 UQCRC1 +NDUFA2 NDUFB4 +NDUFA2 TCEB2 +NDUFA2 NDUFA3 +NDUFA2 UCRC +HLA-DRB4 HLA-DRB5 +FAM96B NDUFV1 +FAM96B NDUFS8 +FAM96B NDUFS3 +FAM96B TUFM +FAM96B NDUFS7 +FAM96B NDUFA10 +FAM96B NDUFA2 +FAM96B NDUFA3 +COQ9 NDUFB4 +COQ9 UQCRFS1 +EEF1B2 RPL30 +EEF1B2 RPL10A +EEF1B2 RPS23 +EEF1B2 RPS15A +EEF1B2 RPL35A +EEF1B2 RPL14 +EEF1B2 RPL36A +EEF1B2 RPL37 +EEF1B2 RPL39 +EEF1B2 LOC440055 +PUM2 WAC +PUM2 SF3B1 +ALOX5AP LAPTM5 +ALOX5AP ITGB2 +DKFZP566N034 ZNF160 +DKFZP566N034 SLC35E1 +ARL6IP5 PAFAH1B1 +TRB@ TRBV3-1 +TRB@ TRBC1 +IFI16 ROCK1 +IFI16 IQGAP1 +IFI16 TRIM22 +IFI16 MBNL1 +IFI16 IFNGR1 +211641_x_at 211908_x_at +211641_x_at 211645_x_at +211641_x_at IGHG3 +211641_x_at IGKV1-5 +211641_x_at 217281_x_at +211641_x_at IGHA1 +211641_x_at IGHM +211908_x_at IGHV1-69 +211908_x_at 217281_x_at +211908_x_at IGHM +WSB1 XPO1 +ARPC1B HLA-C +ARPC1B HLA-A +ARPC1B HLA-B +ARPC1B ISG20 +ARPC1B HLA-G +CD53 ITGB2 +CD53 IL10RA +CD53 HCLS1 +HLA-E HLA-F +HLA-E PSMB8 +HLA-E HLA-J +HLA-E MAN2B1 +HLA-E HLA-G +211645_x_at IGHA1 +211645_x_at IGKC +211645_x_at IGHM +211645_x_at IGLC2 +211645_x_at IGHG3 +211645_x_at IGL@ +211645_x_at IGLV3-25 +211645_x_at IGLJ3 +211645_x_at LOC91316 +211645_x_at IGLC1 +211645_x_at IGKV1-5 +211645_x_at 221651_x_at +211645_x_at IGKV1OR15-118 +211645_x_at CTA-246H3.1 +211645_x_at IGKV1D-13 +211645_x_at 214836_x_at +211645_x_at 215176_x_at +NDUFB2 NDUFS7 +NDUFB2 UQCRC1 +NDUFB2 UQCR +NDUFB2 NDUFV1 +FLJ10154 LOC58486 +FLJ10154 XPO1 +FLJ10154 PFAAP5 +FLJ10154 RNPC2 +FLJ10154 HNRPDL +FLJ10154 SFPQ +FLJ10154 SFRS11 +FLJ10154 NPM1 +EIF4A2 TTC3 +EIF4A2 MATR3 +GPX4 R3HCC1 +GPX4 POLR2I +GPX4 MRPL34 +PRKAR2A SLC35E1 +SLC35E1 UBE2D2 +SLC35E1 WDR45 +SLC35E1 ZNF611 +SLC35E1 ZNF160 +IGLC1 IGLJ3 +IGLC1 IGLV3-25 +IGLC1 IGLC2 +IGLC1 LOC91316 +UQCRC1 UQCRFS1 +TMED5 WAC +MDH2 NDUFB11 +MDH2 UQCRQ +MDH2 UQCRC1 +MDH2 UQCRFS1 +MDH2 NDUFS8 +IGL@ IGLV3-25 +IGL@ IGLC2 +IGL@ IGLC1 +IGL@ IGLJ3 +HLA-DRB1 HLA-F +HLA-DRB1 HLA-DRB4 +HLA-DRB1 HLA-DRB5 +CD74 HLA-DQB1 +CD74 HLA-DRB4 +CD74 HLA-DRB1 +CD74 HLA-DRA +CD74 HLA-DMA +CD74 HLA-DPB1 +CD74 HLA-DRB5 +CD74 HLA-DPA1 +CLTC NARS +RPS17 RPS25 +RPS17 RPS4X +RPS17 RPS6 +RPS17 RPS7 +RPS17 RPS3A +RPS17 RPS20 +RPS17 RPS23 +RPS17 RPS27 +ACTR3 CAP1 +ACTR3 ARPC3 +ACTR3 YWHAZ +ACTR3 CAPZA1 +RPS10 RPS16 +RPS10 RPS21 +RPS10 RPS19 +RPL27A RPS11 +RPL27A RPLP2 +RPL27A RPS19 +RPL27A RPL38 +RPL27A RPS20 +RPL27A RPS18 +DDX17 SON +DDX17 HNRPH1 +C11orf58 YTHDC1 +C11orf58 HNRPK +C11orf58 LOC342346 +C11orf58 SON +NAPA WIPI2 +RGPD5 TTC3 +CD3D TRA@ +CD3D TRB@ +CD3D TRBC1 +IGKV1OR2-108 LOC91316 +IGKV1OR2-108 IGLC2 +IGKV1OR2-108 IGLC1 +IGKV1OR2-108 IGLJ3 +CAB39 SUB1 +ATP6V0B GPX1 +ATP6V0B COPE +ATP6V0B MRPS12 +ATP6V0B ATP6V0D1 +ATP6V0B ATP6V0C +GPX1 PPP1CA +GPX1 PGLS +GPX1 OAZ1 +ENDOG NDUFS7 +LOC388344 RPS2 +LOC388344 RPL18 +LOC388344 RPL18A +LOC388344 RPL13 +LOC388344 RPS15 +LOC440055 RPL22 +LOC440055 RPL17 +LOC440055 RPS7 +LOC440055 RPL9 +LOC440055 RPS3A +LOC440055 RPS15A +LOC440055 RPL21 +LOC440055 RPS4X +LOC440055 U16 +LOC440055 RPL31 +LOC440055 RPL37 +LOC440055 RPL35A +LOC440055 MDS1 +LOC440055 RPS6 +LOC440055 RPS17 +LOC440055 RPL32 +LOC440055 RPL23 +LOC440055 RPL4 +LOC440055 RPL39 +LOC440055 RPS20 +LOC440055 RPL36A +LOC440055 RPL30 +LOC440055 RPS23 +LOC440055 RPL11 +CYBA IFI30 +CYBA HLA-G +CYBA HLA-B +CYBA HLA-J +CYBA TGFB1 +RBPMS TGFB1I1 +KNS2 PRR11 +KNS2 SPG21 +KNS2 LOC152719 +KNS2 PGF +KNS2 UBE2D2 +KNS2 PRKAR2A +KNS2 RIOK3 +KNS2 ZNF611 +KNS2 ZNF160 +KNS2 PDE4C +KNS2 SLC35E1 +NPEPPS SEPT7 +MRPS12 ZNHIT1 +GLTSCR2 RPS2 +GLTSCR2 RPL13 +GLTSCR2 LOC388344 +HLA-DMB HLA-DRA +220725_x_at CDC5L +220725_x_at SLC25A16 +220725_x_at C12orf38 +220725_x_at OPHN1 +220725_x_at MCM3AP +220725_x_at LOC56902 +CDC5L LOC56902 +CDC5L MCM3AP +ARL8B HSP90AA1 +ARL8B NARS +CTA-246H3.1 IGLC2 +CTA-246H3.1 IGLV3-25 +CTA-246H3.1 IGKC +CTA-246H3.1 IGKV1OR15-118 +CTA-246H3.1 IGHM +CTA-246H3.1 IGLJ3 +CTA-246H3.1 IGKV1OR2-108 +CTA-246H3.1 IGL@ +CTA-246H3.1 IGHG3 +CTA-246H3.1 IGKV1-5 +CTA-246H3.1 IGKV1D-13 +CTA-246H3.1 LOC91316 +CTA-246H3.1 IGLC1 +CTA-246H3.1 IGHA1 +218041_x_at BCLAF1 +218041_x_at PUM2 +218041_x_at SLC38A2 +MEFV SPN +SMTN TGFB1I1 +ZNF160 ZNF611 +TUFM UQCRFS1 +TUFM UQCRC1 +CD164 COPB +ARF5 POLR2J +ARF5 ATP6V0D1 +HLA-B MYH9 +HLA-B MAN2B1 +HLA-B HLA-G +HLA-B HLA-F +HLA-B HLA-E +HLA-B HLA-C +HLA-B HLA-J +IGHD IGHM +ADRM1 EXOSC4 +ADRM1 AP2S1 +ADRM1 FIBP +ADRM1 AURKAIP1 +211650_x_at IGLV3-25 +211650_x_at IGHG3 +211650_x_at IGKC +211650_x_at IGHV1-69 +211650_x_at IGHM +FLJ12151 PPP2CA +FLJ12151 LOC152719 +FLJ12151 PRR11 +FLJ12151 PDE4C +FLJ12151 ZNF611 +FLJ12151 SLC35E1 +FLJ12151 ZNF160 +HNRPK HNRPU +HNRPK SET +TOMM20 ZMYND11 +TOMM20 TTC3 +TOMM20 TTC19 +CORO1A LAPTM5 +AD7C-NTP ARL6IP2 +AD7C-NTP MEFV +AD7C-NTP CDC5L +ARL6IP2 CDC5L +ARL6IP2 LOC56902 +ARL6IP2 MEFV +C1orf63 PAPOLA +C1orf63 LOC342346 +RPL31 RPL5 +RPL31 TPT1 +RPL31 RPS27A +RPL31 RPS7 +RPL31 RPS17 +RPL31 RPL9 +RPL31 RPS3A +RPL31 RPL6 +RPL31 RPL35A +RPL31 RPL36A +RPL31 RPL7 +RPL31 RPS6 +RPL31 RPL4 +RPL31 RPS24 +RPL31 RPS23 +RPL31 RPL34 +RPL31 RPS4X +SDHA TUFM +UQCR UQCRQ +UQCR UQCRC1 +FNBP4 RPL26 +FNBP4 RNPC2 +FNBP4 RPL17 +FNBP4 OGT +FNBP4 RPL6 +FNBP4 RPS27A +PNRC2 ZNF638 +PNRC2 PUM2 +PNRC2 SF3B1 +PNRC2 ROCK1 +IGLV3-10 IGLV3-25 +MSN PRG1 +PPIB RPN1 +PPIB SEC61A1 +THRAP1 ZNF638 +IRF7 PRKD2 +IRF7 MAN2B1 +221651_x_at IGLJ3 +221651_x_at IGKV1OR15-118 +221651_x_at IGLC2 +221651_x_at IGKV1-5 +221651_x_at IGKC +221651_x_at IGKV1OR2-108 +221651_x_at IGHG3 +221651_x_at LOC91316 +221651_x_at IGKV1D-13 +221651_x_at CTA-246H3.1 +221651_x_at IGHA1 +221651_x_at IGLC1 +LOC152719 PRR11 +LOC152719 LRRFIP1 +LOC152719 PDE4C +LOC152719 RIOK3 +LOC152719 ZNF611 +LOC152719 SLC35E1 +LOC152719 ZNF160 +PRKAR1A RNF11 +PRKAR1A SRP9 +PRKAR1A TMEM66 +INPP5D ITGB2 +216524_x_at ZNF611 +216524_x_at ZNF160 +216524_x_at PGF +216524_x_at KNS2 +216524_x_at CEP27 +216524_x_at RIOK3 +216524_x_at DBT +216524_x_at PDE4C +216524_x_at FBXW12 +RPL10A RPL39 +RPL10A RPL14 +RPL10A RPL12 +RPL27 RPS20 +RPL27 RPS11 +RPL27 RPLP2 +RPL27 RPL38 +RPL27 RPS19 +RPL27 RPL27A +ERH HNRPK +ERH SUMO2 +PGF POLR1B +PGF PRR11 +PGF ZNF160 +PGF WDR45 +PGF RIOK3 +PGF ZNF611 +CAPNS1 CLPTM1 +CLPTM1 NAPA +RPS19 RPS2 +RPS19 RPS21 +RPS19 RPS20 +DDX42 MT1H +DDX42 MT1M +DDX42 MT1G +DDX42 MT1F +MT1H MT1L +MT1H MT2A +MT1H MT1M +MT1H MT1X +CD37 PTPRCAP +CD37 ISG20 +PTPRCAP TRBV3-1 +EIF3S6 RPS4X +EIF3S6 RPL26 +EIF3S6 ZNF638 +EIF3S6 MDS1 +EIF3S6 RPS3A +EIF3S6 RPL24 +EIF3S6 RPL7 +EIF3S6 RPL31 +EIF3S6 RPL4 +EIF3S6 RPL21 +EIF3S6 U16 +EIF3S6 NPM1 +EIF3S6 RNPC2 +EIF3S6 NACA +EIF3S6 RPS7 +EIF3S6 TINP1 +EIF3S6 VDP +EIF3S6 RPS27A +EIF3S6 RPL34 +EIF3S6 RPL5 +EIF3S6 RPS24 +EIF3S6 RPL17 +EIF3S6 RPL6 +IGKV1OR15-118 IGL@ +IGKV1OR15-118 IGLC2 +IGKV1OR15-118 IGLV3-25 +NDUFB6 TCEB2 +ACTA2 LMOD1 +ACTA2 MYH11 +ACTA2 TAGLN +GMFG ITGB2 +HNRPU SET +HNRPH3 LOC58486 +HNRPH3 RNPC2 +HNRPH3 MATR3 +HNRPH3 KIDINS220 +C16orf24 SCAND1 +NBPF1 NBPF12 +ARPP-19 PRKACB +217052_x_at G3BP +217052_x_at 220725_x_at +CCL5 HLA-F +CCL5 HLA-C +CCL5 HLA-G +RPLP2 RPS16 +RPLP2 RPS20 +RPLP2 RPS19 +RPLP2 RPS11 +ECH1 UQCRQ +RPL12 RPL14 +RPL12 RPL39 +RPL12 RPL19 +CECR1 LAPTM5 +ATP6V0D1 CHMP2A +ATP6V0D1 POLR2J +ATP6V0D1 PSMB3 +ATP6V0D1 OAZ2 +CBX3 HNRPA2B1 +CBX3 XPO1 +SFRS10 SON +TIMM8B UQCRC1 +CD48 IL16 +ARF1 COPE +ARF1 YIPF3 +ARF1 CAPNS1 +ARF1 RPN1 +KPNB1 PAFAH1B1 +HLA-J IFI30 +HLA-J MAN2B1 +USP47 ZNF638 +USP47 VDP +RPL36 RPS16 +RPL36 RPS18 +RPL36 RPS19 +RPL36 RPS10 +CUL4B SH3BGRL +ADD3 WAC +ADD3 SEPT7 +ADD3 MATR3 +DEXI NDUFB11 +LYSMD4 SEPT7 +PFAAP5 ZNF638 +PFAAP5 RNPC2 +215182_x_at SPN +215182_x_at CDC5L +215182_x_at 220725_x_at +215182_x_at LOC56902 +215182_x_at MCM3AP +HLA-DQA1 HLA-DRB4 +HLA-DQA1 HLA-DRA +DDT ZNHIT1 +DDT MRPS12 +RAVER2 SF3B1 +HLA-DMA HLA-DQB1 +HLA-DMA HLA-DRA +HLA-DMA HLA-DRB1 +HLA-DMA HLA-DPA1 +HLA-DMA HLA-DQA1 +HLA-DMA HLA-DRB4 +HLA-DMA HLA-DPB1 +HLA-DMA HLA-DRB5 +RAB6A RAB6C +C1orf160 TCEB2 +C1orf160 NDUFA13 +217679_x_at PDE4C +217679_x_at DBT +217679_x_at PGF +217679_x_at CEP27 +217679_x_at ZNF160 +217679_x_at FLJ12151 +217679_x_at SLC35E1 +217679_x_at KNS2 +217679_x_at FBXW12 +217679_x_at ZNF611 +RPL21 RPL26 +RPL21 RPL36A +RPL21 RPL5 +RPL21 RPL9 +RPL21 RPL30 +RPL21 RPS17 +RPL21 TPT1 +RPL21 RPS7 +RPL21 RPL39 +RPL21 RPS23 +RPL21 RPL24 +RPL21 RPS27A +RPL21 RPS3A +RPL21 U16 +RPL21 RPL22 +RPL21 RPS6 +RPL21 RPL6 +RPL21 RPL23 +RPL21 RPL7 +RPL21 RPS4X +RPL21 RPL4 +RPL21 RPS24 +RPL21 RPL34 +RPL21 RPL31 +RPS13 RPS20 +RPS13 RPS27 +RPL18 RPS2 +RPL18 RPL18A +RPS11 RPS20 +NPTN RAB1A +NPTN PRKAR1A +MCM3AP OPHN1 +MCM3AP ORC6L +EIF4G1 THRAP5 +216412_x_at IGLV3-10 +216412_x_at IGLC2 +CXCR4 LAPTM5 +CXCR4 PRG1 +CXCR4 HCLS1 +FAU RPS9 +FAU RPL35 +FAU RPS5 +FAU RPS16 +FAU RPL28 +RPL39 RPL4 +RPL39 RPS4X +RPL39 RPS20 +RPL39 RPS15A +RPL39 RPS17 +RPL39 RPS23 +IL2RG TRA@ +KIDINS220 ZFR +KIDINS220 SEPT7 +COX4I1 NDUFA7 +ORC6L SLC25A16 +ORC6L RPS11 +GBL GPX4 +ATPIF1 FIBP +MGC2474 TXNL2 +SEPT2 SFRS11 +MT1F MT1L +MT1F MT1X +MT1F MT2A +MT1F MT1G +MT1F MT1H +MT1F MT1M +MT1L MT1M +MT1L MT2A +MT1L MT1X +C11orf2 FLJ14346 +ATP6AP2 PRKAR1A +STAT1 TRIM22 +RPL35 RPS9 +RPL35 RPS5 +RPL35 RPS16 +MT1E MT1L +MT1E MT1F +MT1E MT1G +MT1E MT1X +MT1E MT2A +MT1E MT1M +MT1E MT1H +AKT1 PGLS +LOC56902 MCM3AP +LOC56902 MGC2474 +HNRPM SERBP1 +CALD1 PLEKHC1 +CALD1 TPM1 +MT1M MT2A +MT1M MT1X +C17orf62 IRF7 +KIAA1840 PAPOLA +DES TPM1 +MASP1 POLR2J +212498_at TTC19 +HLA-DPB1 HLA-DQB1 +HLA-DPB1 HLA-DQA1 +HLA-DPB1 HLA-DRB1 +HLA-DPB1 HLA-DRB4 +HLA-DPB1 HLA-DRB5 +FBXW12 PRR11 +FBXW12 RIOK3 +FBXW12 PDE4C +FBXW12 FLJ42393 +FBXW12 FLJ12151 +FBXW12 PGF +FBXW12 SLC35E1 +FBXW12 ZNF611 +FBXW12 LOC152719 +FBXW12 KNS2 +FBXW12 ZNF160 +REEP5 ZMYND11 +PHIP ZNF638 +RPS5 RPS9 +WDR45 ZNF611 +MT1X MT2A +CNOT2 FLJ11021 +P4HB SEC61A1 +TMEM93 UQCRFS1 +RPL37 RPS15A +RPL37 RPS17 +RPL37 RPS13 +RPL37 RPL39 +ACO2 NDUFB2 +MT1G MT1L +MT1G MT1M +MT1G MT2A +MT1G MT1H +MT1G MT1X +ATP5J2 UCRC +RPL32 RPL35A +RPL32 RPS15A +RPL32 RPS17 +RPL32 RPL37 +GOLGA8A GOLGA8B +CAST NFE2L2 +LOC645745 MT1L +LOC645745 MT1F +LOC645745 MT2A +LOC645745 MT1M +LOC645745 MT1E +LOC645745 MT1X +LOC645745 MT1G +LOC645745 MT1H +ITGAV ZMYND11 +CNN1 MYH11 +208238_x_at RIOK3 +FLJ20294 RPLP2 +FLJ20294 RPS11 +FLJ20294 RPL38 +FLJ20294 H3F3A +FLJ20294 RPS20 +FLJ20294 RPS19 +FLJ20294 RPL27A +FLJ20294 RPL27 +SPCS2 SRP9 +208120_x_at ORC6L diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt new file mode 100644 index 0000000..a2e2438 --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_BP_DIRECT_FILTERED.txt @@ -0,0 +1,89 @@ +negative regulation of anoikis P06731,Q07817,P05556,Q07820,P10415 +T cell receptor signaling pathway P35998,P48556,Q13200,P25787,Q99460,P62195,Q9UNM6,O14818,Q15008,P51665,P60900,O00231,P62191 +regulation of cellular response to heat Q99615,P08107,K7ELV2,P62258,Q9UL15,O95816,P0DMV8,P78406,Q92793,P52948 +RNA secondary structure unwinding Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838 +SRP-dependent cotranslational protein targeting to membrane P60866,P08708,P61254,P62269,P39019,P42677,P61927,P62753,Q9Y3U8,P46782 +positive regulation of establishment of protein localization to telomere P40227,P17987,P48643,Q99832 +Fc-epsilon receptor signaling pathway P35998,P48556,Q13200,P25787,Q99460,P62195,P0DP23,Q9UNM6,O14818,Q15008,P51665,P60900,O00231,P62191 +mRNA transport Q09161,O14980,Q9Y5S9,Q53F19,P09651,Q96A72,Q9UN86 +negative regulation of transcription elongation from RNA polymerase II promoter Q8WX92,Q9H3P2,P18615,O94762 +nuclear export Q09161,Q9H814,P52298,P09651 +response to hydrogen peroxide O00629,Q13443,P22460,P42224,Q9P289,P09601,P10415 +intracellular protein transport Q9UM54,Q9H173,Q9ULJ7,Q14974,O00203,P35606,O14964,Q96JC1,P53621,O95373,Q9UBF2,Q9H270,O15397,O14980,O43592,P53618 +rRNA processing P60866,Q9NR30,P39019,P62753,P61927,P57678,O43818,P08708,P61254,Q9NU22,P62269,P42677,P38919,P46782,Q9Y3U8 +release of cytochrome c from mitochondria Q07817,Q16548,Q3ZCQ8,O00429,P10415 +positive regulation of translation P11940,Q9BRP8,Q14011,Q6PKG0,P98179,P38919,Q13838 +focal adhesion assembly P12814,Q08043,P35609,Q14155,P10415 +calcium activated phosphatidylserine scrambling A1A5B4,Q32M45,Q4KMQ2 +gene silencing by RNA P30876,Q09161,Q7KZF4,P11940,K7ELV2,P52298,O15397,P78406,P52948,P24928 +negative regulation of programmed cell death P02768,Q96EY1,O76024,P62195 +protein folding in endoplasmic reticulum Q8N766,Q8IXB1,Q86YB8,P11021 +positive regulation of viral genome replication P62937,P11940,Q6PKG0,Q9P035,P78563 +protein complex assembly P52907,O14745,Q9BSJ2,O43292,Q9Y375,Q9NU22,P30153,Q92793,P06493,O00505 +mitochondrion organization P06576,P35232,Q5VV67,O00429,Q96EY1,P09874,Q9Y276,Q99623 +T cell differentiation in thymus P61769,Q96EY1,P62753,P35222,P10415 +intra-Golgi vesicle-mediated transport P53621,Q9UBF2,P35606,P39880,P53618 +protein homotetramerization Q9H773,Q96GX9,O00429,P34897,Q14500,P35609,P14735 +calcium activated galactosylceramide scrambling A1A5B4,Q32M45,Q4KMQ2 +calcium activated phosphatidylcholine scrambling A1A5B4,Q32M45,Q4KMQ2 +viral entry into host cell P08107,P27487,P05556,P15151,P0DMV8,Q9BYF1,P15144,O00505,P14735 +DNA duplex unwinding Q13283,P12956,P13010,Q9Y265,P25205,O94762 +platelet degranulation P0DP23,P12814,P05155,P02768,O15439,P15692,O43707,P00747,P35609 +regulation of nucleic acid-templated transcription P12814,P35637,O43707,P35609 +cell proliferation O15503,P68036,Q14160,Q13561,P08581,P13010,Q16576,P06493,Q15303,P25100,Q07817,P09603,Q9BXP5,Q6PKG0,O95071,P42677,Q7L590,P12004,Q10589,P10415 +osteoblast differentiation P35998,Q9NR30,Q7KZF4,P06576,P35232,P12109,Q00839,J3QSV6,P38159 +intrinsic apoptotic signaling pathway in response to DNA damage Q07817,Q16548,Q92843,Q07820,P09601,P10415 +regulation of DNA recombination P52294,P52292,Q96FV9 +positive regulation of histone H3-K4 methylation P26358,P35222,Q9H3P2,P18615 +transport P53007,Q9Y6R1,P02768,Q9UBX3,P52948,Q15818,Q9UN86,Q9NZJ7,Q15311,P46379,P33527,P38606,Q13283,Q00325,Q02978,Q9Y3A6,Q13224,Q9H936,P48065 +positive regulation of intrinsic apoptotic signaling pathway O14745,Q07817,O00429,P63244,P10415 +7-methylguanosine mRNA capping P30876,Q09161,P52298,Q53F19,P24928 +SREBP signaling pathway O15503,Q9Y5U4,Q12770 +IRES-dependent viral translational initiation P26599,Q14152,O00303 +extrinsic apoptotic signaling pathway in absence of ligand Q07817,Q16548,Q92843,Q07820,P10415 +ribosomal small subunit assembly P08708,P39019,P42677,P46782 +ubiquitin-dependent protein catabolic process P35998,P68036,P46379,Q9UNM6,P25787,O14818,Q9NXK8,O95071,P60900,O00231,P63279,Q9NVW2 +regulation of translational initiation Q09161,P52298,O75822,Q14152,O00303 +establishment of integrated proviral latency P62937,P12956,P13010 +response to endoplasmic reticulum stress Q8IXB1,Q13438,P57088,Q9Y4L1,P16615,O76024,Q9BS26 +chaperone-mediated protein folding Q96AY3,P68400,O14656,Q9Y680,Q00688 +gastrulation P37173,P63244,P62753,P09622 +fibrinolysis P07355,P00750,P05155,P00747 +negative regulation of mRNA splicing,via spliceosome O00422,P26599,Q9UKV3,P38159 +RNA processing Q13310,Q8TDD1,Q9UHI6,P98179,Q96FV9,Q00839,P78563,Q86U42 +negative regulation of apoptotic process Q9NVI7,P31689,P02768,Q96GX9,Q92843,Q96EY1,P62753,Q86W42,P06493,Q07820,Q15303,P06731,Q07817,Q16548,P46379,Q9Y466,P11021,P15692,Q5JVF3,Q99623,P10415,P63104 +negative regulation of intrinsic apoptotic signaling pathway Q07817,Q92843,Q07820,P10415 +protein targeting P27348,Q13438,P62258,Q96RT1,P63104 +regulation of smooth muscle cell proliferation P12956,P13010,P35222 +positive regulation of RNA polymerase II transcriptional preinitiation complex assembly P35998,P62191,P62195 +response to hypoxia P22460,P00750,P37173,P27487,O00469,Q12770,P15692,Q92793,P09601,O43707,P05026 +response to drug Q9UM54,Q5JWF2,P42224,P13010,P06493,Q92820,P35222,P61769,P37173,P33527,O14980,P40939,O15439,P28300,P05023,P10415 +membrane repolarization during cardiac muscle cell action potential P62258,P05023,P05026 +endoplasmic reticulum mannose trimming Q13438,Q9BZQ6,Q9NYU1 +positive regulation of potassium ion transport P33176,P35609,Q12959 +positive regulation of cellular component movement P39019,P15692,O43707 +response to unfolded protein P34932,Q96CS3,P31689,O95757,Q9BS26 +cellular protein modification process P00750,P68036,P38435,O00469,Q86YB8,P22314,P28300,P63279 +protein peptidyl-prolyl isomerization Q6UX04,P62937,Q96AY3,Q9Y680,Q00688 +muscle contraction Q9NYL9,P0DP23,P19105,Q08043,P60660,Q14500,P50402,Q86U42,O14950 +type I interferon signaling pathway P42224,P52630,P04439,P05161,Q10589,Q14653 +positive regulation of protein ubiquitination Q9NV92,P68036,Q13155,Q96EY1,P11021,O76024 +gluconeogenesis P04406,P53007,O75746,Q9UBX3,Q02978 +mitotic nuclear envelope disassembly K7ELV2,P78406,P06493,P50402,P52948 +positive regulation of mesenchymal cell proliferation P37173,P42224,P15692,P35222 +protein targeting to plasma membrane P07355,P19105,Q92953,P35613,O14950 +positive regulation of protein targeting to mitochondrion P19784,P48556,Q8TAD8,P68036,Q9H270,P55786,Q9Y265 +nuclear pore complex assembly P57088,Q9NQC3,P52948 +positive regulation of DNA-templated transcription,elongation Q13769,Q96FV9,Q13838 +endoplasmic reticulum unfolded protein response O76061,P11021,Q9BZQ6,Q9NYU1,O76024 +ER-associated misfolded protein catabolic process Q99470,Q9NYU1,O14656 +relaxation of cardiac muscle P05023,P16615,P05026 +proteasome assembly P48556,Q9UNM6,O00231 +regulation of cardiac muscle cell contraction Q14524,P05023,P63165 +lysine catabolic process Q9UDR5,Q9BQT8,P09622 +potassium ion import Q14500,P48050,P05023,P05026 +proteolysis involved in cellular protein catabolic process P25787,O14818,P11021,P60900,P14735 +substantia nigra development P0DP23,P27348,O15027,P62258,P11021 +establishment or maintenance of epithelial cell apical/basal polarity Q7KZI7,Q96RT1,Q12959 +microtubule cytoskeleton organization Q9P0L2,P04406,Q96N67,Q7KZI7,P06493,P27448 +positive regulation of protein insertion into mitochondrial membrane involved in apoptotic signaling pathway P27348,P62258,P10415,P63104 diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt new file mode 100644 index 0000000..1f0d7e4 --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_CC_DIRECT_FILTERED.txt @@ -0,0 +1,53 @@ +membrane P04406,Q13200,O00203,P62753,Q9NXS2,P09601,Q16630,J3QSV6,P61769,P53621,Q9BSJ2,O43292,P62269,O00148,O95071,P53396,Q2PZI1,P50402,Q00839,Q8IXH7,Q8TEQ8,O14656,P10415,P05556,Q13561,Q13155,P39019,Q8TDD1,Q13011,O60884,Q9BZQ6,Q07820,P29122,Q13352,Q4KMQ2,Q9NZJ7,Q9P2E5,O60684,P11940,Q07817,P62258,P61254,Q9NU22,P38159,Q8NC51,Q9UM54,P35998,Q96A65,Q14974,O43633,P12956,P09874,P16615,P35222,P62195,P30876,Q02224,Q7KZF4,P27348,Q15311,P35232,Q9Y673,Q6PKG0,Q12789,P11021,Q5JWF2,O94826,P35613,Q14152,P07195,Q6DN90,P06576,P50395,P09603,P33527,O75400,Q9Y3U8,P53618,P07355,O14745,P31689,Q9H488,Q99519,Q9UL15,P39656,P30153,Q9Y6E2,Q99460,Q96S66,P11586,P27487,O75592,Q9H307,O00303,Q9Y265,P05023,P09651,P05026,P51570,Q96CW5,Q9UHI6,P04439,Q99470,Q7Z2K6,Q96S52,P26599,P20700,P47756,Q00325,P15692,P51665,P61158,P62191,P19338,Q15365,P27105,P06493,P33176,Q8IXB1,O95373,P46379,O15439,Q14739,P52292,O00429,P38919,O43765,P46782,Q99623,Q16891,P60866,Q9NR30,Q9C0B5,P60660,P13010,P57678,P25205,P08708,Q6P2E9,P62937,Q99615,Q8NEW0,O60762,P38435,P12109,Q9UNM6,O14980,P25705,Q7KZI7,Q9Y4L1,O43852,O00231,Q08431,P49069,Q6NXT4,Q10589 +nucleoplasm P48556,Q13200,Q13769,Q5H9R7,Q3ZCQ8,P53778,P62753,Q16630,Q9NTJ3,P0DP23,Q9BSJ2,P62269,O00148,O95071,P53396,Q7L590,Q00839,Q8IXH7,Q96J01,Q86WX3,Q13151,Q9BRP8,P39019,P39880,Q6NZY4,Q86W42,Q07820,O60885,Q13352,O60684,Q6IEG0,Q9UKV3,P52630,P67809,Q15008,Q9H936,P38159,Q9UM54,P35998,Q00403,Q9Y580,Q14974,P26358,Q14160,P12956,Q92793,P09622,Q96FV9,Q15303,P09874,P35222,P62195,P78563,P30876,P35232,Q6PKG0,Q12789,Q96MM7,Q92900,P63279,O94762,P63104,Q8WX92,Q86VM9,Q9BUQ8,Q92994,Q8N5P1,Q9H3P2,Q9NVW2,P63165,Q14653,Q13838,Q9NQG5,O43818,Q9BXP5,Q9H814,O95347,Q9Y466,O75400,O43592,Q9UER7,P61326,Q09161,P42224,Q99460,Q16576,P18615,Q6PJT7,Q9NWV8,Q5VV67,Q9H307,Q9Y265,P09651,O00629,P52294,Q9BQ95,O95619,P62140,P52298,Q8N0Z6,P49756,Q9UHI6,Q96RT1,P14735,P24928,P26599,P20700,O14818,P35637,Q9Y5S9,Q8NI27,P51665,Q96HW7,P60900,P62191,P19338,Q15365,P19784,Q7Z4H7,P25787,P67870,P06493,Q9ULX6,P68400,O00422,O95373,P08107,P83916,P46379,Q8NEM0,P52292,O75934,P98179,P38919,P12004,P05161,P46782,Q86U42,P60866,Q6I9Y2,Q9UKS6,Q9NR30,Q8TAD8,P13010,P57678,O00505,P52948,P25205,P08708,Q6P2E9,Q99615,Q14011,Q9UNM6,O15397,O14980,P0DMV8,P42677,P08621,Q86U32,O00231 +pseudopodium P12814,Q08043,P55085,O43707,P35609 +brush border P12814,P19105,Q08043,P47756,P60660,O00429,O43707,P61158,O14950 +nuclear matrix O95619,P20700,O75400,O43592,P60900,Q9ULX6,Q96FV9,Q9Y265,Q99623,Q13838 +zona pellucida receptor complex P49368,P17987,P48643,Q99832 +extracellular matrix P07355,P60866,P40227,P04406,P15924,P60660,P39019,P29122,P08708,P06576,P00750,P12109,P62269,P40939,P25705,P11021,Q00839,Q08431,P46782 +cytosolic small ribosomal subunit P60866,P08708,P62269,P39019,P42677,P62753,P46782 +actin filament P12814,Q08043,Q9H270,P47756,Q7KZI7,Q96EY1,P13797,P35609 +cytosolic proteasome complex P35998,P62191,P14735,P62195 +sarcolemma P07355,Q14315,P05556,P12109,Q14524,P35613,P05023,P05026,Q12959 +vesicle P07355,P0DP23,P27105,P33176,P50395,O14745,Q8NEW0,P04406,Q13561,Q9ULV4,P60660 +platelet alpha granule lumen P12814,P05155,P02768,P15692,O43707,P00747,P35609 +perinuclear region of cytoplasm P07355,Q9UM54,P27105,O14745,P04406,P31689,P42224,Q9UL15,O76061,P62753,P09601,P16615,P35222,P33176,Q9NV92,P08107,O00429,O43707,Q5JWF2,P05556,Q99569,P25205,Q8NEW0,P09603,P22460,P35637,P0DMV8,P63244,Q9P289,Q8NC51,Q12959 +spliceosomal complex P49756,O00148,O75934,P08621,Q9NW13,Q9UKA9,O75940,P09651,Q13838 +endoplasmic reticulum quality control compartment Q13438,Q96DZ1,Q9BZQ6,Q9NYU1 +membrane raft P07355,P27105,O14745,P22460,P37173,P27487,P05556,P35613,P31431,P35052,Q9BYF1,P07195,Q10589,Q12959 +Z disc Q14315,P12814,P19105,P22460,Q08043,P47756,Q14524,O43707,P35222,P35609,O14950 +COPI vesicle coat P53621,Q9UBF2,P35606,P53618 +small ribosomal subunit P60866,P62269,P63244,P62753,P46782 +mitochondrial nucleoid P06576,Q9NVI7,Q7L8L6,P40939,P34897,Q96EY1 +cell surface P07355,P05556,Q96PE1,Q14524,P08581,P04439,Q9BYF1,P29122,P14735,Q4KMQ2,Q13443,P06576,P00750,P35232,P27487,P15151,P31431,P11021,P15692,Q13224,Q00839,P00747,Q10589,P34741,Q9BS26,Q99623 +CRD-mediated mRNA stability complex P67809,O75534,Q00839 +endoplasmic reticulum-Golgi intermediate compartment O95070,P11021,Q9Y3A6,P15144,Q9NYU1,Q9BS26,P53618 +microtubule P40227,Q5SW79,Q96CW5,Q7Z4H7,P17987,Q13561,Q14152,Q99832,P49368,P33176,Q02224,Q9BSJ2,P48643,O00429,O60333,P50402,Q12959 +endocytic vesicle Q9UM54,P33176,Q9H270,P27487,P35247,P51148 +cytoplasmic stress granule P11940,Q14011,Q13310,Q13283,P67809 +proteasome regulatory particle,lid subcomplex P48556,Q9UNM6,O00231 +proteasome core complex,alpha-subunit complex P25787,O14818,P60900 +nuclear proteasome complex P35998,P62191,P62195 +mitochondrial matrix Q96EY1,P34897,O75439,Q07820,Q15303,P09622,Q8IZ52,Q9UDR5,Q07817,P06576,P11310,Q5JRX3,Q9BW92,Q10713,P25705,P13804,Q6L8Q7 +cytoplasmic mRNA processing body P35998,Q6P2E9,P25787,Q9Y5A9,P60900,Q9HCE1,Q92900 +lysosomal membrane P07355,Q9UM54,Q8IWA5,O75348,Q99519,O00203,P22314,Q96JC1,P51148,B7Z2Y1,P12109,P27487,Q9H270,P38606,P15144 +intracellular membrane-bounded organelle P35998,P04406,O14745,Q99519,P39656,Q96EY1,Q96S66,O14964,Q9NV92,P46379,O00429,O95070,Q9Y265,P05023,Q8N0Z8,P25205,P51148,Q9UDR5,Q6P2E9,O60762,Q9H814,P67809,O14980,Q15365,P53618 +mitochondrial outer membrane Q07817,Q16548,O94826,Q92843,O00429,Q86UT6,Q07820,Q99623,P10415,Q8N0X7 +integral component of endoplasmic reticulum membrane Q9NZ01,O00124,Q14739,P11021,P57088,Q9P035,Q9NQC3,O76024 +desmosome P15924,Q99569,P22314,Q9H307 +centrosome Q6NXT6,O14745,Q5SW79,Q96CW5,Q8TF76,Q7Z4H7,P17987,Q13561,P06493,P35222,P25205,Q9NQG5,P0DP23,Q07817,Q9H814,Q9BSJ2,P48643,Q8IWR1,Q9P289,P12004 +fascia adherens P12814,P15924,P35222 +cytoplasmic ribonucleoprotein granule P11940,P62753,Q00839,P19338 +caveola P22460,P37173,Q14524,P09601,P05023,P05026 +endoplasmic reticulum chaperone complex Q8IXB1,P11021,Q9Y4L1 +AP-3 adaptor complex Q9H270,O00203,Q96JC1 +DNA-directed RNA polymerase II,holoenzyme Q9NQG5,Q9UM54,O94762 +nuclear euchromatin P16402,P35222,P24928,P38159 +PcG protein complex P19784,Q9BSM1,P67870,P68400 +ruffle P07355,P12814,Q5JWF2,Q9UM54,O14745,P05556,Q14155 +ribosome P60866,P08708,Q86WX3,Q92552,P62269,P39019,P42677,P61927,P62753,Q9Y3U8 +transport vesicle Q5JWF2,P53621,Q9UBF2,Q9H8W4,P35606,Q15818,P53618 +cell-cell junction P07355,Q9NYL9,P12814,Q99569,Q14160,Q9H307,O43707,P61158,P35222,Q12959 +RNA cap binding complex Q09161,Q53F19 +Scrib-APC-beta-catenin complex Q14160,P35222 +proteasome storage granule Q13200,Q99460 diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt new file mode 100644 index 0000000..f576830 --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Gene_Ontology_GOTERM_MF_DIRECT_FILTERED.txt @@ -0,0 +1,33 @@ +poly(A) RNA binding P62753,J3QSV6,Q16630,Q13283,P62269,O00148,Q02978,O43707,Q00839,Q86WX3,Q13151,P68036,Q9BRP8,P17987,P39019,Q8TDD1,Q6NZY4,P11940,Q9UKV3,P62258,P56270,P16402,P67809,P61254,P63244,Q8NC51,P38159,P40227,Q9Y580,Q14974,P12956,P09874,Q00688,P78563,P30876,Q7KZF4,Q6PKG0,P63279,Q92900,P63104,Q7L8L6,Q8N5P1,Q9BUQ8,Q86VM9,Q14152,P63165,Q9UN86,Q13838,Q8WTT2,O43818,P50395,Q9BXP5,O75400,P61326,Q96A72,Q9Y3U8,P07355,Q09161,Q9Y5A9,P31948,P18615,Q6PJT7,Q9H307,Q5VV67,Q8NCA5,P09651,P15924,Q13310,P20290,P52298,P22314,P49756,P04439,P24928,P26599,P35637,Q9Y5S9,Q9NW13,P19338,P62191,Q15365,P04003,O75534,Q9ULX6,Q9HCE1,Q53F19,O00422,P52292,Q14739,P98179,P38919,Q9UKA9,P46782,Q86U42,Q16891,P60866,Q8TAD8,Q9NR30,P13010,P08708,P62937,P49368,Q14011,P25705,Q7KZI7,P42677,P08621,Q9NQC3,Q86U32,O75940,Q10589 +protein binding Q9P021,Q13200,Q96GX9,Q13769,Q5H9R7,Q3ZCQ8,Q99988,Q86VR2,P62753,Q9NTJ3,P0DP23,P61769,Q9BSJ2,O43292,P62269,Q8TBB1,P59998,P50402,Q8IXH7,Q00839,P34741,Q99569,Q13561,Q07820,P55198,Q13352,Q4KMQ2,Q9NZJ7,Q96GG9,Q9H3K2,Q07817,Q9UKV3,P52630,P62258,Q86YB8,Q8NC51,P35998,P40227,Q00403,P26358,Q96PE1,P35606,Q92793,Q96FV9,Q15303,P09874,P35609,Q9Y680,Q00688,P78563,Q13443,Q15311,P27348,Q6PKG0,Q12789,P11021,Q92900,P63104,Q9ULJ7,Q8WX92,Q7L8L6,P35613,Q9BUQ8,Q9UBX3,Q9P035,P25100,Q13838,Q9NQG5,P06576,Q9Y5U4,Q13438,Q9BXP5,O60333,P55085,Q13224,Q9NS75,P53618,Q09161,O14745,Q9P0K1,Q9UL15,Q9Y5A9,P31948,P30153,Q9H773,Q6PJT7,O75592,Q8NCA5,Q9Y3A6,P57088,O00303,P05023,P09651,P05026,Q9NZ01,P52294,Q9BQ95,Q96CW5,O95619,Q13310,P20290,P52298,Q8N0Z6,P49756,P14735,P24928,P37173,Q9H270,Q9Y5S9,P51665,O00192,P60900,Q96HW7,P00747,P19338,Q15365,Q12959,P27105,Q9BSM1,P25787,P04003,Q9UQB3,Q14CM0,P81408,P67870,Q9HCE1,P68400,Q8IXB1,P33176,O00422,P00750,Q96IZ7,P08107,P46379,P83916,Q8NEM0,Q96DZ1,P48643,O00429,P52292,P05161,O43765,P46782,Q99623,Q9UKS6,Q9NR30,O75348,P52948,P51148,Q8N0X7,P49368,Q99615,P05155,O15397,P0DMV8,P08621,Q6ZTQ3,Q9NQC3,O43852,Q10589,O76024,P25440,P04406,Q9ULV4,Q14524,Q96EY1,P53778,P35247,P09601,Q16630,Q5BJF2,Q9H845,O15027,Q13283,P15151,Q9NXK8,O00148,O95071,O95070,P53396,Q7L590,O43707,O14656,P10415,Q05639,Q86WX3,P68036,Q9BRP8,Q9Y6R1,P05556,O75822,P17987,Q13155,P39019,Q00765,Q13011,Q15293,P48668,O60884,Q6NZY4,O60885,O60684,P11940,P61254,P56270,P67809,Q9NU22,Q15008,P63244,Q96HR9,P31150,P48065,P38159,Q9BS26,Q9H173,Q9UM54,P34932,Q96A65,Q14974,O43633,Q96IV0,P12956,Q14160,Q96PV6,P16615,P35222,Q99832,P62195,P12814,P30876,P19105,Q9H078,Q02224,P52907,Q7KZF4,P35232,Q9Y375,O95816,Q14257,P63279,Q5JWF2,O94826,Q8N5P1,Q86VM9,Q14152,P07195,Q9H3P2,P63165,Q9UN86,Q14155,Q6DN90,Q14653,Q96NW7,P09603,P50395,Q9H814,O95347,P40939,P31431,O75400,Q8IWR1,Q9UER7,P61326,Q96A72,P07355,P31689,P42224,K7ELV2,P39656,Q16576,Q9BYF1,P11586,P18615,P27487,Q9NWV8,Q7Z5G4,Q9H8W4,P13804,Q9Y6M9,Q8N8U2,Q9Y265,Q96SF7,O00629,Q5SW79,P15924,P62140,P22459,P22314,Q9UHI6,Q99471,Q9UH99,P04439,Q96RT1,O14950,Q96S52,P22460,P26599,O14818,P20700,P35637,Q12770,Q8NI27,Q9H4F8,Q86UT6,P15692,Q9P289,P61158,P62191,Q9Y276,P19784,Q8IV08,Q96CS3,O15503,P08581,Q92843,O75534,P06493,Q9ULX6,P61916,Q53F19,O14964,O95373,Q9NV92,Q16548,Q96F46,O00124,Q14739,P28300,O75934,P98179,P48050,P38919,P12004,Q86U42,Q16891,P60866,Q8IWF2,Q6I9Y2,Q8TAD8,Q08043,Q8TF76,P02768,P60660,P13010,P34897,P57678,Q9NYU1,O00505,P27448,P25205,Q14315,Q6P2E9,P62937,O60762,Q14011,Q92552,O14980,P25705,Q7KZI7,P42677,Q96K17,Q5JVF3,O00231,Q86U32,O75940,P49069 +protein domain specific binding Q14974,O43633,Q14524,Q9UHI6,P67870,P35609,O14964,P0DP23,P27348,Q9H270,P62258,O14980,P11021,P05023,P00747,P63104 +ion channel binding P0DP23,P12814,P27348,Q08043,P62258,Q14524,O43707,P35222,P63165,P35609,Q12959 +Hsp70 protein binding Q8IXB1,P31689,P46379,P31948,Q96EY1,P06493 +glycoprotein binding Q8IWF2,P61769,P00750,Q13438,Q96DZ1,P11021,Q9BYF1,P14735 +TBP-class protein binding P35998,Q00403,Q92994,P62191,P62195 +estrogen receptor binding P35637,Q8TDD1,P09874,P12004,P35222,Q99623 +misfolded protein binding Q8IXB1,P46379,P11021,O14656 +protein transporter activity O00629,P52294,O60684,O95373,Q14974,O15397,P52292,O00505 +BH3 domain binding Q07817,Q07820,P10415 +actin filament binding Q14315,P12814,Q9UM54,P47756,Q9ULV4,P59998,O43707,P61158,P13797,Q8NHY3,P35609 +poly(A) binding P11940,Q6PJT7,Q13310,P38919 +identical protein binding P04406,P42224,Q96GX9,P67870,P09874,Q99832,P35609,P61769,Q9H773,P11310,P27487,Q8NEM0,O95816,O00429,O00148,Q7L590,P12004,P10415,P63104,P02768,Q13561,Q9UH99,P34897,P07195,Q13838,Q14653,P06731,Q07817,O14818,P52630,P35637,P15692,Q9P289,P19338,P38159 +Ran GTPase binding O95373,Q14974,O15397,O14980,O43592 +integrin binding Q13443,P12814,Q08043,Q9P0K1,P05556,O43707,Q96RT1,Q08431,P35609 +proteasome-activating ATPase activity P35998,P62191,P62195 +dolichyl-phosphate-mannose-protein mannosyltransferase activity O60762,Q9Y673,Q99470 +cytoskeletal protein binding P07355,Q14315,Q9UKS6,O14656,P35609,Q12959 +oxidoreductase activity,acting on the CH-CH group of donors Q9NZ01,Q9H845,P11310,Q14739 +virus receptor activity P08107,P27487,P05556,P15151,P0DMV8,Q9BYF1,P15144,P14735 +protein N-terminus binding P19784,Q96A65,P27348,Q9UER7,O43707,P09874,P68400,Q99623 +protein homodimerization activity P27105,P42224,Q92843,O76061,P09601,Q15303,P35609,P12814,Q16548,P83916,P27487,P15151,O00429,O43707,Q96SF7,P10415,Q08043,P39019,Q07820,P14735,Q14653,Q4KMQ2,P06731,Q07817,P09603,P63244,P15692,P51665,Q9UER7,Q9P289,Q10589,Q6UY11 +NAD binding P04406,P40939,P07195,P09622,P09874 +protein kinase binding Q05639,Q13151,P62140,Q14524,Q9UL15,P39019,Q96EY1,P62753,P09874,P35222,Q14155,P0DP23,Q07817,P22460,P62269,Q9UER7,P05023,P63104,Q12959 +rRNA binding Q9NR30,Q7L8L6,P62269,P61927,P46782 +histone deacetylase binding P35232,P08107,P62258,P0DMV8,P52292,Q9UHI6,Q9Y466,Q9ULX6,P09874 +telomeric DNA binding P12956,P13010,P19338,Q92900 +RNA cap binding Q09161,Q6PKG0,P52298 +chromatin binding P25440,P26358,Q8N0Z6,Q9ULI0,P67870,P34897,Q92793,P06493,O60885,Q9H3P2,P18615,P30876,Q8WTT2,P83916,P67809,Q92900,P12004,Q6PML9,P38159 +cation transmembrane transporter activity Q8NEW0,Q6NXT4,Q6PML9 +phospholipid scramblase activity A1A5B4,Q32M45,Q4KMQ2 +peptidyl-prolyl cis-trans isomerase activity Q6UX04,P62937,Q96AY3,Q9Y680,Q00688 \ No newline at end of file diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt new file mode 100644 index 0000000..6571ad7 --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Pathways_KEGG_PATHWAY_FILTERED.txt @@ -0,0 +1,4 @@ +Viral carcinogenesis Q00403,Q08043,Q14160,P04439,Q96EY1,Q92793,P06493,P35609,Q14653,P12814,P27348,Q7KZF4,P62258,O43707,P62191,P63104,Q12959 +Influenza A P52294,P42224,P78406,Q92793,P53778,P52948,Q13838,Q14653,P08107,P52630,P0DMV8,O14980,P52292,Q86UT6,P00747,Q86U42 +Herpes simplex infection P19784,P42224,P62140,P67870,P04439,Q92793,P06493,P68400,P24928,Q14653,P52630,Q9UER7,Q86U32 +Proximal tubule bicarbonate reclamation Q9Y6R1,Q9UBX3,P05023,P05026 diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt new file mode 100644 index 0000000..21ddc96 --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_INTERPRO_FILTERED.txt @@ -0,0 +1,58 @@ +P-loop containing nucleoside triphosphate hydrolase Q9UM54,P35998,Q9NVI7,Q9ULI0,Q5T9A4,Q9HCE1,Q9NTJ3,P11586,P62195,Q02224,P33176,Q9H078,P38606,O00429,O15439,O00148,Q00839,Q9Y265,P38919,Q92900,Q96MM7,O14656,O94762,Q05639,Q5JWF2,Q9NR30,Q8TDD1,Q9UHI6,Q9BUQ8,Q13838,P51148,P25205,P06576,P33527,O95347,Q9NU22,P25705,O60333,P62191,Q9Y276,Q12959 +EF-hand-like domain Q08043,P42224,P60660,Q15293,Q07820,P35609,Q9Y680,O14950,P0DP23,P12814,P19105,P52630,O75746,Q9H4F8,Q96AY3,O43707,O43852,Q14257,P13797 +Apoptosis regulator,Bcl-2,BH4 motif,conserved site Q07817,Q92843,P10415 +ATPase,F1/V1/A1 complex,alpha/beta subunit,C-terminal P06576,P38606,P25705 +Apoptosis regulator,Bcl-2 protein,BH4 Q07817,Q92843,P10415 +DNA/RNA helicase,DEAD/DEAH box type,N-terminal Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762 +Filamin/ABP280 repeat-like Q14315,O75592,Q7Z4H8,Q6UW63 +Chaperone DnaJ P31689,Q96EY1,O60884 +Calponin homology domain Q14315,P12814,Q08043,O43707,P13797,Q8NHY3,P35609,Q14155 +RNA recognition motif domain,eukaryote P11940,Q14011,Q13310,P19338,P38159 +ATPase,AAA-type,conserved site P35998,Q9ULI0,P62191,Q9Y276,P62195 +Peptidase M16,zinc-binding site Q10713,O75439,P14735 +Kinase associated domain 1 (KA1) Q9P0L2,Q7KZI7,P27448 +ATPase,alpha/beta subunit,N-terminal P06576,P38606,P25705 +Heat shock protein DnaJ,cysteine-rich domain P31689,Q96EY1,O60884 +ATPase,F1/V1/A1 complex,alpha/beta subunit,nucleotide-binding domain P06576,P38606,P25705 +ATPase,alpha/beta subunit,nucleotide-binding domain,active site P06576,P38606,P25705 +Spectrin/alpha-actinin P12814,Q08043,P15924,O43707,P35609 +Immunoglobulin E-set Q14315,Q96DL1,O75592,P08581,Q14500,Q7Z4H8,P48050,P61916,Q6UW63 +EF-Hand 1,calcium-binding site P0DP23,P12814,P19105,O75746,Q9H4F8,Q15293,Q96AY3,O43707,O43852,P13797,Q14257,Q9Y680,O14950 +EF-hand domain Q08043,P60660,Q15293,P35609,Q9Y680,O14950,P12814,P0DP23,P19105,O75746,Q96AY3,O43707,O43852,P13797,Q14257 +26S proteasome subunit P45 P35998,P62191,P62195 +G-protein beta WD-40 repeat O43818,P53621,K7ELV2,P78406,P35606,P63244,Q16576,Q96J01 +14-3-3 protein P27348,P62258,P63104 +14-3-3 protein,conserved site P27348,P62258,P63104 +14-3-3 domain P27348,P62258,P63104 +Apoptosis regulator,Bcl-2,BH3 motif,conserved site Q07817,Q07820,P10415 +Proteasome A-type subunit P25787,O14818,P60900 +Proteasome,alpha-subunit,N-terminal domain P25787,O14818,P60900 +Chaperone DnaJ,C-terminal P31689,Q96EY1,O60884 +HSP40/DnaJ peptide-binding P31689,Q96EY1,O60884 +Helicase,C-terminal Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762 +Cation efflux protein Q8NEW0,Q6NXT4,Q6PML9 +Cation efflux protein transmembrane domain Q8NEW0,Q6NXT4,Q6PML9 +Filamin/ABP280 repeat Q14315,Q7Z4H8,Q6UW63 +Spectrin repeat P12814,Q08043,O43707,P35609 +Helicase,superfamily 1/2,ATP-binding domain Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762 +Bromodomain,conserved site P25440,Q9ULI0,Q92793,O60885 +Anoctamin/TMEM 16 A1A5B4,Q32M45,Q4KMQ2 +RNA helicase,ATP-dependent,DEAD-box,conserved site Q8TDD1,Q9BUQ8,Q9UHI6,P38919 +Mitochondrial carrier protein P53007,O75746,Q9BQT8,Q9H936 +Tetratricopeptide TPR-1 Q99615,O94826,P31948,O43765 +Band 7 protein P27105,P35232,Q99623 +WD40-repeat-containing domain K7ELV2,Q9ULV4,P78406,P35606,Q86W42,Q16576,Q96JC1,O43818,B7Z2Y1,P53621,Q6P2E9,Q9H270,Q12770,P63244,Q96J01 +von Willebrand factor,type A Q9NWV8,P05556,P12109,P12956,Q9NU22,P13010,P10155 +DnaJ domain Q8IXB1,Q99615,P31689,Q96EY1,O60884 +Proteasome/cyclosome,regulatory subunit Q13200,Q99460 +Apoptosis regulator,Bcl-2/ BclX Q07817,P10415 +Transcription factor TFIIB,cyclin-like domain Q00403,Q92994 +Insulin-induced protein family O15503,Q9Y5U4 +Coatomer,WD associated region P53621,P35606 +Ku70/Ku80 C-terminal arm P12956,P13010 +Ku70/Ku80,N-terminal alpha/beta P12956,P13010 +Ku70/Ku80 beta-barrel domain P12956,P13010 +Rab GDI protein P50395,P31150 +ATPase,F1 complex beta subunit/V1 complex,C-terminal P06576,P38606 +Mago nashi protein P61326,Q96A72 +Clathrin/coatomer adaptor,adaptin-like,N-terminal Q9UBF2,O00203,P53618 \ No newline at end of file diff --git a/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_SMART_FILTERED.txt b/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_SMART_FILTERED.txt new file mode 100644 index 0000000..c464f8e --- /dev/null +++ b/www/Examples/IntAct_COVID19/HomoSapiens_Protein_Domains_SMART_FILTERED.txt @@ -0,0 +1,12 @@ +SM00265:BH4 Q07817,Q92843,P10415 +SM00033:CH Q14315,P12814,Q08043,O43707,P13797,Q8NHY3,P35609,Q14155 +SM00361:RRM_1 P11940,Q14011,Q13310,P19338,P38159 +SM00150:SPEC P12814,Q08043,P15924,O43707,P35609 +SM00088:PINT Q9UNM6,Q15008,Q14152,O00231 +SM00101:14_3_3 P27348,P62258,P63104 +SM00948:SM00948 P25787,O14818,P60900 +SM00557:IG_FLMN Q14315,Q7Z4H8,Q6UW63 +SM00490:HELICc Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762 +SM00487:DEXDc Q9NR30,Q8TDD1,Q9BUQ8,Q9UHI6,O00148,P38919,Q13838,O94762 +SM00244:PHB P27105,P35232,Q99623 +SM00271:DnaJ Q8IXB1,Q99615,P31689,Q96EY1,O60884 diff --git a/www/Examples/IntAct_COVID19/Intact-data_COVID19_no_self_loops.txt b/www/Examples/IntAct_COVID19/Intact-data_COVID19_no_self_loops.txt new file mode 100644 index 0000000..69e87de --- /dev/null +++ b/www/Examples/IntAct_COVID19/Intact-data_COVID19_no_self_loops.txt @@ -0,0 +1,1094 @@ +Source Target +P31809 P11224 +P33767 P41811 +Q3T133 P41811 +P39656 P41811 +P41811 O15503 +Q91AV1 P41811 +Q96WV5 P68978 +Q9Y5U4 P41811 +P53622 P41811 +P53622 Q3T133 +P53622 P41811 +P53622 P33767 +P53622 P41811 +P53622 P68978 +P53622 P41811 +P53622 P39656 +Q3T133 Q96WV5 +Q96WV5 P39656 +Q96WV5 P33767 +Q96WV5 P68978 +P53622 P41811 +P53622 Q9Y5U4 +P53622 P41811 +P53622 Q91AV1 +P53622 P41811 +P53622 O15503 +Q96WV5 Q9Y5U4 +Q96WV5 O15503 +Q91AV1 Q96WV5 +P53622 P41811 +P53622 P41811 +P53622 P41811 +P53622 P41811 +P53622 P41811 +P27487 K0BRG7 +P27487 K0BRG7 +Q9BYF1-1 P59594 +P27487 K0BRG7 +Q9BYF1-1 P59594 +P27487 K0BRG7 +P27487 K0BRG7 +P0C6U8 P62753 +Q9BYF1-1 Q6Q1S2 +Q9BYF1-1 Q6Q1S2 +Q9BYF1-1 Q6Q1S2 +Q7TLC7 O14964 +Q7TLC7 P05155 +Q7TLC7 Q13561 +Q7TLC7 Q9NRH1 +J9TC74 Q13561 +J9TC74 P62258 +J9TC74 Q99471 +P59633 Q13561 +P59633 P05155 +P59633 P02768 +Q6S8E0 P08708 +Q6S8E0 P25787 +P59634 Q13561 +P59634 Q92994 +Q19QW4 Q9BQB6 +Q19QW4 Q9H4F8 +Q7T6S2 P49069 +Q7T6S2 P05155 +Q6S8E0 P60866 +Q6S8E0 P27448 +Q19QW2 P46379 +Q6S8E0 P60866 +Q19QW2 P46379 +P59634 Q13561 +Q7T6S2 P49069 +Q19QW4 Q9BQB6 +Q6S8E0 P27448 +Q6S8E0 P25787 +Q7TLC7 O14964 +Q7TLC7 Q13561 +Q1HVL3 P15144 +Q1HVL8 P15144 +Q1HVK9 P15144 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +K9N5Q8 P27487 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P59594 +P61769 P04439 +P61769 P59595 +P61769 P04439 +P61769 P03407 +P61769 P04439 +P61769 P59595 +P61769 P04439 +P61769 P59596 +P61769 P04439 +P61769 Q692E0 +P61769 P04439 +P61769 P59596 +P61769 P04439 +P61769 Q76R37 +P61769 P04439 +P61769 P59637 +Q9BYF1 P0DTC2 +Q9BYF1 P0DTC2 +Q9BYF1 P0DTC2 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P0DTC2 +Q9BYF1 Q695T7 +Q9BYF1 Q695T7 +Q9BYF1 P0DTC2 +Q9BYF1 Q695T7 +Q9BYF1 P37173 +Q9BYF1 P37173 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +P11021 K0BRG7 +K0BRG7 P11021 +P0DTC4 Q8IWA5 +P0DTC4 Q86VM9 +P0DTC4 Q6UX04 +P0DTC4 P25440 +P0DTC4 O60885 +P0DTC4 O00203 +P0DTC9 Q9Y3U8 +P0DTC9 Q9NW13 +P0DTC9 Q9NR30 +P0DTC9 Q9UN86 +P0DTC9 Q9HCE1 +P0DTC9 Q92900 +P0DTC9 Q8TAD8 +P0DTC9 Q8NCA5 +P0DTC9 Q6PKG0 +P0DTC9 Q13310 +P0DTC9 Q13283 +P0DTC9 P67870 +P0DTC9 P19784 +P0DTC9 P11940 +P0DTC9 O43818 +P0DTC2 Q9C0B5 +P0DTC2 Q7Z5G4 +P0DTC5 Q9Y6E2 +P0DTC5 Q9Y312 +P0DTC5 Q9ULX6 +P0DTC5 Q9UDR5 +P0DTC5 Q9UBU6 +P0DTC5 Q9NQC3 +P0DTC5 Q9BW92 +P0DTC5 Q9BSJ2 +P0DTC5 Q9BQT8 +P0DTC5 Q96HW7 +P0DTC5 Q96HR9 +P0DTC5 Q96ER3 +P0DTC5 Q96D53 +P0DTC5 Q96CW5 +P0DTC5 Q8NEW0 +P0DTC5 Q7L8L6 +P0DTC5 Q6PML9 +P0DTC5 Q5JRX3 +P0DTC5 Q4KMQ2 +P0DTC5 Q10713 +P0DTC5 Q00765 +P0DTC5 P48556 +P0DTC5 P38606 +P0DTC5 P38435 +P0DTC5 P27105 +P0DTC5 P13804 +P0DTC5 P11310 +P0DTC5 P05026 +P0DTC5 O95070 +P0DTC5 O75439 +P0DTC6 Q9NZJ7 +P0DTC6 P78406 +P0DTC6 P52948 +P0DTC3 Q9Y673 +P0DTC3 Q9UH99 +P0DTC3 Q9H270 +P0DTC3 Q96S66 +P0DTC3 Q96JC1 +P0DTC3 Q8N6S5 +P0DTC3 Q8IWR1 +P0DTC3 P09601 +P0DTC7 Q9NU22 +P0DTC7 Q7Z4Q2 +P0DTC8 Q96IV0 +P0DTC8 Q8N0Z8 +P0DTC8 Q96AY3 +P0DTC8 Q13443 +P0DTC8 Q9Y680 +P0DTC8 Q9Y4L1 +P0DTC8 Q9UHI8 +P0DTC8 Q9P2E5 +P0DTC8 Q9NYU1 +P0DTC8 Q9NXK8 +P0DTC8 Q9H8W4 +P0DTC8 Q9H4F8 +P0DTC8 Q9H488 +P0DTC8 Q9H173 +P0DTC8 Q9BZQ6 +P0DTC8 Q9BS26 +P0DTC8 Q9BRN9 +P0DTC8 Q99988 +P0DTC8 Q99519 +P0DTC8 Q99470 +P0DTC8 Q96MM7 +P0DTC8 Q96F46 +P0DTC8 Q96DZ1 +P0DTC8 Q92820 +P0DTC8 Q8N766 +P0DTC8 Q8IZ52 +P0DTC8 Q8IWF2 +P0DTC8 Q8IV08 +P0DTC8 Q86YB8 +P0DTC8 Q7Z4H8 +P0DTC8 Q6UW63 +P0DTC8 Q15818 +P0DTC8 Q13438 +P0DTC8 Q08431 +P0DTC8 P61916 +P0DTC8 P58166 +P0DTC8 P29122 +P0DTC8 P28300 +P0DTC8 P26358 +P0DTC8 P15151 +P0DTC8 P12109 +P0DTC8 P0C7P0 +P0DTC8 P05556 +P0DTC8 P00750 +P0DTC8 O76061 +P0DTC8 O14656 +P0DTC8 O00469 +P0DTD2 Q9UL15 +P0DTD2 Q9UKA9 +P0DTD2 Q9P0L2 +P0DTD2 Q9H773 +P0DTD2 Q9H2P9 +P0DTD2 Q7KZI7 +P0DTD2 P27448 +P0DTD2 O94826 +P0DTD2 O75534 +P0DTD2 O43633 +P0DTD2 O14745 +P0DTD3 Q9Y3A6 +P0DTD3 Q2PZI1 +P0DTD3 Q96S52 +P0DTD3 Q9Y6M9 +P0DTD3 Q9Y375 +P0DTD3 Q9Y276 +P0DTD3 Q9NV92 +P0DTD3 Q9H845 +P0DTD3 Q9H3K2 +P0DTD3 Q9GZU3 +P0DTD3 Q9BVK2 +P0DTD3 Q9BQ95 +P0DTD3 Q96K12 +P0DTD3 Q8TEQ8 +P0DTD3 Q86VR2 +P0DTD3 Q86UT6 +P0DTD3 Q7Z2K6 +P0DTD3 Q6NXT6 +P0DTD3 Q6NXT4 +P0DTD3 Q5BJF2 +P0DTD3 Q12770 +P0DTD3 P55085 +P0DTD3 P33527 +P0DTD3 O76024 +P0DTD3 O43292 +P0DTD3 O00124 +Q1LZX8 P59594 +Q1LZX8 P84198 +Q1LZX8 P84198 +Q1LZX8 P59594 +P84198 P59594 +P84198 P59594 +P59637 Q64373 +P59637 O35843 +Q64373 Q07817 +Q64373 P59637 +P59637 O35843 +P59637 Q07817 +P06731-1 K0BRG7 +P06731 K0BRG7 +K0BRG7 P06731 +P06731 K0BRG7 +P06731 K0BRG7 +P59637 P59636 +P59633 P59637 +Q7TFA0 P59636 +Q7TFA1 Q19QW5 +Q80H93 Q7TFA0 +P59596 P59632 +P59594 P59635 +Q7TFA1 P59637 +P59594 Q80H93 +Q7TFA1 Q80H93 +Q7TFA0 Q80H93 +Q7TLC7 P59636 +Q7TFA1 P59636 +Q80H93 P59636 +Q7TLC7 Q80H93 +Q7TFA1 P59637 +P59595 P59596 +Q7TFA1 Q19QW5 +Q7TLC7 P59636 +Q7TLC7 P59636 +Q9BYF1 P02768 +F1RG45 K7GLM4 +Q9BYF1 P05814 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q92793 Q14653 +Q92793 Q14653 +Q07817 P59635 +P59635 P10415 +Q07820 P59635 +Q16548 P59635 +P59635 Q92843 +Q9BYF1 P59594 +Q9BYF1 Q6Q1S2 +Q9BYF1 P59594 +Q9BYF1 Q6Q1S2 +Q9BYF1 Q6Q1S2 +P59594 P00747 +P59594 P00762 +Q6ZMR5 P59594 +Q9BYF1 P59594 +Q9BYF1 P59594 +Q9BYF1 P59594 +Q9BYF1 P59594 +Q9BYF1 P59594 +Q9BYF1 P59594 +P59594 Q56NL1 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P20290 Q86VG3 +Q86U32 Q86VG3 +P24928 Q86VG3 +P20290 Q86VG3 +P20290 Q86VG3 +P20290 Q86VG3 +O00303 P59594 +O00303 P59594 +O00303 P11223 +O00303 P59594 +O00303 P11223 +O00303 P59594 +O00303 P59594 +P59595 Q05639 +P03070 P02340 +P59595 Q05639 +Q05639 P59595 +P59595 Q05639 +P59595 Q05639 +P15130 Q05639 +Q05639 P59595 +Q05639 P08107 +Q05639 P59595 +Q05639 P08107 +P59595 Q05639 +P59595 Q05639 +P59595 Q05639 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P63279 +P59595 P62937 +P59595 P62937 +P35613 P62937 +Q9BYF1 P59594 +P59594 Q5EGZ1 +Q9BYF1 P59594 +P59594 Q5EGZ1 +P59594 Q56NL1 +Q9BYF1 Q5GDB5 +Q9BYF1 P59594 +Q56NL1 P59594 +Q9BYF1 P59594 +P59594 Q56NL1 +Q56NL1 Q5GDB5 +Q9BYF1 Q5GDB5 +Q9BYF1 P59594 +P0DTC2 P62269 +P0DTC2 Q96N67 +P0DTC2 P63244 +P0DTC2 P40227 +P0DTC2 P07195 +P0DTC2 P08621 +P0DTC2 P60660 +P0DTC2 P47756 +P0DTC2 P52907 +P0DTC2 Q9NYL9 +P0DTC2 P13797 +P0DTC2 Q9ULV4 +P0DTC2 P59998 +P0DTC2 P61158 +P0DTC2 Q9UM54 +P0DTC4 Q9ULX6 +P0DTC4 P50402 +P0DTC4 Q00325 +P0DTC4 P05023 +P0DTC4 O75746 +P0DTC4 P16615 +P0DTC4 Q9P035 +P0DTC4 Q5H9R7 +P0DTC5 P30876 +P0DTC5 O15397 +P0DTC5 Q9H078 +P0DTC5 P62195 +P0DTC5 P26599 +P0DTC5 O60762 +P0DTC5 Q9H936 +P0DTC5 O00231 +P0DTC5 Q9ULX6 +P0DTC5 Q9UHI6 +P0DTC5 Q7Z4H7 +P0DTC5 Q96EY1 +P0DTC5 Q00325 +P0DTC5 Q15008 +P0DTC5 O14818 +P0DTC5 P40939 +P0DTC5 P50402 +P0DTC5 Q5H9R7 +P0DTC5 O95757 +P0DTC5 P57678 +P0DTC5 P31689 +P0DTC5 O15027 +P0DTC5 P60900 +P0DTC5 P25705 +P0DTC5 P51665 +P0DTC5 P62191 +P0DTC5 P06576 +P0DTC5 Q14257 +P0DTC5 Q99615 +P0DTC5 O95816 +P0DTC5 O95071 +P0DTC5 P46379 +P0DTC5 O75746 +P0DTC5 Q9Y5A9 +P0DTC5 Q13200 +P0DTC5 P53618 +P0DTC5 Q9NZ01 +P0DTC5 P35998 +P0DTC5 Q16891 +P0DTC5 O60884 +P0DTC5 O43852 +P0DTC5 Q99460 +P0DTC5 Q9NTJ3 +P0DTC5 P53621 +P0DTC5 P55786 +P0DTC5 Q15293 +P0DTC5 P34932 +P0DTC5 Q9UBF2 +P0DTC5 O43592 +P0DTC5 P35606 +P0DTC5 P05023 +P0DTC5 O95347 +P0DTC5 P16615 +P0DTC5 O14980 +P0DTC5 Q96CS3 +P0DTC5 Q9P035 +P0DTC9 P09622 +P0DTC9 P08621 +P0DTC9 P38159 +P0DTC9 Q13151 +P0DTC9 Q9UN86 +P0DTC9 Q13283 +P0DTC9 Q9UM54 +P0DTC3 P40939 +P0DTC3 Q14257 +P0DTC3 P22314 +P0DTC3 P04406 +P0DTC3 P57088 +P0DTC3 Q8IXB1 +P0DTC3 Q00325 +P0DTC3 P31689 +P0DTC3 P25705 +P0DTC3 O43852 +P0DTC3 O95757 +P0DTC3 P46379 +P0DTC3 P53621 +P0DTC3 Q9H936 +P0DTC3 O60884 +P0DTC3 P26599 +P0DTC3 O75746 +P0DTC3 P11586 +P0DTC3 Q9NZ01 +P0DTC3 P53007 +P0DTC3 P05023 +P0DTC3 Q96CS3 +P0DTC3 O43592 +P0DTC3 O95347 +P0DTC3 P16615 +P0DTC3 Q16891 +P0DTC3 O14980 +P0DTC3 Q9P035 +P0DTC6 Q5SW79 +P0DTC6 P25705 +P0DTC6 Q92552 +P0DTC6 P06576 +P0DTC6 P15924 +P0DTC6 P53396 +P0DTC6 P16615 +P0DTC6 Q5T9A4 +P0DTC6 P05023 +P0DTC6 O75592 +P0DTC6 O75934 +P0DTC6 O14980 +P0DTC6 Q96CS3 +P0DTC6 P78406 +P0DTC6 Q9NVI7 +P0DTC6 P33176 +P0DTC6 Q13561 +P0DTC6 Q6P2E9 +P0DTC7 P30153 +P0DTC7 P06576 +P0DTC7 P40939 +P0DTC7 Q8IXB1 +P0DTC7 Q9UHI6 +P0DTC7 O95816 +P0DTC7 P53007 +P0DTC7 Q96EY1 +P0DTC7 P50402 +P0DTC7 O43852 +P0DTC7 Q9H936 +P0DTC7 P62195 +P0DTC7 P30876 +P0DTC7 Q99615 +P0DTC7 P55786 +P0DTC7 P57678 +P0DTC7 Q00325 +P0DTC7 O75746 +P0DTC7 O15027 +P0DTC7 P34932 +P0DTC7 Q15008 +P0DTC7 Q9UNM6 +P0DTC7 O95071 +P0DTC7 O60884 +P0DTC7 P62191 +P0DTC7 P05023 +P0DTC7 P51665 +P0DTC7 P53618 +P0DTC7 Q9NZ01 +P0DTC7 P35998 +P0DTC7 Q13200 +P0DTC7 P53621 +P0DTC7 Q5H9R7 +P0DTC7 P16615 +P0DTC7 Q99460 +P0DTC7 O43592 +P0DTC7 Q9NXS2 +P0DTC7 P46379 +P0DTC7 Q16891 +P0DTC7 O95347 +P0DTC7 P35606 +P0DTC7 Q9UBF2 +P0DTC7 Q9P035 +P0DTC7 O14980 +P0DTC7 Q96CS3 +P0DTD8 Q14257 +P0DTD8 O60762 +P0DTD8 O43852 +P0DTD8 P25705 +P0DTD8 P31689 +P0DTD8 P06576 +P0DTD8 Q8WTT2 +P0DTD8 Q7KZF4 +P0DTD8 Q9NVI7 +P0DTD8 P05023 +P0DTD8 P55786 +P0DTD8 Q00325 +P0DTD8 P53618 +P0DTD8 P57088 +P0DTD8 O43592 +P0DTD8 O95347 +P0DTD8 Q9NZ01 +P0DTD8 Q16891 +P0DTD8 P16615 +P0DTD8 P53621 +P0DTD8 Q9NXS2 +P0DTD8 Q96CS3 +P0DTD8 Q02978 +P0DTD8 P35606 +P0DTD8 Q9UBF2 +P0DTD8 Q9P035 +P0DTD8 O14980 +P0DTD8 P10155 +P0DTC8 Q14152 +P0DTC8 P50402 +P0DTC8 Q9UHI6 +P0DTC8 P06576 +P0DTC8 P62195 +P0DTC8 P25705 +P0DTC8 P40939 +P0DTC8 Q5JWF2 +P0DTC8 O14818 +P0DTC8 P55786 +P0DTC8 P57678 +P0DTC8 O43852 +P0DTC8 Q12789 +P0DTC8 Q9H936 +P0DTC8 P31689 +P0DTC8 Q15008 +P0DTC8 Q9UNM6 +P0DTC8 Q00325 +P0DTC8 O15027 +P0DTC8 P30876 +P0DTC8 Q7KZF4 +P0DTC8 Q9NVI7 +P0DTC8 O60884 +P0DTC8 Q13200 +P0DTC8 P05023 +P0DTC8 O95071 +P0DTC8 P35998 +P0DTC8 O43592 +P0DTC8 Q02978 +P0DTC8 Q9NZ01 +P0DTC8 P53618 +P0DTC8 Q99460 +P0DTC8 O75746 +P0DTC8 P16615 +P0DTC8 P53621 +P0DTC8 Q16891 +P0DTC8 O95347 +P0DTC8 O14980 +P0DTC8 Q9P035 +P0DTC8 Q9UBF2 +P0DTC8 P46379 +P0DTD2 P19105 +P0DTD2 Q14315 +P0DTD2 P62140 +P0DTD2 P0DP23 +P0DTD2 P09622 +P0DTD2 P52907 +P0DTD2 P60660 +P0DTD2 Q9NYL9 +P0DTD2 P13797 +P0DTD2 Q9UM54 +P0DTD2 Q9ULV4 +P0DTD2 P59998 +P0DTD3 Q9BUQ8 +P0DTD3 Q96N67 +P0DTD3 Q15365 +P0DTD3 P17987 +P0DTD3 O00231 +P0DTD3 Q14257 +P0DTD3 Q16576 +P0DTD3 P62269 +P0DTD3 Q58FF8 +P0DTD3 P27348 +P0DTD3 P40227 +P0DTD3 P63244 +P0DTD3 P49368 +P0DTD3 Q9Y265 +P0DTD3 P07355 +P0DTD3 P46379 +P0DTD3 P48643 +P0DTD3 Q99832 +P0DTD3 Q9UHI6 +P0DTD3 P31948 +P0DTD3 Q3ZCQ8 +P0DTD3 Q9NVI7 +P0DTD3 P46782 +P0DTD3 P25205 +P0DTD3 P30153 +P0DTD3 Q9ULX6 +P0DTD3 P42677 +P0DTD3 P61158 +P0DTD3 P09874 +P0DTD3 P50402 +P0DTD3 P30876 +P0DTD3 Q9NXS2 +P0DTD3 P40939 +P0DTD3 P06493 +P0DTD3 P51570 +P0DTD3 Q7Z4H7 +P0DTD3 P57088 +P0DTD3 Q14739 +P0DTD3 P10155 +P0DTD3 Q9UM54 +P0DTD3 P04406 +P0DTD3 Q9H936 +P0DTD3 P11586 +P0DTD3 Q9UBX3 +P0DTD3 Q5H9R7 +P0DTD3 P53618 +P0DTD3 P53007 +P0DTD3 P12004 +P0DTD3 P05023 +P0DTD3 Q16891 +P0DTD3 Q9NZ01 +P0DTD3 O14980 +P0DTD3 O95347 +P0DTD3 Q9NTJ3 +P0DTD3 P53621 +P0DTD3 P16615 +P0DTD3 O43592 +P0DTD3 Q9UBF2 +P0DTD3 P35606 +P0DTD3 Q9P035 +P59594 P35247 +P52294 P52630 +P52294 P42224 +P52292 Q14974 +P52294 P42224 +P52294 Q14974 +P52292 Q14974 +P52294 P42224 +P52292 Q14974 +P52292 Q14974 +Q99623 P35232 +Q99623 P35232 +O35129 P67778 +O35129 Q9JIA7 +O35129 P67778 +O35129 P19783 +O35129 P00397 +O35129 P67778 +P00405 P19783 +P00405 P12787 +P00405 P19536 +P00405 Q9CPQ1 +P00405 P56391 +P00405 P56392 +P12904 P06782 +P33469 P13010 +P33469 P39019 +P33469 P61254 +P33469 P16402 +P33469 P09651 +P33469 P67809 +P33469 P0DMV8 +P33469 P11940 +P33469 P12956 +P33469 Q00839 +P33469 P19338 +P33469 P67809 +P33469 P12956 +P33469 Q00839 +P33469 P19338 +K0BWD0 P05161 +P05161 K0BWD0 +K0BWD0 P05161 +K0BWD0 P05161 +R9QB93 K0BWD0 +O43765 P59635 +O43765 P59635 +P59635 P59596 +P59635 P59637 +Q5MAG3 P59635 +O43765 P59635 +P59595 P59596 +P0DTC2 P35613 +P0DTC2 P35613 +P0DTC2 P35613 +P0DTC2 P35613 +P0DTC2 P35613 +P04135 F1S215 +P04135 F1RNN5 +P04135 A6M930 +P04135 A6M930 +P04135 A6M930 +P04135 A6M930 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P59594 +Q9BYF1 P0DTC2 +Q9BYF1 P0DTC2 +Q53F19 P56270 +Q53F19 Q14011 +Q53F19 Q8N0Z6 +Q53F19 Q8N8U2 +Q53F19 P35052 +Q53F19 O95373 +Q53F19 Q9UH99 +Q53F19 Q8NEM0 +Q53F19 Q32M45 +Q53F19 Q6L8Q7 +Q53F19 Q6PJT7 +Q53F19 Q02224 +Q53F19 P31150 +Q53F19 Q5DJT8 +Q53F19 Q9UER7 +Q53F19 P48668 +Q53F19 O75940 +Q53F19 J3QSV6 +Q53F19 O95619 +Q53F19 Q7L590 +Q53F19 Q96GX9 +Q53F19 Q6IEG0 +Q53F19 Q9BY42 +Q53F19 P61326 +Q53F19 Q96IZ7 +Q53F19 Q8TF76 +Q53F19 O00148 +Q53F19 P49756 +Q53F19 Q10589 +Q53F19 P31431 +Q53F19 P34741 +Q53F19 P83916 +Q53F19 P63104 +Q53F19 O75400 +Q53F19 Q8TDD1 +Q53F19 P09603 +Q53F19 Q86WX3 +Q53F19 Q00403 +Q53F19 Q13352 +Q53F19 Q13011 +Q53F19 Q9BRP8 +Q53F19 Q9BSM1 +Q53F19 Q6SPF0 +Q53F19 O60684 +Q53F19 Q16630 +Q53F19 P55198 +Q53F19 P78563 +Q53F19 O00422 +Q53F19 Q9ULL5 +Q53F19 P52298 +Q53F19 Q09161 +Q53F19 Q9BXP5 +Q53F19 Q96PV6 +Q53F19 Q9UHI6 +Q53F19 Q9UKV3 +Q53F19 Q86U42 +Q53F19 Q13155 +Q53F19 P34897 +Q53F19 Q9UKS6 +Q53F19 O14950 +Q53F19 P53582 +Q53F19 P20700 +Q53F19 P68400 +Q53F19 Q5JVF3 +Q53F19 P19784 +Q53F19 O00629 +Q53F19 P68036 +Q53F19 Q96A72 +Q53F19 Q9Y5S9 +Q53F19 P15692 +Q53F19 P35637 +Q53F19 Q00688 +Q53F19 P98179 +Q53F19 Q9P289 +Q53F19 O00505 +Q53F19 Q96K17 +Q53F19 Q9H307 +Q53F19 Q96J01 +Q53F19 Q8NI27 +Q53F19 Q13769 +Q53F19 Q13838 +Q53F19 Q86W42 +Q53F19 Q96FV9 +Q53F19 P38919 +Q53F19 Q6I9Y2 +Q53F19 P51148 +Q53F19 P50395 +P52298 Q9UH99 +P52298 P04003 +P52298 Q8N0X7 +P52298 K7ELV2 +P52298 P14735 +P52298 Q5VV67 +P52298 P31150 +P52298 Q5DJT8 +P52298 P52294 +P52298 P09603 +P52298 P53007 +P52298 Q9NWV8 +P52298 Q8N5P1 +P52298 Q9ULL5 +P52298 O94762 +P52298 P39880 +P52298 Q8IXH7 +P52298 Q9H3P2 +P52298 Q6NZY4 +P52298 Q8WX92 +P52298 Q9Y580 +P52298 Q86VM9 +P52298 Q9H814 +P52298 P18615 +P52298 Q09161 +P52298 Q9BXP5 +P52298 Q9UHI6 +P52298 Q8NC51 +P52298 O75822 +P52298 Q9P289 +P52298 O00505 +P52298 P51148 +P52298 P50395 +P52298 P61927 +P52298 O00505 +P52298 Q13769 +P52298 Q9H814 +P52298 Q09161 +P52298 Q9BXP5 +P52298 P38919 +Q53F19 O00505 +Q53F19 Q13769 +Q53F19 Q9H814 +Q53F19 P38919 +Q53F19 Q09161 +Q53F19 Q9BXP5 +Q09161 Q53F19 +P33724 P59632 +P33724 P59632 +P33724 P59632 +P33724 P59632 +P59632 P33724 +P0DTC9 P0DTC4 +P0DTC3 P0DTC4 +P0DTD2 P0DTC4 +Q14160 B7Z2Y1 +Q14160 Q14155 +Q14160 Q7Z628 +P22460 Q14160 +Q96DN2 Q14160 +O00429 Q14160 +Q99569 Q14160 +Q96DL1 Q14160 +B7Z2Y1 Q14160 +Q14155 Q14160 +Q9UQB3 Q14160 +Q9BR11 Q14160 +Q96DL1 Q14160 +Q14155 Q14160 +B7Z2Y1 Q14160 +Q9ULI0 Q14160 +P25100 Q14160 +P53778 Q14160 +Q9NYB5 Q14160 +Q15311 Q14160 +P33402 Q14160 +Q9P0K1 Q14160 +Q92953 Q14160 +O15439 Q14160 +P35222 Q14160 +P48065 Q14160 +Q14160 P08581 +P22460 Q14160 +O00429 Q14160 +Q14160 Q6UY11 +Q14160 Q9ULJ7 +Q14160 A6NIM6 +P81408 Q14160 +Q9NQG5 Q14160 +Q6UXZ0 Q14160 +Q92502 Q14160 +Q8TA94 Q14160 +Q9Y6R1 Q14160 +Q9Y466 Q14160 +Q8TBB1 Q14160 +P22460 Q12959 +Q9UQB3 Q12959 +Q12959 Q96DL1 +Q12959 Q15311 +Q12959 Q96A65 +Q6ZTQ3 Q12959 +Q12959 Q14CM0 +Q12959 Q9NYB5 +Q12959 Q14524 +Q12959 Q13224 +Q12959 P85299 +Q12959 Q9NVW2 +Q12959 Q99569 +Q12959 Q6DN90 +Q12959 Q86W11 +Q12959 A1L4L8 +Q12959 Q15303 +Q12959 Q96GG9 +Q96GL9 Q12959 +Q86UD3 Q12959 +O60333 Q12959 +A1A5B4 Q12959 +Q7Z628 Q12959 +P0C2L3 Q12959 +Q9NS75 Q12959 +Q96SF7 Q12959 +B7Z2Y1 Q12959 +Q9NS75 Q12959 +Q9P021 Q12959 +Q7Z628 Q12959 +Q99569 Q96NW7 +Q9UQB3 Q96NW7 +Q9UQB3 Q96RT1 +Q99569 Q96RT1 +O00192 Q96RT1 +Q9BY21 Q96RT1 +Q96DL1 Q96RT1 +Q8NHY3 Q96RT1 +P50804 Q12959 +P24835 Q12959 +P50804 Q12959 +P24835 Q12959 +P27228 Q12959 +Q06093 Q12959 +P06463 Q12959 +P21735 Q12959 +P30911 Q12959 +P06427 Q12959 +P0C213 Q12959 +P03126 Q12959 +P27962 Q12959 +P54667 Q12959 +P54667 Q12959 +P09708 Q12959 +P17386 Q12959 +P36807 Q12959 +P26554 Q12959 +Q0A442 Q12959 +P30910 Q12959 +P26555 Q12959 +P16717 Q12959 +P50804 Q12959 +P24835 Q12959 +P21735 Q12959 +P06427 Q12959 +P27228 Q12959 +P09708 Q12959 +A3EX99 Q96RT1 +A3EXD5 Q96RT1 +P06427 Q96RT1 +Q9IDV3 Q96RT1 +Q1A244 Q96RT1 +P03126 Q96RT1 +P50804 Q96RT1 +P0C9G5 Q96RT1 +A3EXD4 Q96RT1 +P89432 Q96RT1 +P0C213 Q96RT1 +Q14160 P06427 +Q14160 P03126 +Q14160 P17589 +P0C213 Q14160 +Q14160 P0C222 +P06427 Q14160 +P03126 Q14160 +P0C213 Q14160 +P27228 Q14160 +P24835 Q14160 +P04299 Q14160 +P16717 Q14160 +P50804 Q14160 +P21735 Q14160 +P0C222 Q14160 +P03333 Q14160 +Q09SZ7 Q14160 +Q08089 Q14160 +Q18LE1 Q96RT1 +Q14500 Q14160 +P48050 Q14160 +P12814 Q14160 +P35609 Q14160 +Q08043 Q14160 +O43707 Q14160 +Q8IWK6 Q12959 +Q86SQ6 Q12959 +Q96PE1 Q12959 +P22459 Q12959 +Q14500 Q12959 +Q14155 Q14160 +P33402 Q14160 +P53778 Q14160 +Q99569 Q14160 +Q99569 Q14160 +Q14155 Q14160 +P53778 Q14160 +P33402 Q14160 +P35222 Q14160 +P59595 P63165 +P59595 P63165 +P59595 P09651 +P59595 P09651 +P59595 P09651 +P0C6X7 O75348 +P0C6X7 O75348 +P0C6X7 O75348 +P0C6X7 O75348 +P0C6X7 O75348 diff --git a/www/Examples/TAU/TAU_KEGG_Annotation_NORMA.txt b/www/Examples/TAU/TAU_KEGG_Annotation_NORMA.txt new file mode 100644 index 0000000..6543980 --- /dev/null +++ b/www/Examples/TAU/TAU_KEGG_Annotation_NORMA.txt @@ -0,0 +1,10 @@ +Ribosome RpL10,RpL10Ab,RpL11,RpL12,RpL13,RpL18A,RpL26,RpL3,RpL30,RpL4,RpLP0,RpLP2,RpS10a,RpS10b,RpS11,RpS12,RpS13,RpS15Aa,RpS15Ab,RpS16,RpS17,RpS18,RpS2,RpS23,RpS27,RpS3,RpS3A,RpS4,RpS6,RpS7,RpS8,bonsai,mRpL12,mRpS10,mRpS7,mRpS9,sta +Proteasome Prosalpha6,Prosalpha7,Prosbeta7,Rpn12,Rpn13,Rpn2,Rpn3,Rpn5,Rpn6,Rpt1,Rpt2,Rpt4,Rpt4R,Rpt5,Rpt6,Rpt6R +Spliceosome B52,CG10077,CG10777,CG16941,Hrb98DE,LS2,Prp8,Ref1,SF2,Tango4,U2A,U2af50,mago +Phagosome Act79B,Cnx99A,Cp1,Dhc64C,Dlic,Hrs,Mtl,Rab5,Rac1,Vha100-1,Vha100-2,Vha68-2,betaTub56D +Carbon metabolism AcCoAS,CG10932,CG11876,CG11899,CG12262,CG3011,CG32026,CG33791,Cat,Eno,Mdh2,Men,Men-b,Nc73EF,Pfk,PyK,Tpi,l(1)G0156 +Oxidative phosphorylation ATPsynCF6,ATPsynbeta,COX4,COX5A,COX6B,ND-15,ND-51,ND-51L1,ND-B16.6,ND-PDSW,NP15.6,Vha100-1,Vha100-2,Vha68-2,sun +RNA degradation CG2091,Eno,Hsc70-5,Hsp60,Hsp60C,Not1,Not3,Pfk,me31B,pAbp +RNA transport CG9769,Fmr1,Mtor,Rae1,Ref1,Trip1,eIF-3p66,eIF3-S10,eIF3-S8,eIF3-S9,eIF4G,emb,mago,pAbp,smt3 +Glycolysis AcCoAS,Aldh,CG11876,Eno,ImpL3,Pfk,PyK,Tpi +Pyruvate metabolism ACC,AcCoAS,Aldh,CG10932,CG11876,ImpL3,Mdh2,Men,Men-b,PyK diff --git a/www/Examples/TAU/TAU_Louvain.txt b/www/Examples/TAU/TAU_Louvain.txt new file mode 100644 index 0000000..cc8efac --- /dev/null +++ b/www/Examples/TAU/TAU_Louvain.txt @@ -0,0 +1,9 @@ +Group-1 Mgstl,KP78a,CG9257,par-1,CG31064,LpR1,Rab39,LpR2,CG3529,Lam,LamC,Sap-r,Vps60,lqf,ninaC,Hrs,Arr2,Arr1,Rab5,CG32683,pins,chp +Group-2 Sc2,Uch,Prx2540-1,Ace,CG4572,Sgt,CG14715,CG10359,Ect3,CalpB,Prx5,Ost48,Hsp60C,Prx2540-2,Hsp60,Ubqn,Hsp70Ab,Ugt,Iswi,Trap1,Cat,Hsc70-5,CG8209,DnaJ-H,p47,Rad23,Cnx99A,pont,Cp1,cathD,Uba2,Pfk,Gdi,Uch-L5,Rpt4R,Prosalpha1,mor,Bap55,Ufd1-like,TER94,Hsc70-3,PyK,Hsp83,ERp60,arm,Rpt1,Rpn13,Rpt2,Rpn5,Rpt6R,Rpn2,Pdi,CaBP1,Gp93,Rpt4,Hop,CG1416,Prosalpha6,Rpt5,Prosalpha7,Rpn6,CG8258,Rpt6,Rpn3,Rpn12,Prosbeta7,Usp5,CG2918,CG2852,CG1354,CG6891,HDAC6,Hsp27,His1,CG11577 +Group-3 shep,CG5590,CG17597,ScpX,Mfe2 +Group-4 CG4882,EfTuM,Rbp2,eEF1delta,CG10576,Nacalpha,mRpS34,vig,CG4679,mRpS35,mRpS7,mRpL44,mRpS22,eIF4G,bonsai,mRpS10,Srp54k,mRpL12,Tctp,eIF-2gamma,mRpS23,mRpS18B,ade3,mRpS9,mRpS5,Srp19,Srp72,eIF-3p66,Cctgamma,Ef1beta,Tango7,RpL10Ab,Trip1,eIF3-S10,eIF3-S9,Tcp-1eta,RpL13,RpS11,eIF3-S8,RpS6,RpS2,RpL12,RpS7,RpL10,RpS8,RpL30,RpS10a,RpS17,RpLP0,eRF1,Elf,RpS15Aa,RpS27,RpS18,RpS10b,RpS3A,RpS16,RpS15Ab,RpL3,Tcp-1zeta,T-cp1,RpS3,RpL26,RpS12,CG5642,CG9769,CG7033,Cct5,RpLP2,RpS23,RpS14a,Rack1,RpL11,RpS13,RpL18A,RpS4,RpL4,sta,wuho,bsf,kra,Hsp67Bc,su(r),FeCH,CG8635 +Group-5 nito,bol,Kap-alpha3,CG2082,lig,clu,Dbp80,Plc21C,Hrb27C,bel,Dp1,Lpin,Pkc53E,Nlp,rin,poe,CG43367,emb,fne,RanBPM,yps,Art4,CG10077,mub,AGO1,glo,Srp54,Rbp9,Rae1,Mtor,Ref1,mago,B52,LS2,U2af50,Fmr1,Top2,heph,Hrb98DE,Tudor-SN,SF2,me31B,CG16941,Prp8,Tango4,U2A,smt3,pAbp,Not1,Not3,CG2091,Capr,Gbeta76C,CG10777,CG6617,CG3800,tyf,CG11505 +Group-6 CG6178,frj,CG9090,Ssadh,l(1)G0156,CG32026,Men-b,CG11899,CG16935,AGBE,ImpL3,Nc73EF,CG3902,CG11876,Pdk,Vha100-1,AcCoAS,ACC,Mdh2,Cyt-c-p,Aldh,CG15093,ND-51L1,ND-51,Vha68-2,Vha100-2,ND-15,CG12262,yip2,ade5,CG11089,COX6B,CG11752,COX4,COX5A,Eno,Tpi,ND-PDSW,NP15.6,ATPsynbeta,ATPsynCF6,CG10932,CG33791,CG3011,Men,sun,porin,ND-B16.6,CG7603,Mpcp +Group-7 Ndg,trol,Cg25C,vkg,LanA,LanB2,Glt +Group-8 Lar,CG31715,PlexA,gammaSnap2,CG10186,Klp10A,Mtl,bai,Act79B,Abl,Tim17b,p115,Sec24AB,robl,tsr,flr,FKBP59,bic,BicD,CadN,syd,Rac1,zip,alphaCOP,zetaCOP,CG7048,CG7770,Dlic,Dhc64C,betaCOP,cpa,cpb,epsilonCOP,beta'COP,Klc,Khc,alpha-Cat,deltaCOP,RhoGDI,Dscam1,Tim9a,sqh,dock,Dscam2,CG8498,CLIP-190,betaTub56D +Group-9 Mapmodulin,Aats-val,Aats-his,Aats-asp,CG33123,Aats-ile,Aats-arg,CG31739 diff --git a/www/Examples/TAU/TAU_expressions.txt b/www/Examples/TAU/TAU_expressions.txt new file mode 100644 index 0000000..0af4485 --- /dev/null +++ b/www/Examples/TAU/TAU_expressions.txt @@ -0,0 +1,363 @@ +CG7603 red +His1 red +Act79B red +Gbeta76C red +CG11876 red +Kap-alpha3 red +CG2082 red +LanA red +Vha68-2 red +CalpB red +Ssadh red +CG43367 red +l(1)G0156 red +CG9090 red +Prx5 red +CG1354 red +porin red +GstD9 red +CG32026 red +Aldh red +Glt red +Tpi red +PyK red +ATPsynbeta red +Eno red +Mtl red +Arr1 red +Mpcp red +Arr2 red +Ndg red +FeCH red +Nc73EF red +CG10932 red +CG33791 red +betaTub56D red +Cyt-c-p red +CG16935 red +Prx2540-2 red +GstE4 red +pins red +chp red +Men-b red +GstD3 red +HDAC6 red +CG10359 red +mago red +LamC red +Ace red +Mdh2 red +ninaC red +LanB2 red +Cg25C red +sun red +vkg red +Pfk red +Prx2540-1 red +EfTuM red +Vha100-1 red +Vha100-2 red +AGBE red +frj red +DnaJ-H red +trol red +Men green +p47 green +mRpS10 green +NP15.6 green +RpS15Ab green +Not3 green +Tim17b green +eIF4G green +cathD green +CG12262 green +Prosalpha7 green +Usp5 green +yip2 green +Dscam1 green +CG2918 green +cpa green +RpS4 green +ade3 green +CG8209 green +CG11505 green +FKBP59 green +RpS11 green +Rab39 green +RpL13 green +CG1371 green +Prp8 green +Prosalpha6 green +Rpn12 green +Rpn2 green +Rpn5 green +Nacalpha green +Hsp60 green +Tudor-SN green +Rpt5 green +Rpt4 green +lqf green +Rpt1 green +Top2 green +Rpn6 green +ND-PDSW green +sta green +sqh green +CG6178 green +CG32683 green +Aats-asp green +LpR2 green +eIF-2gamma green +RpL11 green +Elf green +ERp60 green +Sec24AB green +CG11089 green +Rae1 green +CG3011 green +Abl green +Rpt6R green +Uch green +Trap1 green +CG7048 green +Rpt6 green +CG10186 green +wuho green +Aats-arg green +Lam green +Hsp60C green +Trip1 green +RpS3 green +Tctp green +CG31064 green +Dscam2 green +emb green +Uba2 green +pAbp green +Capr green +CG4972 green +Aats-his green +Aats-ile green +PlexA green +Cctgamma green +Rab5 green +zip green +Mi-2 green +AcCoAS green +su(r) green +mRpS23 green +arm green +CG10077 green +Ref1 green +RpS7 green +CG10777 green +Aats-val green +ImpL3 green +Pdi green +Sgt green +beta'COP green +alphaCOP green +Fmr1 green +CG31715 green +mRpS7 green +Ef1beta green +ND-B16.6 green +ScpX green +Cat green +lig green +CG10576 green +p115 green +ade5 green +Bap55 green +Hsp83 green +RpL4 green +Prosbeta7 green +RpLP2 green +CG4572 green +CG31739 green +Tim9a green +pont green +Hsc70-5 green +Hsp27 green +bic green +Hsp67Bc green +KP78a green +CG3529 green +BicD green +RpS12 green +bsf green +Hsc70-3 green +Hrb98DE green +RpS8 green +CG8498 green +Dp1 green +Ost48 green +Srp54k green +Rad23 green +Tango4 green +kra green +Hsp70Ab green +RpS15Aa green +tyf green +Lpin green +Cnx99A green +mRpS18B green +eEF1delta green +CG3800 green +RpL18A green +par-1 green +me31B green +COX4 green +eIF3-S10 green +CG11752 green +CG14715 green +Mtor green +U2af50 green +mRpS5 green +Khc green +Cct5 green +RpL12 green +yps green +B52 green +CG1416 green +bonsai green +TER94 green +Lar green +alpha-Cat green +glo green +CadN green +CG7033 green +CG2091 green +RpS17 green +Art4 green +syd green +RpL3 green +Tcp-1eta green +CG4679 green +Srp54 green +mRpS34 green +vig green +Srp72 green +ND-15 green +Mapmodulin green +Rack1 green +Cp1 green +Dlic green +RpS18 green +fne green +RpS14a green +eIF3-S8 green +CG5642 green +RpLP0 green +gammaSnap2 green +robl green +RpS16 green +Ugt green +Hrs green +Klc green +eIF-3p66 green +ND-51L1 green +betaCOP green +zetaCOP green +Ubqn green +rin green +eRF1 green +LS2 green +Dbp80 green +Ufd1-like green +eIF3-S9 green +ND-51 green +COX6B green +mRpS9 green +CG33123 green +CG9769 green +bel green +Sap-r green +deltaCOP green +Rbp9 green +RpS13 green +CG9257 green +mRpL12 green +Mfe2 green +CaBP1 green +AGO1 green +Mgstl green +CG17597 green +CG6617 green +mub green +ATPsynCF6 green +Rbp2 green +Uch-L5 green +dock green +LpR1 green +CG6891 green +RpL10 green +heph green +Hop green +RpL10Ab green +COX5A green +RpL30 green +mRpS22 green +Tcp-1zeta green +Nlp green +Rpt4R green +Ect3 green +poe green +Vps60 green +tsr green +RanBPM green +Plc21C green +mor green +epsilonCOP green +Rpt2 green +ACC green +Rpn3 green +bol green +RpS10a green +CG4882 green +CG8258 green +bai green +shep green +Srp19 green +CG2852 green +CG11899 green +clu green +Iswi green +Dhc64C green +RpS6 green +RpS10b green +CG5590 green +Klp10A green +Gdi green +RpS2 green +RpS27 green +Not1 green +flr green +Pkc53E green +CG11577 green +CG15093 green +CLIP-190 green +smt3 green +Rac1 green +cpb green +CG16941 green +RhoGDI green +Prosalpha1 green +mRpL44 green +Gp93 green +Pdk green +RpS23 green +T-cp1 green +RpL26 green +CG3902 green +CG7770 green +mRpS35 green +CG8635 green +RpS3A green +Tango7 green +Hrb27C green +nito green +Sc2 green +U2A green +Rpn13 green +SF2 green diff --git a/www/Examples/TAU/TAU_network_DEGs_NORMA.txt b/www/Examples/TAU/TAU_network_DEGs_NORMA.txt new file mode 100644 index 0000000..5f02df8 --- /dev/null +++ b/www/Examples/TAU/TAU_network_DEGs_NORMA.txt @@ -0,0 +1,3177 @@ +Source Target +CG6178 yip2 +shep ScpX +nito Ref1 +bol Hrb27C +Lar Abl +Lar CadN +CG31715 cpb +Mapmodulin Aats-ile +CG5590 CG17597 +Sc2 Ost48 +frj CG10932 +Kap-alpha3 emb +Uch Usp5 +Prx2540-1 Prx2540-2 +CG2082 CG2091 +lig Capr +lig rin +clu Dp1 +clu eIF3-S10 +clu rin +clu eIF3-S8 +clu eIF3-S9 +CG9090 Cyt-c-p +CG9090 COX5A +Ace Cat +Dbp80 emb +Ssadh CG33791 +Ssadh CG32026 +Ssadh Mdh2 +CG4572 CG10576 +CG4572 Hsc70-3 +PlexA alpha-Cat +l(1)G0156 CG11876 +l(1)G0156 Ssadh +l(1)G0156 CG33791 +Plc21C Gbeta76C +Plc21C Pkc53E +Hrb27C poe +Hrb27C heph +Hrb27C PyK +Hrb27C bel +Hrb27C me31B +Hrb27C pAbp +Hrb27C TER94 +Khc TAU +CG32026 CG33791 +bel Ref1 +bel poe +bel emb +bel vig +bel me31B +Dp1 CG2918 +Sgt Trap1 +Sgt Tcp-1zeta +Men-b CG11876 +Men-b PyK +Men-b Mdh2 +CG11899 CG3011 +Mgstl CG3529 +Mgstl Ost48 +CG14715 CG2852 +Lpin Pkc53E +gammaSnap2 zetaCOP +gammaSnap2 betaCOP +gammaSnap2 epsilonCOP +gammaSnap2 beta'COP +gammaSnap2 deltaCOP +CG10359 Gp93 +CG10359 CG1354 +CG10359 CaBP1 +CG10359 Pdi +KP78a CG32683 +KP78a Arr2 +CG9257 poe +CG9257 CG3529 +Ect3 Rpn5 +Ect3 CG6891 +Ect3 Cat +Ect3 CalpB +CG4882 bonsai +CalpB CG6891 +CalpB Cat +par-1 CG32683 +par-1 KP78a +par-1 Arr2 +Prx5 Prx2540-2 +Prx5 Cat +Pkc53E zip +CG31064 Rab5 +Nlp mago +Nlp eEF1delta +Nlp smt3 +CG10186 betaCOP +CG10186 cpb +CG10186 cpa +CG10186 beta'COP +CG10186 bai +CG10186 epsilonCOP +CG10186 Dlic +CG10186 p115 +CG10186 zetaCOP +CG10186 deltaCOP +rin Capr +LpR1 Arr2 +LpR1 Arr1 +LpR1 Ubqn +LpR1 CG32683 +LpR1 lqf +LpR1 Hrs +LpR1 LpR2 +LpR1 Rab5 +CG16935 CG12262 +CG16935 yip2 +poe betaCOP +poe CG3529 +CG43367 betaCOP +CG43367 poe +Rab39 Rab5 +Rab39 Gdi +emb Rae1 +emb alphaCOP +emb CG10576 +AGBE Eno +AGBE PyK +LpR2 Ubqn +LpR2 CG32683 +LpR2 Arr2 +LpR2 Hrs +LpR2 lqf +LpR2 Rab5 +CG3529 Ost48 +Ost48 CG2918 +Hsp60C Hop +Hsp60C Hsc70-3 +Prx2540-2 Cat +EfTuM RpS14a +EfTuM Hsc70-5 +EfTuM RpL10Ab +EfTuM RpS13 +EfTuM RpS16 +EfTuM RpS23 +EfTuM RpS15Aa +EfTuM RpL4 +EfTuM CG11876 +EfTuM bonsai +EfTuM mRpS10 +EfTuM RpL12 +EfTuM sta +EfTuM RpL3 +EfTuM Hsp60 +EfTuM RpS18 +EfTuM RpS2 +EfTuM mRpS9 +EfTuM RpS3 +EfTuM mRpL12 +EfTuM RpL11 +fne Tango4 +fne Srp54 +fne CG10777 +fne Prp8 +fne mago +fne CG16941 +fne yps +fne CG10077 +fne Rbp9 +fne U2A +fne Pkc53E +fne Hrb98DE +fne LS2 +fne SF2 +fne mub +fne heph +Klp10A deltaCOP +Mtl zip +Mtl tsr +Mtl RhoGDI +Hsp60 CG7033 +RanBPM CG6617 +yps Tango4 +yps LS2 +yps mago +yps CG10777 +Art4 CG10777 +Art4 CG10077 +Art4 pAbp +ImpL3 Cat +ImpL3 Men-b +ImpL3 Pfk +ImpL3 Men +ImpL3 Eno +ImpL3 Tpi +ImpL3 PyK +ImpL3 CG11876 +CG10077 CG10777 +CG10077 LS2 +CG10077 Tango4 +CG10077 yps +CG10077 mago +Lam Rab5 +LamC Lam +Ubqn HDAC6 +Ubqn CG32683 +mub CG3800 +mub mago +mub U2A +mub Srp54 +mub Tango4 +mub Prp8 +mub CG10777 +mub CG16941 +mub yps +mub CG10077 +mub LS2 +mub Hrb98DE +mub Rbp9 +mub SF2 +AGO1 Rbp9 +AGO1 pAbp +AGO1 Rae1 +AGO1 me31B +glo Hrb98DE +Sap-r CG3529 +Sap-r Mgstl +Sap-r pins +Sap-r Rab5 +Sap-r Ost48 +Ndg Cg25C +Ndg LanB2 +trol Cp1 +trol LanB2 +trol Cg25C +trol Ndg +Hsp70Ab Pdi +Hsp70Ab DnaJ-H +Hsp70Ab Hop +Hsp70Ab Hsp27 +bai Srp19 +bai CG2852 +bai gammaSnap2 +bai Klp10A +bai Klc +bai Dlic +bai cpa +bai p115 +bai cpb +bai deltaCOP +bai betaCOP +bai epsilonCOP +bai beta'COP +bai zetaCOP +Nc73EF Ssadh +Nc73EF CG32026 +Nc73EF l(1)G0156 +Nc73EF CG33791 +Nc73EF CG11876 +Srp54 yps +Srp54 CG10777 +Srp54 mago +Srp54 LS2 +Srp54 CG10077 +Srp54 Tango4 +Ugt CG2918 +Rbp9 CG10777 +Rbp9 Tango4 +Rbp9 Srp54 +Rbp9 Prp8 +Rbp9 CG10077 +Rbp9 CG16941 +Rbp9 U2A +Rbp9 Pkc53E +Rbp9 LS2 +Rbp9 Hrb98DE +Rbp9 yps +Rbp9 mago +Act79B cpb +Act79B flr +Act79B zip +Rbp2 pAbp +Rbp2 RpL10Ab +Rbp2 RpS4 +Rbp2 RpL18A +Rbp2 RpS3 +Rbp2 RpS11 +Rbp2 eIF3-S9 +Rbp2 RpL11 +Rbp2 eIF3-S10 +Rbp2 RpL13 +Rbp2 eIF3-S8 +Rbp2 RpS10b +Rbp2 RpS10a +Rbp2 RpS27 +Rbp2 RpL3 +Rbp2 RpS12 +Rbp2 RpS7 +Rbp2 RpL12 +Rbp2 Tango7 +Rbp2 RpS23 +Rbp2 RpS13 +Rbp2 sta +Rbp2 RpS18 +Rbp2 RpS16 +Rbp2 RpL4 +Rbp2 RpS6 +Rbp2 Trip1 +Rbp2 RpS2 +Rbp2 eIF-3p66 +Rbp2 CG5642 +Rbp2 CG9769 +Iswi His1 +Iswi Bap55 +Iswi smt3 +Iswi mor +Vps60 Hrs +eEF1delta eIF3-S10 +eEF1delta RpS10b +eEF1delta RpS13 +eEF1delta RpS6 +eEF1delta RpS16 +eEF1delta RpL12 +eEF1delta sta +eEF1delta RpS2 +eEF1delta CG5642 +eEF1delta RpL13 +eEF1delta Trip1 +CG10576 RpS6 +CG10576 RpS4 +CG10576 Capr +CG10576 sta +CG10576 RpS16 +CG10576 Tcp-1zeta +CG10576 wuho +CG10576 RpL12 +CG10576 CG7033 +Abl Dscam1 +Abl syd +Abl alpha-Cat +Abl arm +Rae1 mago +Mtor mago +Mtor AGO1 +Mtor Hsc70-3 +Mtor Hsc70-5 +Mtor smt3 +Mtor Rae1 +Ref1 LS2 +Ref1 mago +Trap1 Hsp70Ab +Trap1 Hop +Trap1 Hsp60C +Trap1 Hsp60 +Trap1 CG2918 +Trap1 Hsc70-3 +Trap1 CG1416 +CG17597 Cat +CG17597 ScpX +CG17597 Mfe2 +lqf CG32683 +lqf Ubqn +lqf Arr2 +lqf Rab5 +lqf Hrs +Tim17b Tim9a +CG3902 CG10932 +Cat CG6891 +p115 deltaCOP +Sec24AB deltaCOP +Sec24AB betaCOP +Sec24AB epsilonCOP +Sec24AB zetaCOP +Sec24AB alphaCOP +Sec24AB beta'COP +Sec24AB p115 +ninaC Act79B +ninaC zip +ninaC Arr2 +mago sta +mago RpS6 +mago CG10777 +CG11876 Men +CG11876 Mdh2 +CG11876 PyK +CG11876 ND-PDSW +CG11876 AcCoAS +CG11876 Eno +CG11876 CG33791 +Pdk ade3 +Pdk CG11876 +B52 RpS3 +B52 Hrb27C +B52 glo +B52 CG10777 +B52 fne +B52 CG10077 +B52 yps +B52 Rbp9 +B52 CG16941 +B52 Tango4 +B52 U2A +B52 mago +B52 Srp54 +B52 mub +B52 heph +B52 Prp8 +B52 LS2 +B52 Hrb98DE +B52 U2af50 +B52 SF2 +Vha100-1 sun +Vha100-1 Vha100-2 +Vha100-1 Vha68-2 +robl Dlic +Hsc70-5 CG7033 +Hsc70-5 T-cp1 +Hsc70-5 Tpi +Hsc70-5 DnaJ-H +Hsc70-5 Tcp-1eta +Hsc70-5 Hop +Hsc70-5 Rae1 +Hsc70-5 Trap1 +Hsc70-5 Hsp60C +Hsc70-5 Hsc70-3 +Hsc70-5 Hsp60 +Hsc70-5 Gp93 +tsr sqh +tsr cpa +tsr RpS6 +flr cpa +flr tsr +AcCoAS CG12262 +AcCoAS CG10932 +ACC Eno +ACC CG10932 +ACC AcCoAS +LS2 mago +LS2 CG10777 +FKBP59 ninaC +FKBP59 Dlic +FKBP59 cpb +FKBP59 cpa +FKBP59 Hop +U2af50 Ref1 +U2af50 LS2 +U2af50 yps +U2af50 CG10777 +U2af50 CG10077 +U2af50 Rbp9 +U2af50 fne +U2af50 Srp54 +U2af50 mago +U2af50 mub +U2af50 Tango4 +U2af50 heph +U2af50 CG16941 +U2af50 Hrb98DE +U2af50 Prp8 +U2af50 SF2 +U2af50 U2A +Fmr1 dock +Fmr1 PyK +Fmr1 poe +Fmr1 bel +Fmr1 pAbp +Fmr1 eIF4G +Fmr1 Dscam1 +Fmr1 RanBPM +Fmr1 Hrb98DE +Fmr1 Hrb27C +Fmr1 Capr +Fmr1 TER94 +Fmr1 Rbp9 +Fmr1 me31B +Fmr1 AGO1 +Fmr1 vig +ScpX Cat +ScpX Mfe2 +Top2 RpS13 +Top2 Pkc53E +Top2 Uba2 +Top2 Iswi +Top2 smt3 +CG8209 Ufd1-like +heph Tango4 +heph Srp54 +heph CG16941 +heph CG10077 +heph CG10777 +heph U2A +heph mago +heph yps +heph Prp8 +heph mub +heph LS2 +heph pAbp +heph Hrb98DE +heph Rbp9 +heph SF2 +Hrs CG4572 +Hrs CG32683 +Hrs Arr2 +Hrs Ubqn +Hrs Rab5 +Hrb98DE CG10777 +Hrb98DE mago +Hrb98DE Srp54 +Hrb98DE CG16941 +Hrb98DE CG10077 +Hrb98DE yps +Hrb98DE LS2 +Hrb98DE Tango4 +bic RpS3 +Nacalpha RpS13 +Nacalpha RpS2 +Nacalpha sta +Nacalpha RpS6 +Nacalpha RpS16 +Nacalpha RpS27 +Nacalpha RpS4 +Nacalpha RpS23 +Nacalpha RpS18 +Nacalpha RpL12 +Nacalpha RpL18A +Nacalpha RpL11 +Nacalpha RpS10b +Nacalpha RpS17 +Nacalpha RpL13 +Nacalpha RpS3 +Nacalpha RpL4 +Nacalpha bic +Aats-val eEF1delta +Aats-val Ef1beta +Aats-val Aats-asp +Aats-val Aats-ile +Aats-val CG33123 +Aats-val Aats-arg +BicD Lam +BicD bic +BicD Khc +BicD cpb +BicD cpa +BicD Dlic +DnaJ-H CG2918 +DnaJ-H Hsc70-3 +DnaJ-H Hop +mRpS34 CG4882 +mRpS34 bonsai +CadN alpha-Cat +CadN arm +vig RpS15Aa +vig RpS13 +vig RpS16 +vig RpS17 +vig RpL12 +Tudor-SN bel +Tudor-SN AGO1 +Tudor-SN Fmr1 +Tudor-SN vig +Mdh2 Aldh +Mdh2 ND-51 +Mdh2 COX4 +Mdh2 Cyt-c-p +Mdh2 COX5A +Mdh2 Men +Mdh2 Eno +Cyt-c-p Cat +Cyt-c-p porin +Cyt-c-p ND-PDSW +Cyt-c-p COX6B +Aldh Cat +Aldh Eno +Aldh CG12262 +Aldh AcCoAS +CG15093 CG12262 +CG15093 AcCoAS +CG15093 CG3902 +CG15093 Aldh +CG4679 mRpS34 +CG4679 bsf +CG4679 CG4882 +CG4679 mRpL12 +CG4679 mRpL44 +CG4679 bonsai +CG4679 mRpS10 +CG4679 mRpS23 +CG4679 mRpS22 +CG4679 mRpS9 +CG4679 mRpS18B +SF2 CG10777 +SF2 CG10077 +SF2 U2A +SF2 Tango4 +SF2 yps +SF2 Prp8 +SF2 Rbp9 +SF2 CG16941 +SF2 Srp54 +SF2 Ref1 +SF2 LS2 +SF2 mago +SF2 Hrb98DE +p47 CG8209 +p47 Ufd1-like +p47 Rpt2 +Rad23 me31B +Rad23 Uch-L5 +Rad23 Rpt1 +Rad23 HDAC6 +Rad23 Usp5 +Rad23 Uba2 +Rad23 Ufd1-like +Rad23 Sgt +Rad23 Rpt5 +Rad23 Bap55 +Rad23 pont +Rad23 CG8209 +Rad23 Iswi +Rad23 Rpn13 +Rad23 Ubqn +Rad23 smt3 +Rad23 Rpt2 +Rad23 TER94 +me31B poe +me31B CG2091 +me31B yps +ND-51L1 ND-51 +ND-51L1 NP15.6 +ND-51L1 ND-PDSW +ND-51L1 ND-B16.6 +syd alpha-Cat +syd Dlic +syd Klc +Arr2 Ubqn +Arr2 CG32683 +Arr1 ninaC +Arr1 chp +Arr1 KP78a +Arr1 LpR2 +Arr1 Ubqn +Arr1 CG32683 +Arr1 par-1 +Arr1 Hrs +Arr1 lqf +Arr1 Rab5 +Arr1 Arr2 +ND-51 ND-B16.6 +ND-51 ND-PDSW +CG16941 yps +CG16941 mago +CG16941 CG10777 +CG16941 CG10077 +CG16941 Srp54 +CG16941 LS2 +CG16941 Tango4 +Cnx99A Ost48 +Cnx99A Hsp70Ab +Cnx99A CG2918 +Cnx99A Ugt +Cnx99A Gp93 +Cnx99A Pdi +Cnx99A Hsc70-3 +Cnx99A ERp60 +Aats-his Aats-asp +Aats-his Cct5 +Aats-his Aats-arg +Aats-his Aats-val +Aats-his CG33123 +Aats-his Aats-ile +mRpS35 RpS2 +mRpS35 CG4882 +mRpS35 mRpL12 +mRpS35 mRpS34 +mRpS35 mRpL44 +mRpS35 CG4679 +mRpS35 mRpS23 +mRpS35 mRpS18B +mRpS35 mRpS10 +mRpS35 mRpS9 +mRpS35 mRpS22 +mRpS35 bonsai +mRpS35 mRpS5 +Vha68-2 sun +Vha68-2 Eno +Vha100-2 sun +Vha100-2 Vha68-2 +Rac1 sqh +Rac1 Fmr1 +Rac1 tsr +Rac1 Dscam2 +Rac1 Gdi +Rac1 Abl +Rac1 dock +Rac1 zip +Rac1 RhoGDI +mRpS7 ade3 +mRpS7 CG9769 +mRpS7 CG4882 +mRpS7 eIF3-S8 +mRpS7 Srp19 +mRpS7 EfTuM +mRpS7 eIF3-S10 +mRpS7 eIF-2gamma +mRpS7 Srp54k +mRpS7 eRF1 +mRpS7 Tango7 +mRpS7 RpLP2 +mRpS7 eIF3-S9 +mRpS7 RpL4 +mRpS7 RpS10a +mRpS7 RpS12 +mRpS7 RpS10b +mRpS7 CG7048 +mRpS7 RpS15Ab +mRpS7 RpS15Aa +mRpS7 RpL10Ab +mRpS7 RpS14a +mRpS7 RpL18A +mRpS7 Rack1 +mRpS7 RpS4 +mRpS7 RpS17 +mRpS7 RpL13 +mRpS7 RpS7 +mRpS7 RpS6 +mRpS7 RpS8 +mRpS7 RpS16 +mRpS7 sta +mRpS7 RpS13 +mRpS7 RpL3 +mRpS7 RpS23 +mRpS7 RpS27 +mRpS7 RpL12 +mRpS7 RpS18 +mRpS7 RpS3A +mRpS7 RpS2 +mRpS7 RpS3 +mRpS7 RpS11 +mRpS7 RpL11 +mRpS7 RpL30 +mRpS7 mRpL44 +mRpS7 mRpS22 +mRpS7 mRpS34 +mRpS7 CG4679 +mRpS7 mRpS23 +mRpS7 bonsai +mRpS7 mRpS10 +mRpS7 mRpS35 +mRpS7 mRpL12 +mRpS7 mRpS18B +mRpS7 mRpS9 +mRpS7 mRpS5 +Prp8 yps +Prp8 Srp54 +Prp8 CG10777 +Prp8 CG10077 +Prp8 LS2 +Prp8 mago +Prp8 Hrb98DE +Prp8 CG16941 +Prp8 Tango4 +pont Tcp-1zeta +pont Tcp-1eta +pont Rpt6 +pont Uch-L5 +mRpL44 mRpS34 +mRpL44 mRpL12 +mRpL44 bonsai +mRpS22 CG4882 +mRpS22 mRpL12 +mRpS22 mRpS10 +mRpS22 mRpL44 +mRpS22 mRpS34 +mRpS22 mRpS23 +mRpS22 bonsai +mRpS22 mRpS18B +mRpS22 mRpS9 +Cp1 Ect3 +Cp1 PyK +Cp1 Rpn5 +Cp1 CG6891 +Cp1 CalpB +Cp1 Rpn6 +Cp1 Cat +Cp1 Rpt5 +cathD CG4572 +cathD CG6891 +cathD PyK +cathD CG8258 +cathD Ect3 +cathD Pfk +cathD Rpn5 +cathD CalpB +cathD Rpt5 +cathD Rpn6 +cathD Cat +cathD Sap-r +cathD Cp1 +Tango4 CG10777 +Tango4 LS2 +Tango4 mago +U2A yps +U2A CG10077 +U2A CG10777 +U2A Srp54 +U2A Hrb98DE +U2A mago +U2A LS2 +U2A CG16941 +U2A Prp8 +U2A Tango4 +Uba2 Usp5 +Uba2 Ubqn +smt3 tsr +smt3 mago +smt3 Ufd1-like +smt3 CG10777 +smt3 CG10077 +smt3 Rae1 +smt3 Uba2 +zip sqh +ND-15 CG11752 +ND-15 ND-51L1 +ND-15 COX5A +ND-15 ND-51 +ND-15 NP15.6 +ND-15 ND-B16.6 +ND-15 ND-PDSW +pAbp rin +pAbp RpL3 +pAbp RpL4 +pAbp emb +pAbp Dp1 +pAbp RpS6 +pAbp tyf +pAbp bel +pAbp yps +pAbp CG11505 +pAbp me31B +eIF4G Dp1 +eIF4G kra +eIF4G bel +eIF4G PlexA +eIF4G me31B +eIF4G RpL18A +eIF4G RpL12 +eIF4G RpS12 +eIF4G Hsp67Bc +eIF4G RpS4 +eIF4G RpL11 +eIF4G RpS23 +eIF4G RpS7 +eIF4G RpS27 +eIF4G RpLP2 +eIF4G RpS10b +eIF4G RpS10a +eIF4G sta +eIF4G RpS16 +eIF4G RpL4 +eIF4G RpL13 +eIF4G RpS18 +eIF4G RpS13 +eIF4G RpL3 +eIF4G RpL10Ab +eIF4G RpS11 +eIF4G RpS8 +eIF4G RpS2 +eIF4G Hsp27 +eIF4G eIF-3p66 +eIF4G mago +eIF4G Tango7 +eIF4G RpS3 +eIF4G CG5642 +eIF4G CG9769 +eIF4G Trip1 +eIF4G Rbp2 +eIF4G RpS6 +eIF4G alpha-Cat +eIF4G eIF3-S10 +eIF4G eIF3-S8 +eIF4G eIF3-S9 +eIF4G pAbp +Pfk CG11876 +Pfk CG6891 +Pfk Ect3 +Pfk Rpt5 +Pfk Cp1 +Pfk Rpn6 +Pfk CalpB +Pfk Rpn5 +Pfk Cat +Pfk Eno +Pfk Tpi +Pfk PyK +bonsai RpS14a +bonsai RpS16 +bonsai sta +bonsai RpS6 +CG12262 CG10932 +yip2 Uch +yip2 AcCoAS +yip2 CG10932 +yip2 CG3902 +yip2 CG12262 +Rab5 Mgstl +Rab5 CG32683 +Rab5 Ost48 +Rab5 CG3529 +Rab5 Arr2 +Gdi Cat +Gdi CG6891 +Gdi Pfk +Gdi Rpt1 +Gdi Rpt5 +Gdi Rpn5 +Gdi CG8258 +Gdi Rpn6 +Gdi Rpn3 +Gdi PyK +Gdi TER94 +Gdi RhoGDI +Gdi Rab5 +mRpS10 Srp54k +mRpS10 CG4882 +mRpS10 RpL10Ab +mRpS10 RpS14a +mRpS10 RpS16 +mRpS10 RpS10b +mRpS10 RpS13 +mRpS10 RpS2 +mRpS10 RpS15Aa +mRpS10 RpL3 +mRpS10 RpL12 +mRpS10 RpL13 +mRpS10 RpS4 +mRpS10 RpS17 +mRpS10 RpS6 +mRpS10 sta +mRpS10 mRpS23 +mRpS10 mRpS18B +mRpS10 mRpS34 +mRpS10 mRpL12 +mRpS10 mRpL44 +mRpS10 bonsai +mRpS10 mRpS9 +Srp54k bonsai +Srp54k RpS14a +Srp54k RpS15Aa +Srp54k RpS17 +Srp54k RpL10Ab +Srp54k RpS10b +Srp54k RpL12 +Srp54k RpS4 +Srp54k RpS6 +Srp54k RpS16 +Srp54k RpS12 +Srp54k sta +Srp54k Srp19 +mRpL12 RpS14a +mRpL12 RpL10Ab +mRpL12 RpS16 +mRpL12 sta +mRpL12 mRpS34 +mRpL12 RpS15Aa +mRpL12 RpL12 +mRpL12 bonsai +Tctp RpS10b +Tctp eEF1delta +Tctp RpL10Ab +Tctp RpS14a +Tctp RpS15Aa +Tctp RpS4 +Tctp RpS17 +Tctp sta +Tctp RpS6 +Tctp RpS16 +Tctp RpL12 +Tctp RpS13 +Tctp RpS12 +Tctp RpS2 +Tctp RpL13 +eIF-2gamma Tcp-1zeta +eIF-2gamma Tcp-1eta +eIF-2gamma CG10576 +eIF-2gamma Cctgamma +eIF-2gamma CG7033 +eIF-2gamma pAbp +eIF-2gamma Cct5 +eIF-2gamma RpS14a +eIF-2gamma CG8258 +eIF-2gamma CG33123 +eIF-2gamma CG1354 +eIF-2gamma RpS15Ab +eIF-2gamma RpS15Aa +eIF-2gamma T-cp1 +eIF-2gamma RpS17 +eIF-2gamma RpL10Ab +eIF-2gamma RpLP2 +eIF-2gamma RpL18A +eIF-2gamma Rbp2 +eIF-2gamma CG5642 +eIF-2gamma RpL30 +eIF-2gamma eIF-3p66 +eIF-2gamma eIF4G +eIF-2gamma RpS3A +eIF-2gamma CG9769 +eIF-2gamma RpL12 +eIF-2gamma RpL11 +eIF-2gamma RpS8 +eIF-2gamma RpS10a +eIF-2gamma RpS10b +eIF-2gamma RpL13 +eIF-2gamma RpS27 +eIF-2gamma RpS23 +eIF-2gamma RpS18 +eIF-2gamma RpS11 +eIF-2gamma RpS7 +eIF-2gamma RpL3 +eIF-2gamma RpS12 +eIF-2gamma RpL4 +eIF-2gamma RpS4 +eIF-2gamma RpS13 +eIF-2gamma Trip1 +eIF-2gamma RpS2 +eIF-2gamma eIF3-S10 +eIF-2gamma RpS16 +eIF-2gamma RpS6 +eIF-2gamma Tango7 +eIF-2gamma sta +eIF-2gamma RpS3 +eIF-2gamma eIF3-S8 +eIF-2gamma eIF3-S9 +Uch-L5 Usp5 +Uch-L5 Rpt4R +Uch-L5 Rpn12 +Rpt4R Rpn12 +Prosalpha1 Rpt4R +Prosalpha1 Rpn2 +Prosalpha1 Rpn12 +Prosalpha1 Rpn6 +Prosalpha1 Rpt6R +Prosalpha1 Rpn5 +Prosalpha1 Rpt6 +Prosalpha1 Rpt5 +Prosalpha1 Prosalpha6 +Prosalpha1 Rpt2 +Prosalpha1 Prosbeta7 +mRpS23 CG4882 +mRpS23 mRpL44 +mRpS23 mRpL12 +mRpS23 bonsai +mRpS23 mRpS34 +mRpS18B CG4882 +mRpS18B mRpL12 +mRpS18B bonsai +mRpS18B mRpS34 +mRpS18B mRpL44 +mRpS18B mRpS23 +mor pont +Bap55 Uch-L5 +Bap55 pont +Bap55 mor +ade5 CG3011 +Cg25C LanA +vkg Glt +vkg LanA +vkg Ndg +vkg trol +vkg Cg25C +Ufd1-like Ubqn +Ufd1-like Usp5 +TER94 poe +TER94 bel +TER94 Cnx99A +TER94 ERp60 +TER94 Pdi +TER94 smt3 +TER94 Hop +TER94 Hsc70-3 +TER94 CalpB +TER94 Pfk +TER94 CG6891 +TER94 Ect3 +TER94 PyK +TER94 Rpn2 +TER94 Cat +TER94 Cp1 +TER94 me31B +TER94 CG8258 +TER94 cathD +TER94 Ubqn +TER94 Rpn5 +TER94 Rpn6 +TER94 Rpt1 +TER94 Rpt5 +TER94 CG8209 +TER94 Rpt2 +TER94 p47 +TER94 Ufd1-like +ade3 RpS15Aa +ade3 RpL12 +ade3 RpS14a +ade3 RpL10Ab +ade3 su(r) +ade3 RpS6 +ade3 RpS4 +ade3 ade5 +ade3 CG3011 +mRpS9 CG4882 +mRpS9 ade3 +mRpS9 RpS14a +mRpS9 RpL10Ab +mRpS9 RpS16 +mRpS9 sta +mRpS9 RpS13 +mRpS9 RpS15Aa +mRpS9 RpL12 +mRpS9 RpS2 +mRpS9 mRpS34 +mRpS9 mRpL44 +mRpS9 mRpS18B +mRpS9 mRpS23 +mRpS9 bonsai +mRpS9 mRpL12 +mRpS5 CG11876 +mRpS5 RpS16 +mRpS5 RpS14a +mRpS5 RpL10Ab +mRpS5 RpLP2 +mRpS5 CG4882 +mRpS5 Srp54k +mRpS5 RpS10b +mRpS5 RpL4 +mRpS5 RpS10a +mRpS5 RpS8 +mRpS5 RpS13 +mRpS5 sta +mRpS5 RpL3 +mRpS5 RpL12 +mRpS5 RpS15Aa +mRpS5 RpS23 +mRpS5 RpS15Ab +mRpS5 RpS27 +mRpS5 RpS7 +mRpS5 EfTuM +mRpS5 RpL18A +mRpS5 RpL13 +mRpS5 RpS4 +mRpS5 RpS17 +mRpS5 RpS6 +mRpS5 RpS18 +mRpS5 RpS11 +mRpS5 RpL11 +mRpS5 RpS3 +mRpS5 mRpS34 +mRpS5 mRpS18B +mRpS5 mRpS23 +mRpS5 mRpS22 +mRpS5 CG4679 +mRpS5 mRpL44 +mRpS5 mRpL12 +mRpS5 mRpS10 +mRpS5 bonsai +mRpS5 mRpS9 +Not1 Iswi +Not1 eIF4G +Not1 pAbp +Not1 Prp8 +Not1 me31B +Not3 frj +Not3 me31B +Not3 Not1 +Hsc70-3 CG2852 +Hsc70-3 Hsp60 +Hsc70-3 Ost48 +Hsc70-3 Rae1 +Hsc70-3 CG2918 +PyK bel +PyK poe +PyK me31B +PyK Men +PyK Aldh +PyK Ect3 +PyK CG6891 +PyK CalpB +PyK ND-51 +PyK Rpn5 +PyK Vha68-2 +PyK Cat +PyK Mdh2 +PyK Eno +LanA LanB2 +alphaCOP deltaCOP +Srp19 RpS14a +Srp19 RpS15Aa +Srp19 RpS17 +Srp19 RpL10Ab +Srp19 RpS10b +Srp19 RpS6 +Srp19 RpS4 +Srp19 sta +Srp19 RpS12 +Srp19 RpL12 +Srp19 RpS16 +Srp72 RpS12 +Srp72 RpS10b +Srp72 RpS6 +Srp72 RpS13 +Srp72 RpL10Ab +Srp72 RpL13 +Srp72 sta +Srp72 RpS4 +Srp72 RpS2 +Srp72 RpL3 +Srp72 RpS16 +Srp72 RpL12 +Srp72 Srp54k +Srp72 Srp19 +Hsp83 Tcp-1zeta +Hsp83 Rack1 +Hsp83 zip +Hsp83 ERp60 +Hsp83 pont +Hsp83 Pdi +Hsp83 Tpi +Hsp83 HDAC6 +Hsp83 bai +Hsp83 Cctgamma +Hsp83 Cnx99A +Hsp83 CG2918 +Hsp83 Cct5 +Hsp83 CG7033 +Hsp83 Sgt +Hsp83 Hsp27 +Hsp83 T-cp1 +Hsp83 CalpB +Hsp83 Ect3 +Hsp83 CG6891 +Hsp83 Dlic +Hsp83 cpa +Hsp83 Cp1 +Hsp83 Dhc64C +Hsp83 Hsp60C +Hsp83 cpb +Hsp83 cathD +Hsp83 Pfk +Hsp83 Gdi +Hsp83 Rac1 +Hsp83 PyK +Hsp83 Rpn6 +Hsp83 Hsp60 +Hsp83 Rpn5 +Hsp83 Rpt1 +Hsp83 Rpn3 +Hsp83 Rpt5 +Hsp83 Hsp70Ab +Hsp83 Cat +Hsp83 CG8258 +Hsp83 Hsc70-5 +Hsp83 FKBP59 +Hsp83 TER94 +Hsp83 DnaJ-H +Hsp83 Hsc70-3 +Hsp83 CG1416 +Hsp83 Hop +CG11089 ade5 +CG11089 ade3 +CG11089 CG3011 +ERp60 Pdi +ERp60 Ost48 +ERp60 CG2852 +ERp60 CG2918 +ERp60 CaBP1 +ERp60 Hsc70-3 +ERp60 Gp93 +zetaCOP Klp10A +zetaCOP Dlic +zetaCOP p115 +zetaCOP cpa +zetaCOP alphaCOP +zetaCOP betaCOP +zetaCOP deltaCOP +COX6B ND-B16.6 +CG7048 cpa +CG7048 Srp19 +CG7048 Nlp +CG7048 mRpS23 +CG7048 CG8498 +CG7048 robl +CG7048 Tim9a +CG7048 eEF1delta +CG7770 RpS18 +CG7770 pont +CG7770 CG7048 +arm Hsc70-3 +arm Prosalpha6 +arm Uch-L5 +arm Rpn5 +arm Prosbeta7 +arm Rpn12 +arm Rpt6R +arm Rpt6 +arm syd +arm Rpn2 +arm Rpn6 +arm Rpt1 +arm Rpt2 +arm Rpt5 +arm emb +arm Rpt4R +arm pont +arm alpha-Cat +eIF-3p66 Tcp-1eta +eIF-3p66 Cct5 +eIF-3p66 Cctgamma +eIF-3p66 RpS12 +eIF-3p66 RpS16 +eIF-3p66 RpS11 +eIF-3p66 RpS27 +eIF-3p66 RpS23 +eIF-3p66 RpS10a +eIF-3p66 RpS10b +eIF-3p66 RpS13 +eIF-3p66 RpL10Ab +eIF-3p66 RpL13 +eIF-3p66 RpS7 +eIF-3p66 RpL12 +eIF-3p66 sta +eIF-3p66 RpS4 +eIF-3p66 RpL11 +eIF-3p66 RpS18 +eIF-3p66 RpS2 +eIF-3p66 RpL3 +eIF-3p66 RpL18A +eIF-3p66 RpS3 +eIF-3p66 RpL4 +eIF-3p66 RpS6 +eIF-3p66 CG9769 +eIF-3p66 eIF3-S10 +eIF-3p66 Tango7 +eIF-3p66 eIF3-S8 +eIF-3p66 eIF3-S9 +eIF-3p66 Trip1 +eIF-3p66 CG5642 +CG11752 ND-B16.6 +COX4 CG11752 +COX4 porin +COX4 ND-B16.6 +COX4 ND-PDSW +COX4 Cyt-c-p +COX4 COX6B +COX5A sun +COX5A ND-51 +COX5A CG7603 +COX5A CG11752 +COX5A ND-B16.6 +COX5A Cyt-c-p +COX5A ND-PDSW +COX5A COX6B +COX5A COX4 +Eno Hsc70-3 +Eno sta +Tpi Cat +Tpi CG11899 +Tpi RpS13 +Tpi CG11876 +Tpi ade3 +Tpi CG3011 +Tpi Hsc70-3 +Tpi Mdh2 +Tpi PyK +Tpi Eno +Cctgamma Trip1 +Cctgamma CG10576 +Cctgamma CG3800 +Cctgamma Hop +Cctgamma Tcp-1zeta +Cctgamma Tcp-1eta +Cctgamma CG7033 +Rpt1 Uch +Rpt1 RpS15Aa +Rpt1 RpS15Ab +Rpt1 RpS6 +Rpt1 Ubqn +Rpt1 RpL3 +Rpt1 RpS11 +Rpt1 RpS18 +Rpt1 Ufd1-like +Rpt1 Cct5 +Rpt1 CalpB +Rpt1 Ect3 +Rpt1 CG6891 +Rpt1 PyK +Rpt1 Cp1 +Rpt1 Cat +Rpt1 Pfk +Rpt1 CG8258 +Rpt1 cathD +Rpt1 Prosalpha1 +Rpt1 Uch-L5 +Rpt1 Rpt6R +Rpt1 Rpt4R +Rpt1 Prosalpha6 +Rpt1 Rpn12 +Rpt1 Rpt5 +Rpt1 Rpn5 +Rpt1 Prosbeta7 +Rpt1 Rpn2 +Rpt1 Rpt6 +Rpt1 Rpt2 +Rpt1 Rpn6 +Dlic p115 +Dlic deltaCOP +Dlic cpa +Dhc64C CLIP-190 +Dhc64C betaTub56D +Dhc64C Klc +Dhc64C poe +Dhc64C bai +Dhc64C FKBP59 +Dhc64C betaCOP +Dhc64C deltaCOP +Dhc64C epsilonCOP +Dhc64C beta'COP +Dhc64C CG10186 +Dhc64C zetaCOP +Dhc64C p115 +Dhc64C cpa +Dhc64C cpb +Dhc64C robl +Dhc64C Khc +Dhc64C BicD +Dhc64C Dlic +Ef1beta FeCH +Ef1beta CG7048 +Ef1beta Aats-asp +Ef1beta RpS15Aa +Ef1beta RpS10a +Ef1beta RpS15Ab +Ef1beta CG5642 +Ef1beta RpL3 +Ef1beta RpS3A +Ef1beta RpS12 +Ef1beta RpS13 +Ef1beta RpS10b +Ef1beta RpS16 +Ef1beta RpS2 +Ef1beta RpS6 +Ef1beta RpL12 +Ef1beta RpL30 +Ef1beta RpS23 +Ef1beta RpS11 +Ef1beta eEF1delta +Ef1beta RpL13 +Ef1beta sta +Ef1beta RpS7 +Ef1beta Tctp +Ef1beta Trip1 +Ef1beta RpS8 +Ef1beta RpS18 +Ef1beta Rack1 +Ef1beta RpS17 +Ef1beta RpL10Ab +Ef1beta RpL18A +Ef1beta RpS3 +Ef1beta RpS4 +Ef1beta RpL11 +Ef1beta RpLP2 +Ef1beta RpL4 +Rpn13 TER94 +Rpn13 Ufd1-like +Rpn13 Ubqn +Rpn13 Prosalpha6 +Rpn13 Prosbeta7 +Rpn13 Rpt4R +Rpn13 Prosalpha7 +Rpn13 Rpn12 +Rpn13 Rpt6R +Rpn13 Rpn5 +Rpn13 Rpt6 +Rpn13 Rpn6 +Rpn13 Uch-L5 +Rpn13 Rpt2 +Rpn13 Rpt5 +Rpn13 Rpt1 +Rpn13 Rpn2 +Tango7 CG7033 +Tango7 Tcp-1zeta +Tango7 mRpS10 +Tango7 Rpn5 +Tango7 bonsai +Tango7 Rpn2 +Tango7 Rpn6 +Tango7 RpS14a +Tango7 RpS15Aa +Tango7 RpS17 +Tango7 RpL10Ab +Tango7 RpL12 +Tango7 RpL13 +Tango7 RpL18A +Tango7 RpL3 +Tango7 RpS10a +Tango7 RpL11 +Tango7 RpS10b +Tango7 RpS2 +Tango7 RpS27 +Tango7 RpS23 +Tango7 RpS18 +Tango7 RpS12 +Tango7 RpS13 +Tango7 RpS4 +Tango7 RpS6 +Tango7 RpS16 +Tango7 sta +Tango7 eIF3-S10 +Tango7 RpS3 +Tango7 Trip1 +Tango7 RpL4 +Tango7 CG9769 +Tango7 eIF3-S9 +Tango7 CG5642 +Tango7 eIF3-S8 +Rpt2 Ubqn +Rpt2 Ufd1-like +Rpt2 CG8209 +Rpt2 Uch-L5 +Rpt2 Rpt4R +Rpt2 Rpn12 +Rpt2 Rpt5 +Rpt2 Rpt6 +Rpt2 Prosalpha6 +Rpt2 Rpn5 +Rpt2 Prosbeta7 +Rpn5 Ubqn +Rpn5 Ufd1-like +Rpn5 Cat +Rpn5 CalpB +Rpn5 CG6891 +Rpn5 Uch-L5 +Rpn5 Rpt4R +Rpn5 Rpt6 +Rpn5 Rpn12 +betaCOP cpa +betaCOP Dlic +betaCOP Klp10A +betaCOP p115 +betaCOP alphaCOP +betaCOP deltaCOP +Rpt6R RpS4 +Rpt6R pont +Rpt6R Rpt6 +Rpt6R Uch-L5 +Rpt6R Rpt4R +Rpt6R Prosalpha6 +Rpt6R Rpt2 +Rpt6R Rpt5 +Rpt6R Prosbeta7 +Rpt6R Rpn12 +Rpt6R Rpn2 +Rpt6R Rpn5 +Rpn2 Trip1 +Rpn2 RpS2 +Rpn2 Usp5 +Rpn2 Uch-L5 +Rpn2 Prosalpha6 +Rpn2 Rpt4R +Rpn2 Prosbeta7 +Rpn2 Rpn12 +Rpn2 Rpn5 +Rpn2 Rpt6 +Rpn2 Rpt2 +Rpn2 Rpt5 +RpL10Ab bonsai +RpL10Ab mago +RpL10Ab CG5642 +RpL10Ab RpS15Aa +RpL10Ab RpS14a +RpL10Ab RpL12 +RpL10Ab RpS16 +RpL10Ab RpS12 +RpL10Ab RpS10b +RpL10Ab RpS4 +RpL10Ab sta +RpL10Ab RpS6 +Trip1 RpS14a +Trip1 Tcp-1zeta +Trip1 CG10576 +Trip1 RpS17 +Trip1 CG7033 +Trip1 RpL10Ab +Trip1 RpS10b +Trip1 RpL12 +Trip1 RpS12 +Trip1 RpS4 +Trip1 RpS6 +Trip1 RpS16 +Trip1 sta +Trip1 CG9769 +Trip1 eIF3-S10 +Trip1 CG5642 +cpa p115 +cpa deltaCOP +cpb CG6891 +cpb flr +cpb p115 +cpb deltaCOP +cpb tsr +cpb betaCOP +cpb zetaCOP +cpb Dlic +cpb cpa +eIF3-S10 CG33123 +eIF3-S10 Aats-ile +eIF3-S10 RpS14a +eIF3-S10 RpS17 +eIF3-S10 RpS15Aa +eIF3-S10 RpL10Ab +eIF3-S10 RpL12 +eIF3-S10 RpS12 +eIF3-S10 RpS10b +eIF3-S10 RpS6 +eIF3-S10 RpS16 +eIF3-S10 RpS4 +eIF3-S10 sta +eIF3-S10 CG5642 +eIF3-S10 CG9769 +eIF3-S9 Cctgamma +eIF3-S9 rin +eIF3-S9 RpS14a +eIF3-S9 RpS15Aa +eIF3-S9 RpS17 +eIF3-S9 Tcp-1eta +eIF3-S9 CG7033 +eIF3-S9 T-cp1 +eIF3-S9 CG10576 +eIF3-S9 pAbp +eIF3-S9 RpL10Ab +eIF3-S9 RpL12 +eIF3-S9 RpS12 +eIF3-S9 RpS10a +eIF3-S9 RpS27 +eIF3-S9 RpS10b +eIF3-S9 RpS18 +eIF3-S9 RpL11 +eIF3-S9 RpL13 +eIF3-S9 RpL3 +eIF3-S9 RpL4 +eIF3-S9 RpS16 +eIF3-S9 sta +eIF3-S9 RpS2 +eIF3-S9 RpS13 +eIF3-S9 RpS4 +eIF3-S9 RpS6 +eIF3-S9 RpS3 +eIF3-S9 CG5642 +eIF3-S9 eIF3-S8 +eIF3-S9 CG9769 +eIF3-S9 Trip1 +eIF3-S9 eIF3-S10 +Pdi Ugt +Pdi Ost48 +Pdi CG2852 +Pdi CG2918 +Pdi Hsc70-3 +epsilonCOP Dlic +epsilonCOP Klc +epsilonCOP Klp10A +epsilonCOP cpb +epsilonCOP cpa +epsilonCOP p115 +epsilonCOP deltaCOP +epsilonCOP zetaCOP +epsilonCOP alphaCOP +epsilonCOP betaCOP +beta'COP cpb +beta'COP cpa +beta'COP Dlic +beta'COP Klp10A +beta'COP Klc +beta'COP p115 +beta'COP zetaCOP +beta'COP alphaCOP +beta'COP deltaCOP +beta'COP betaCOP +beta'COP epsilonCOP +CaBP1 Ugt +CaBP1 Ost48 +CaBP1 CG2918 +CaBP1 Hsc70-3 +CaBP1 Pdi +Gp93 Cctgamma +Gp93 bai +Gp93 Hsp27 +Gp93 Hop +Gp93 Hsp70Ab +Gp93 CG11577 +Gp93 Pdi +Gp93 Hsc70-3 +Gp93 CG2918 +Gp93 CaBP1 +Tcp-1eta RpL12 +Tcp-1eta RpS6 +Tcp-1eta RpL13 +Tcp-1eta RpL10Ab +Tcp-1eta sta +Tcp-1eta RpS2 +Tcp-1eta RpS12 +Tcp-1eta RpS13 +Tcp-1eta CG10576 +Tcp-1eta RpS4 +Tcp-1eta RpS16 +Tcp-1eta Trip1 +Tcp-1eta Hop +Tcp-1eta ade3 +Tcp-1eta Tcp-1zeta +Tcp-1eta CG7033 +RpL13 mRpL12 +RpL13 CG7033 +RpL13 ade3 +RpL13 bonsai +RpL13 Srp19 +RpL13 mago +RpL13 CG5642 +RpL13 eIF3-S10 +RpL13 CG10576 +RpL13 Srp54k +RpL13 CG9769 +RpL13 Trip1 +RpL13 RpS15Aa +RpL13 RpL12 +RpL13 RpS12 +RpL13 RpS13 +RpL13 RpS6 +RpL13 RpS14a +RpL13 RpL10Ab +RpL13 RpS4 +RpL13 sta +RpL13 RpS17 +RpL13 RpS10b +RpL13 RpS16 +Rpt4 Prosalpha1 +Rpt4 arm +Rpt4 Rpt4R +Rpt4 Uch-L5 +Rpt4 Rpt6R +Rpt4 Rpn13 +Rpt4 Rpt1 +Rpt4 Rpt6 +Rpt4 Prosalpha6 +Rpt4 Prosbeta7 +Rpt4 Rpn6 +Rpt4 Rpt2 +Rpt4 Rpn3 +Rpt4 Prosalpha7 +Rpt4 Rpn2 +Rpt4 Rpn12 +Rpt4 Rpn5 +Rpt4 Rpt5 +RpS11 vig +RpS11 bic +RpS11 CG7033 +RpS11 Tctp +RpS11 Tcp-1eta +RpS11 eEF1delta +RpS11 mago +RpS11 EfTuM +RpS11 Srp72 +RpS11 bonsai +RpS11 mRpS10 +RpS11 CG5642 +RpS11 mRpS9 +RpS11 mRpL12 +RpS11 eIF3-S8 +RpS11 CG9769 +RpS11 eIF3-S10 +RpS11 Srp19 +RpS11 ade3 +RpS11 eIF3-S9 +RpS11 Srp54k +RpS11 Tango7 +RpS11 Trip1 +RpS11 RpL10Ab +RpS11 RpS23 +RpS11 RpL3 +RpS11 RpL12 +RpS11 RpL11 +RpS11 RpS14a +RpS11 RpL4 +RpS11 RpS18 +RpS11 RpS6 +RpS11 sta +RpS11 RpS10a +RpS11 RpS17 +RpS11 RpS2 +RpS11 RpL18A +RpS11 RpS12 +RpS11 RpS3 +RpS11 RpS13 +RpS11 RpS27 +RpS11 RpS10b +RpS11 RpL13 +RpS11 RpS16 +RpS11 RpS15Aa +RpS11 RpS4 +eIF3-S8 CG33123 +eIF3-S8 Tcp-1zeta +eIF3-S8 eEF1delta +eIF3-S8 pAbp +eIF3-S8 RpS14a +eIF3-S8 RpS17 +eIF3-S8 RpS15Aa +eIF3-S8 CG10576 +eIF3-S8 RpL12 +eIF3-S8 RpL10Ab +eIF3-S8 RpL11 +eIF3-S8 RpL13 +eIF3-S8 RpL3 +eIF3-S8 RpS12 +eIF3-S8 sta +eIF3-S8 RpS2 +eIF3-S8 RpS13 +eIF3-S8 RpS4 +eIF3-S8 RpS10b +eIF3-S8 RpS27 +eIF3-S8 RpS10a +eIF3-S8 RpS16 +eIF3-S8 RpL4 +eIF3-S8 RpS18 +eIF3-S8 RpS6 +eIF3-S8 RpS3 +eIF3-S8 eIF3-S10 +eIF3-S8 Trip1 +eIF3-S8 CG9769 +eIF3-S8 CG5642 +Hop Hsp60 +Hop Hsc70-3 +Hop Ubqn +CG1416 CG7033 +CG1416 Hsp60 +CG1416 Gp93 +CG1416 Cctgamma +CG1416 Hop +RpS6 RpS14a +RpS6 sta +RpS2 CG10576 +RpS2 ade3 +RpS2 bonsai +RpS2 CG7033 +RpS2 mago +RpS2 mRpL12 +RpS2 CG5642 +RpS2 eIF3-S10 +RpS2 Srp19 +RpS2 CG9769 +RpS2 Srp54k +RpS2 Trip1 +RpS2 RpS6 +RpS2 RpS15Aa +RpS2 RpL12 +RpS2 RpS14a +RpS2 RpS10b +RpS2 RpS16 +RpS2 RpL10Ab +RpS2 RpS17 +RpS2 sta +RpS2 RpS4 +RpS2 RpS13 +RpS2 RpL13 +RpS2 RpS12 +Prosalpha6 Trip1 +Prosalpha6 Tcp-1zeta +Prosalpha6 Uch-L5 +Prosalpha6 Rpn5 +Prosalpha6 Rpt4R +Prosalpha6 Rpt6 +Prosalpha6 Prosbeta7 +Prosalpha6 Rpn12 +ND-PDSW COX6B +ND-PDSW CG7603 +ND-PDSW ND-B16.6 +ND-PDSW CG11752 +NP15.6 CG7603 +NP15.6 COX4 +NP15.6 CG11752 +NP15.6 ND-51 +NP15.6 COX5A +NP15.6 ND-B16.6 +NP15.6 ND-PDSW +Klc Dlic +Klc betaCOP +Klc deltaCOP +Klc zetaCOP +Klc Klp10A +Khc Dlic +Khc zetaCOP +Khc epsilonCOP +Khc bai +Khc deltaCOP +Khc beta'COP +Khc syd +Khc betaCOP +Khc Klp10A +Khc betaTub56D +Khc Klc +RpL12 bonsai +RpL12 mago +RpL12 RpS16 +RpL12 sta +RpL12 RpS14a +RpL12 RpS6 +Rpt5 Ubqn +Rpt5 Cat +Rpt5 CalpB +Rpt5 CG6891 +Rpt5 Ect3 +Rpt5 PyK +Rpt5 Tcp-1eta +Rpt5 Ufd1-like +Rpt5 Uch-L5 +Rpt5 Rpt4R +Rpt5 Prosalpha6 +Rpt5 Prosbeta7 +Rpt5 Rpn5 +Rpt5 Rpt6 +Rpt5 Rpn12 +Prosalpha7 RpS16 +Prosalpha7 Cct5 +Prosalpha7 Tcp-1eta +Prosalpha7 CG7033 +Prosalpha7 Trip1 +Prosalpha7 CG8258 +Prosalpha7 arm +Prosalpha7 Uch-L5 +Prosalpha7 Rpt4R +Prosalpha7 Prosalpha1 +Prosalpha7 Rpt6R +Prosalpha7 Rpn12 +Prosalpha7 Rpt6 +Prosalpha7 Rpn6 +Prosalpha7 Rpt2 +Prosalpha7 Rpt1 +Prosalpha7 Prosbeta7 +Prosalpha7 Prosalpha6 +Prosalpha7 Rpn5 +Prosalpha7 Rpn2 +Prosalpha7 Rpt5 +Rpn6 Trip1 +Rpn6 RpS2 +Rpn6 Ubqn +Rpn6 CalpB +Rpn6 Cat +Rpn6 PyK +Rpn6 Ect3 +Rpn6 CG6891 +Rpn6 Uch-L5 +Rpn6 Prosalpha6 +Rpn6 Rpt4R +Rpn6 Prosbeta7 +Rpn6 Rpn2 +Rpn6 Rpt5 +Rpn6 Rpn12 +Rpn6 Rpn5 +Rpn6 Rpt2 +Rpn6 Rpt6R +Rpn6 Rpt6 +ATPsynbeta Rack1 +ATPsynbeta COX4 +ATPsynbeta COX6B +ATPsynbeta sta +ATPsynbeta Hsc70-5 +ATPsynbeta Eno +ATPsynbeta RpL4 +ATPsynbeta RpS3A +ATPsynbeta CG11876 +ATPsynbeta Mpcp +ATPsynbeta porin +ATPsynbeta Vha100-1 +ATPsynbeta RpS2 +ATPsynbeta Vha100-2 +ATPsynbeta Hsp83 +ATPsynbeta ND-51 +ATPsynbeta Vha68-2 +ATPsynbeta EfTuM +ATPsynbeta Mdh2 +ATPsynbeta CG9090 +ATPsynbeta COX5A +ATPsynbeta sun +ATPsynCF6 Vha100-1 +ATPsynCF6 Vha100-2 +ATPsynCF6 NP15.6 +ATPsynCF6 Vha68-2 +ATPsynCF6 ND-B16.6 +ATPsynCF6 COX6B +ATPsynCF6 CG11752 +ATPsynCF6 Cyt-c-p +ATPsynCF6 COX4 +ATPsynCF6 ND-PDSW +ATPsynCF6 COX5A +ATPsynCF6 sun +ATPsynCF6 ATPsynbeta +CG8258 RpS13 +CG8258 RpL11 +CG8258 eIF3-S9 +CG8258 Tango7 +CG8258 Hsp60C +CG8258 Rpn2 +CG8258 RpS3 +CG8258 RpL12 +CG8258 RpL3 +CG8258 Hsc70-5 +CG8258 RpL4 +CG8258 RpS4 +CG8258 RpS2 +CG8258 RpS16 +CG8258 Hsp60 +CG8258 CG10576 +CG8258 Cat +CG8258 CG6891 +CG8258 CalpB +CG8258 Ect3 +CG8258 Cp1 +CG8258 Pfk +CG8258 PyK +CG8258 ade3 +CG8258 Trip1 +CG8258 Rpt5 +CG8258 Rpn6 +CG8258 Rpn5 +CG8258 Hop +CG8258 Cct5 +CG8258 Cctgamma +CG8258 Tcp-1zeta +CG8258 CG7033 +CG8258 Tcp-1eta +CG8258 T-cp1 +RpS7 Rpt6R +RpS7 Rpt6 +RpS7 vig +RpS7 eEF1delta +RpS7 Rpt1 +RpS7 Rpt2 +RpS7 ade3 +RpS7 mRpS10 +RpS7 Srp72 +RpS7 mago +RpS7 bonsai +RpS7 Tctp +RpS7 CG5642 +RpS7 CG9769 +RpS7 eIF3-S10 +RpS7 Srp54k +RpS7 Srp19 +RpS7 Trip1 +RpS7 eIF3-S9 +RpS7 eIF3-S8 +RpS7 Tango7 +RpS7 RpS15Ab +RpS7 RpS10a +RpS7 RpS15Aa +RpS7 RpS14a +RpS7 RpL4 +RpS7 RpL13 +RpS7 sta +RpS7 RpS2 +RpS7 RpS23 +RpS7 RpS16 +RpS7 RpL10Ab +RpS7 RpS6 +RpS7 RpS18 +RpS7 RpL11 +RpS7 RpS12 +RpS7 RpS4 +RpS7 RpS3 +RpS7 RpS13 +RpS7 RpS27 +RpS7 RpL3 +RpS7 RpS10b +RpS7 RpS17 +RpS7 RpL18A +RpS7 RpS11 +RpS7 RpL12 +RpL10 mRpS10 +RpL10 ade3 +RpL10 Cct5 +RpL10 mRpS5 +RpL10 eEF1delta +RpL10 mRpL12 +RpL10 mRpS9 +RpL10 Ef1beta +RpL10 eIF4G +RpL10 Srp72 +RpL10 mago +RpL10 Rbp2 +RpL10 eIF3-S10 +RpL10 eIF3-S8 +RpL10 CG5642 +RpL10 CG10576 +RpL10 Tango7 +RpL10 Trip1 +RpL10 Srp19 +RpL10 mRpS7 +RpL10 eIF-2gamma +RpL10 Elf +RpL10 eIF-3p66 +RpL10 CG9769 +RpL10 eIF3-S9 +RpL10 Srp54k +RpL10 eRF1 +RpL10 RpS15Ab +RpL10 Tctp +RpL10 RpS14a +RpL10 RpS15Aa +RpL10 RpS10b +RpL10 RpS17 +RpL10 RpL13 +RpL10 RpS8 +RpL10 RpS16 +RpL10 sta +RpL10 RpL12 +RpL10 RpS3 +RpL10 RpL4 +RpL10 RpS27 +RpL10 RpL3 +RpL10 RpS12 +RpL10 RpL11 +RpL10 RpLP2 +RpL10 RpL18A +RpL10 RpL26 +RpL10 RpS4 +RpL10 Rack1 +RpL10 RpS3A +RpL10 RpS11 +RpL10 RpS13 +RpL10 RpS10a +RpL10 RpL30 +RpL10 RpS2 +RpL10 RpS18 +RpL10 RpS23 +RpL10 RpL10Ab +RpL10 RpS6 +RpL10 RpS7 +Aats-asp CG5642 +Aats-asp CG31739 +Aats-asp CG33123 +Aats-asp Aats-arg +Aats-asp Aats-ile +CG33123 Aats-arg +Aats-ile CG33123 +Aats-ile Aats-arg +RpS8 T-cp1 +RpS8 vig +RpS8 Rpn2 +RpS8 Cct5 +RpS8 eEF1delta +RpS8 mRpS10 +RpS8 CG8258 +RpS8 Tcp-1eta +RpS8 bonsai +RpS8 CG7033 +RpS8 ade3 +RpS8 CG10576 +RpS8 Srp72 +RpS8 mago +RpS8 Rbp2 +RpS8 Nacalpha +RpS8 CG5642 +RpS8 eIF-3p66 +RpS8 Srp54k +RpS8 Srp19 +RpS8 eIF3-S10 +RpS8 Tctp +RpS8 Trip1 +RpS8 Tango7 +RpS8 eIF3-S8 +RpS8 CG9769 +RpS8 eIF3-S9 +RpS8 RpL13 +RpS8 RpS17 +RpS8 RpL11 +RpS8 RpS12 +RpS8 RpL4 +RpS8 RpS3 +RpS8 RpS27 +RpS8 RpS18 +RpS8 RpS2 +RpS8 RpS13 +RpS8 RpS7 +RpS8 RpS6 +RpS8 sta +RpS8 RpL10Ab +RpS8 RpS10b +RpS8 RpS10a +RpS8 RpS14a +RpS8 RpS4 +RpS8 RpL12 +RpS8 RpS11 +RpS8 RpL18A +RpS8 RpLP2 +RpS8 RpL3 +RpS8 RpS23 +RpS8 RpS15Ab +RpS8 RpS15Aa +RpS8 RpS16 +Rpt6 Ubqn +Rpt6 RpS4 +Rpt6 Uch-L5 +Rpt6 Rpt4R +Rpt6 Rpn12 +Rpn3 bel +Rpn3 Ubqn +Rpn3 CalpB +Rpn3 Ect3 +Rpn3 Pfk +Rpn3 CG6891 +Rpn3 Cat +Rpn3 Cp1 +Rpn3 PyK +Rpn3 arm +Rpn3 cathD +Rpn3 CG8258 +Rpn3 Prosalpha1 +Rpn3 Uch-L5 +Rpn3 TER94 +Rpn3 Prosalpha6 +Rpn3 Prosbeta7 +Rpn3 Prosalpha7 +Rpn3 Rpt4R +Rpn3 Rpn2 +Rpn3 Rpt6R +Rpn3 Rpt2 +Rpn3 Rpn5 +Rpn3 Rpt1 +Rpn3 Rpt5 +Rpn3 Rpn6 +Rpn3 Rpn13 +Rpn3 Rpn12 +Rpn3 Rpt6 +RpL30 vig +RpL30 Nacalpha +RpL30 CG10576 +RpL30 Srp72 +RpL30 eIF4G +RpL30 bonsai +RpL30 eIF3-S8 +RpL30 mago +RpL30 eIF3-S10 +RpL30 CG5642 +RpL30 mRpS5 +RpL30 Rbp2 +RpL30 mRpS10 +RpL30 eIF-3p66 +RpL30 Tctp +RpL30 eIF3-S9 +RpL30 Tango7 +RpL30 CG9769 +RpL30 Trip1 +RpL30 Srp54k +RpL30 Srp19 +RpL30 RpS15Aa +RpL30 RpS15Ab +RpL30 RpS10a +RpL30 RpL18A +RpL30 RpS27 +RpL30 RpL12 +RpL30 RpS12 +RpL30 RpS18 +RpL30 RpS17 +RpL30 RpS2 +RpL30 RpL11 +RpL30 RpS7 +RpL30 RpS14a +RpL30 RpS4 +RpL30 RpS8 +RpL30 RpS10b +RpL30 RpLP2 +RpL30 RpL13 +RpL30 RpS16 +RpL30 RpS3 +RpL30 RpS13 +RpL30 RpS11 +RpL30 RpS6 +RpL30 RpL3 +RpL30 RpL10Ab +RpL30 RpS23 +RpL30 RpL4 +RpL30 sta +RpS10a Tctp +RpS10a eEF1delta +RpS10a bonsai +RpS10a mRpS10 +RpS10a RpS10b +RpS10a Srp72 +RpS10a mago +RpS10a CG5642 +RpS10a CG9769 +RpS10a eIF3-S10 +RpS10a Trip1 +RpS10a Srp19 +RpS10a Srp54k +RpS10a RpS15Aa +RpS10a sta +RpS10a RpS2 +RpS10a RpS3 +RpS10a RpL10Ab +RpS10a RpS4 +RpS10a RpS13 +RpS10a RpS6 +RpS10a RpS12 +RpS10a RpS16 +RpS10a RpS14a +RpS10a RpS27 +RpS10a RpL13 +RpS10a RpL12 +RpS10a RpS17 +RpS10a RpL3 +RpS17 CG7033 +RpS17 CG5642 +RpS17 bonsai +RpS17 RpS6 +RpS17 RpL12 +RpS17 RpL10Ab +RpS17 RpS15Aa +RpS17 RpS12 +RpS17 sta +RpS17 RpS14a +RpS17 RpS4 +RpS17 RpS16 +RpS17 RpS10b +RpLP0 ATPsynbeta +RpLP0 Tcp-1zeta +RpLP0 T-cp1 +RpLP0 CG7033 +RpLP0 EfTuM +RpLP0 Nacalpha +RpLP0 bonsai +RpLP0 Tcp-1eta +RpLP0 mRpS10 +RpLP0 ade3 +RpLP0 Cct5 +RpLP0 mRpS5 +RpLP0 eEF1delta +RpLP0 Srp72 +RpLP0 eIF4G +RpLP0 mRpS9 +RpLP0 Rbp2 +RpLP0 mago +RpLP0 CG5642 +RpLP0 eIF3-S8 +RpLP0 Elf +RpLP0 eIF3-S10 +RpLP0 CG10576 +RpLP0 mRpL12 +RpLP0 mRpS7 +RpLP0 Tctp +RpLP0 Srp19 +RpLP0 Tango7 +RpLP0 eIF-2gamma +RpLP0 eIF-3p66 +RpLP0 CG9769 +RpLP0 eIF3-S9 +RpLP0 RpS15Aa +RpLP0 Srp54k +RpLP0 Trip1 +RpLP0 RpS15Ab +RpLP0 eRF1 +RpLP0 RpS14a +RpLP0 RpS10a +RpLP0 Ef1beta +RpLP0 RpS6 +RpLP0 RpS13 +RpLP0 RpS23 +RpLP0 sta +RpLP0 RpS11 +RpLP0 RpS16 +RpLP0 RpS7 +RpLP0 RpS27 +RpLP0 RpL10 +RpLP0 RpS3 +RpLP0 RpS10b +RpLP0 RpS8 +RpLP0 RpLP2 +RpLP0 RpS12 +RpLP0 RpL3 +RpLP0 Rack1 +RpLP0 RpL13 +RpLP0 RpL26 +RpLP0 RpL30 +RpLP0 RpL18A +RpLP0 RpL4 +RpLP0 RpL10Ab +RpLP0 RpS18 +RpLP0 RpS3A +RpLP0 RpL12 +RpLP0 RpL11 +RpLP0 RpS2 +RpLP0 RpS4 +RpLP0 RpS17 +eRF1 bonsai +eRF1 T-cp1 +eRF1 eIF3-S8 +eRF1 CG8635 +eRF1 CG7033 +eRF1 Cctgamma +eRF1 mRpS5 +eRF1 mRpS10 +eRF1 wuho +eRF1 RpS15Ab +eRF1 RpS14a +eRF1 RpS15Aa +eRF1 eIF3-S10 +eRF1 pAbp +eRF1 CG10576 +eRF1 RpS17 +eRF1 eIF-2gamma +eRF1 Trip1 +eRF1 mago +eRF1 RpLP2 +eRF1 eIF3-S9 +eRF1 eIF4G +eRF1 Rack1 +eRF1 RpS10a +eRF1 RpS10b +eRF1 RpL3 +eRF1 RpL10Ab +eRF1 RpS27 +eRF1 RpS12 +eRF1 RpS7 +eRF1 RpS18 +eRF1 RpL12 +eRF1 RpS23 +eRF1 RpL18A +eRF1 RpS16 +eRF1 RpL30 +eRF1 RpS11 +eRF1 RpS3 +eRF1 RpS3A +eRF1 sta +eRF1 RpS13 +eRF1 RpS6 +eRF1 RpS8 +eRF1 RpL4 +eRF1 RpS4 +eRF1 RpL11 +eRF1 RpS2 +eRF1 RpL13 +Elf ade5 +Elf mRpS7 +Elf RpS15Aa +Elf RpS15Ab +Elf T-cp1 +Elf eIF3-S9 +Elf Cctgamma +Elf RpS17 +Elf CG33123 +Elf Cct5 +Elf RpS14a +Elf eIF3-S8 +Elf eIF3-S10 +Elf eIF-2gamma +Elf RpL10Ab +Elf RpLP2 +Elf mago +Elf RpL13 +Elf RpL3 +Elf RpL30 +Elf RpL18A +Elf RpL12 +Elf RpL11 +Elf Rack1 +Elf RpL4 +Elf RpS4 +Elf RpS10b +Elf RpS10a +Elf RpS12 +Elf eIF4G +Elf RpS11 +Elf RpS13 +Elf RpS27 +Elf RpS8 +Elf RpS16 +Elf RpS3 +Elf sta +Elf RpS23 +Elf RpS18 +Elf RpS7 +Elf RpS6 +Elf RpS3A +Elf RpS2 +Elf pAbp +Elf eRF1 +RpS15Aa bonsai +RpS15Aa RpL12 +RpS15Aa sta +RpS15Aa RpS6 +RpS15Aa RpS16 +RpS15Aa RpS14a +RpS27 vig +RpS27 Srp72 +RpS27 mRpS10 +RpS27 bonsai +RpS27 mago +RpS27 Tctp +RpS27 CG5642 +RpS27 CG9769 +RpS27 Trip1 +RpS27 eIF3-S10 +RpS27 Srp19 +RpS27 Srp54k +RpS27 RpS13 +RpS27 RpS17 +RpS27 RpS4 +RpS27 RpS2 +RpS27 RpL13 +RpS27 RpL12 +RpS27 RpS6 +RpS27 RpS12 +RpS27 RpS10b +RpS27 sta +RpS27 RpS16 +RpS27 RpS14a +RpS27 RpL10Ab +RpS27 RpL3 +RpS27 RpS3 +RpS27 RpS15Aa +RpS18 Rpt6R +RpS18 Rpt6 +RpS18 Tcp-1eta +RpS18 Rpt2 +RpS18 vig +RpS18 ade3 +RpS18 Srp72 +RpS18 mago +RpS18 bonsai +RpS18 Tctp +RpS18 CG5642 +RpS18 mRpL12 +RpS18 mRpS10 +RpS18 mRpS9 +RpS18 eIF3-S10 +RpS18 CG9769 +RpS18 Srp19 +RpS18 Srp54k +RpS18 Trip1 +RpS18 RpS12 +RpS18 RpS2 +RpS18 RpS16 +RpS18 sta +RpS18 RpL10Ab +RpS18 RpL4 +RpS18 RpS14a +RpS18 RpS17 +RpS18 RpS3 +RpS18 RpS4 +RpS18 RpL3 +RpS18 RpS10a +RpS18 RpS13 +RpS18 RpS10b +RpS18 RpL13 +RpS18 RpL12 +RpS18 RpS15Aa +RpS18 RpS6 +RpS18 RpS27 +RpS10b bonsai +RpS10b mago +RpS10b RpS15Aa +RpS10b RpS16 +RpS10b RpS14a +RpS10b RpL12 +RpS10b sta +RpS10b RpS6 +RpS3A CG7048 +RpS3A Nacalpha +RpS3A vig +RpS3A eEF1delta +RpS3A Cct5 +RpS3A CG8258 +RpS3A CG7033 +RpS3A Tcp-1eta +RpS3A Srp72 +RpS3A mRpS5 +RpS3A Rbp2 +RpS3A mRpS10 +RpS3A eIF4G +RpS3A bonsai +RpS3A mago +RpS3A CG5642 +RpS3A eIF-3p66 +RpS3A eIF3-S8 +RpS3A Srp54k +RpS3A eIF3-S10 +RpS3A Srp19 +RpS3A CG9769 +RpS3A ade3 +RpS3A Trip1 +RpS3A Tango7 +RpS3A eIF3-S9 +RpS3A Tctp +RpS3A RpS12 +RpS3A RpS13 +RpS3A RpS2 +RpS3A RpS17 +RpS3A RpS15Ab +RpS3A RpL13 +RpS3A RpS15Aa +RpS3A RpS8 +RpS3A RpS10a +RpS3A RpS4 +RpS3A RpL11 +RpS3A RpS23 +RpS3A RpS7 +RpS3A RpLP2 +RpS3A sta +RpS3A RpS6 +RpS3A RpL12 +RpS3A RpS11 +RpS3A RpL30 +RpS3A RpL3 +RpS3A RpS3 +RpS3A RpS14a +RpS3A RpL10Ab +RpS3A RpL18A +RpS3A RpS27 +RpS3A RpL4 +RpS3A RpS16 +RpS3A RpS18 +RpS3A RpS10b +RpS16 mago +RpS16 RpS14a +RpS16 RpS6 +RpS16 sta +RpS15Ab vig +RpS15Ab CG9769 +RpS15Ab ade3 +RpS15Ab EfTuM +RpS15Ab Srp19 +RpS15Ab eIF3-S9 +RpS15Ab eIF3-S10 +RpS15Ab eIF3-S8 +RpS15Ab bonsai +RpS15Ab mRpS10 +RpS15Ab Srp54k +RpS15Ab Tango7 +RpS15Ab Tctp +RpS15Ab mRpS9 +RpS15Ab mRpL12 +RpS15Ab RpS15Aa +RpS15Ab RpS12 +RpS15Ab RpL10Ab +RpS15Ab RpL13 +RpS15Ab RpL18A +RpS15Ab RpL12 +RpS15Ab RpL3 +RpS15Ab RpL4 +RpS15Ab RpS10a +RpS15Ab RpL11 +RpS15Ab RpS6 +RpS15Ab RpS2 +RpS15Ab RpS13 +RpS15Ab RpS23 +RpS15Ab RpS18 +RpS15Ab RpS3 +RpS15Ab RpS10b +RpS15Ab RpS17 +RpS15Ab RpS14a +RpS15Ab sta +RpS15Ab RpS11 +RpS15Ab RpS27 +RpS15Ab RpS4 +RpS15Ab RpS16 +RpL3 eEF1delta +RpL3 bonsai +RpL3 Tcp-1zeta +RpL3 Tcp-1eta +RpL3 CG7033 +RpL3 Tctp +RpL3 mago +RpL3 Srp19 +RpL3 mRpL12 +RpL3 mRpS9 +RpL3 eIF3-S10 +RpL3 CG10576 +RpL3 CG5642 +RpL3 CG9769 +RpL3 Srp54k +RpL3 Trip1 +RpL3 ade3 +RpL3 RpS14a +RpL3 RpS10b +RpL3 RpS15Aa +RpL3 RpS13 +RpL3 RpS16 +RpL3 RpS2 +RpL3 RpS4 +RpL3 RpS17 +RpL3 sta +RpL3 RpL12 +RpL3 RpL13 +RpL3 RpL10Ab +RpL3 RpS12 +RpL3 RpS6 +Tcp-1zeta Tim9a +Tcp-1zeta RpL12 +Tcp-1zeta Hsp60 +Tcp-1zeta sta +Tcp-1zeta CG7033 +T-cp1 RpS13 +T-cp1 RpS2 +T-cp1 RpL13 +T-cp1 Hop +T-cp1 sta +T-cp1 RpL12 +T-cp1 Aats-ile +T-cp1 RpS4 +T-cp1 RpL3 +T-cp1 CG10576 +T-cp1 CG7033 +T-cp1 Cctgamma +T-cp1 Tcp-1eta +T-cp1 Tcp-1zeta +RpS3 Tcp-1zeta +RpS3 vig +RpS3 CG7033 +RpS3 eEF1delta +RpS3 ade3 +RpS3 Tcp-1eta +RpS3 Srp72 +RpS3 mago +RpS3 bonsai +RpS3 mRpS10 +RpS3 mRpS9 +RpS3 mRpL12 +RpS3 Srp19 +RpS3 CG5642 +RpS3 Srp54k +RpS3 CG9769 +RpS3 Tctp +RpS3 eIF3-S10 +RpS3 RpS15Aa +RpS3 RpL13 +RpS3 RpS6 +RpS3 RpS2 +RpS3 RpL12 +RpS3 RpL10Ab +RpS3 RpS14a +RpS3 RpS12 +RpS3 RpS13 +RpS3 RpS16 +RpS3 RpS10b +RpS3 sta +RpS3 RpS4 +RpS3 RpL3 +RpS3 RpS17 +RpS3 Trip1 +RpL26 CG10576 +RpL26 eEF1delta +RpL26 EfTuM +RpL26 Nacalpha +RpL26 bonsai +RpL26 ade3 +RpL26 mRpS10 +RpL26 mRpS5 +RpL26 Tctp +RpL26 eIF4G +RpL26 CG5642 +RpL26 mago +RpL26 eIF3-S10 +RpL26 Rbp2 +RpL26 mRpS9 +RpL26 eIF3-S9 +RpL26 mRpL12 +RpL26 eIF-3p66 +RpL26 eIF3-S8 +RpL26 Srp72 +RpL26 eIF-2gamma +RpL26 mRpS7 +RpL26 CG9769 +RpL26 Elf +RpL26 Tango7 +RpL26 Trip1 +RpL26 Ef1beta +RpL26 Srp19 +RpL26 eRF1 +RpL26 Srp54k +RpL26 RpS15Aa +RpL26 RpS15Ab +RpL26 RpL10Ab +RpL26 RpL11 +RpL26 RpL3 +RpL26 RpS13 +RpL26 RpS2 +RpL26 RpS6 +RpL26 RpS4 +RpL26 RpS27 +RpL26 RpL30 +RpL26 RpL18A +RpL26 RpS12 +RpL26 RpS10a +RpL26 RpS18 +RpL26 RpS3A +RpL26 RpS8 +RpL26 RpS17 +RpL26 sta +RpL26 RpS14a +RpL26 RpS11 +RpL26 RpS23 +RpL26 RpL13 +RpL26 RpL12 +RpL26 RpS10b +RpL26 Rack1 +RpL26 RpL4 +RpL26 RpS3 +RpL26 RpS7 +RpL26 RpS16 +RpL26 RpLP2 +RpS12 mago +RpS12 RpS14a +RpS12 RpS15Aa +RpS12 RpS10b +RpS12 RpS6 +RpS12 RpS16 +RpS12 RpL12 +RpS12 sta +CG5642 RpL12 +CG5642 RpS6 +CG5642 sta +CG5642 RpS10b +CG5642 RpS12 +CG5642 RpS16 +CG5642 RpS4 +CG9769 RpS15Aa +CG9769 RpS17 +CG9769 RpS12 +CG9769 RpL12 +CG9769 RpS10b +CG9769 RpL10Ab +CG9769 RpS4 +CG9769 sta +CG9769 RpS6 +CG9769 RpS16 +CG9769 CG5642 +CG7033 RpS14a +CG7033 sta +Cct5 RpS13 +Cct5 ade3 +Cct5 Prosalpha6 +Cct5 Hsc70-5 +Cct5 Rpt6 +Cct5 RpS3 +Cct5 Ref1 +Cct5 RpS16 +Cct5 RpL12 +Cct5 RpL13 +Cct5 CG5642 +Cct5 RpL18A +Cct5 RpS6 +Cct5 sta +Cct5 RpL11 +Cct5 Aats-ile +Cct5 pont +Cct5 Trip1 +Cct5 RpS4 +Cct5 Tango7 +Cct5 eIF3-S9 +Cct5 RpL3 +Cct5 RpS2 +Cct5 CG10576 +Cct5 RpL4 +Cct5 Hop +Cct5 Cctgamma +Cct5 Tcp-1eta +Cct5 T-cp1 +Cct5 Tcp-1zeta +Cct5 CG7033 +RpLP2 CG10576 +RpLP2 bonsai +RpLP2 mRpS10 +RpLP2 Srp72 +RpLP2 Srp54k +RpLP2 Srp19 +RpLP2 mago +RpLP2 eIF3-S8 +RpLP2 eIF3-S10 +RpLP2 CG5642 +RpLP2 Rbp2 +RpLP2 Tango7 +RpLP2 eIF3-S9 +RpLP2 eIF-3p66 +RpLP2 Nacalpha +RpLP2 RpS15Aa +RpLP2 CG9769 +RpLP2 Trip1 +RpLP2 Tctp +RpLP2 RpS10a +RpLP2 RpS10b +RpLP2 RpS27 +RpLP2 RpS3 +RpLP2 RpS13 +RpLP2 RpS18 +RpLP2 RpL4 +RpLP2 RpS7 +RpLP2 RpL13 +RpLP2 RpS23 +RpLP2 RpL10Ab +RpLP2 RpS15Ab +RpLP2 RpS6 +RpLP2 RpS14a +RpLP2 RpS2 +RpLP2 RpL3 +RpLP2 RpL12 +RpLP2 RpL18A +RpLP2 RpL11 +RpLP2 RpS11 +RpLP2 sta +RpLP2 RpS16 +RpLP2 RpS17 +RpLP2 RpS12 +RpLP2 RpS4 +RpS23 CG7033 +RpS23 vig +RpS23 eEF1delta +RpS23 bonsai +RpS23 mRpS10 +RpS23 Srp72 +RpS23 mago +RpS23 CG5642 +RpS23 mRpL12 +RpS23 mRpS9 +RpS23 Tctp +RpS23 eIF3-S9 +RpS23 eIF3-S8 +RpS23 eIF3-S10 +RpS23 CG9769 +RpS23 Trip1 +RpS23 Srp19 +RpS23 Srp54k +RpS23 RpS16 +RpS23 RpS27 +RpS23 RpL18A +RpS23 RpS10b +RpS23 RpS15Aa +RpS23 RpS12 +RpS23 RpS3 +RpS23 RpS18 +RpS23 RpS14a +RpS23 RpL11 +RpS23 RpL3 +RpS23 RpS2 +RpS23 sta +RpS23 RpS6 +RpS23 RpL10Ab +RpS23 RpL12 +RpS23 RpL13 +RpS23 RpS17 +RpS23 RpL4 +RpS23 RpS4 +RpS23 RpS10a +RpS23 RpS13 +RpS14a sta +Rack1 vig +Rack1 bel +Rack1 Prosalpha7 +Rack1 Rpn6 +Rack1 Rpn2 +Rack1 eEF1delta +Rack1 Cctgamma +Rack1 Eno +Rack1 pAbp +Rack1 Nacalpha +Rack1 eIF3-S8 +Rack1 CG10576 +Rack1 CG9769 +Rack1 ade3 +Rack1 Dp1 +Rack1 T-cp1 +Rack1 Tcp-1zeta +Rack1 Srp54k +Rack1 Tcp-1eta +Rack1 Srp19 +Rack1 CG8258 +Rack1 CG7033 +Rack1 Cct5 +Rack1 mRpS5 +Rack1 bonsai +Rack1 mRpS10 +Rack1 eIF3-S10 +Rack1 Tctp +Rack1 eIF3-S9 +Rack1 eIF-2gamma +Rack1 Trip1 +Rack1 RpS15Ab +Rack1 Tango7 +Rack1 RpS15Aa +Rack1 RpS10a +Rack1 RpS14a +Rack1 RpS10b +Rack1 RpL10Ab +Rack1 RpS8 +Rack1 RpL3 +Rack1 RpS11 +Rack1 RpS17 +Rack1 RpS2 +Rack1 RpS16 +Rack1 RpS13 +Rack1 RpS12 +Rack1 RpS4 +Rack1 RpS3A +Rack1 RpS3 +Rack1 RpS18 +Rack1 RpS23 +Rack1 RpS6 +Rack1 RpL12 +Rack1 RpL13 +Rack1 RpS27 +Rack1 RpL11 +Rack1 RpL18A +Rack1 RpL30 +Rack1 RpS7 +Rack1 RpLP2 +Rack1 RpL4 +Rack1 sta +RpL11 CG7033 +RpL11 vig +RpL11 eEF1delta +RpL11 Tcp-1eta +RpL11 mago +RpL11 eIF3-S10 +RpL11 CG10576 +RpL11 CG5642 +RpL11 bonsai +RpL11 mRpS10 +RpL11 Srp72 +RpL11 mRpS9 +RpL11 Tctp +RpL11 mRpL12 +RpL11 CG9769 +RpL11 Srp19 +RpL11 Srp54k +RpL11 Trip1 +RpL11 RpS15Aa +RpL11 RpS13 +RpL11 RpS18 +RpL11 RpL3 +RpL11 RpL12 +RpL11 RpS10b +RpL11 RpS17 +RpL11 RpL10Ab +RpL11 RpS10a +RpL11 RpL13 +RpL11 RpS4 +RpL11 RpS16 +RpL11 RpS12 +RpL11 RpS27 +RpL11 RpS2 +RpL11 RpS3 +RpL11 RpS6 +RpL11 RpS14a +RpL11 RpL4 +RpL11 sta +Rpn12 Ubqn +Prosbeta7 Uba2 +Prosbeta7 CG7033 +Prosbeta7 Uch-L5 +Prosbeta7 Rpt4R +Prosbeta7 Rpn5 +Prosbeta7 Rpt6 +Prosbeta7 Rpn12 +RpS13 CG10576 +RpS13 CG7033 +RpS13 mRpL12 +RpS13 ade3 +RpS13 mago +RpS13 CG5642 +RpS13 eIF3-S10 +RpS13 CG9769 +RpS13 Srp19 +RpS13 Srp54k +RpS13 Trip1 +RpS13 RpL10Ab +RpS13 RpS15Aa +RpS13 RpS12 +RpS13 RpS17 +RpS13 RpL12 +RpS13 RpS10b +RpS13 sta +RpS13 RpS16 +RpS13 RpS4 +RpS13 RpS6 +RpS13 RpS14a +RpL18A Tcp-1zeta +RpL18A vig +RpL18A CG7033 +RpL18A eEF1delta +RpL18A Tcp-1eta +RpL18A ade3 +RpL18A mRpS10 +RpL18A mago +RpL18A bonsai +RpL18A CG10576 +RpL18A CG5642 +RpL18A eIF3-S8 +RpL18A eIF3-S10 +RpL18A Srp72 +RpL18A eIF3-S9 +RpL18A Tctp +RpL18A Srp19 +RpL18A CG9769 +RpL18A Srp54k +RpL18A Trip1 +RpL18A RpS15Aa +RpL18A RpS14a +RpL18A sta +RpL18A RpL10Ab +RpL18A RpS2 +RpL18A RpL3 +RpL18A RpS17 +RpL18A RpL4 +RpL18A RpL13 +RpL18A RpS4 +RpL18A RpL11 +RpL18A RpS10a +RpL18A RpS6 +RpL18A RpL12 +RpL18A RpS18 +RpL18A RpS16 +RpL18A RpS12 +RpL18A RpS27 +RpL18A RpS10b +RpL18A RpS3 +RpL18A RpS13 +RpS4 Tcp-1zeta +RpS4 CG7033 +RpS4 mago +RpS4 bonsai +RpS4 RpS15Aa +RpS4 RpL12 +RpS4 sta +RpS4 RpS10b +RpS4 RpS14a +RpS4 RpS16 +RpS4 RpS6 +RpS4 RpS12 +RpL4 vig +RpL4 bonsai +RpL4 Cctgamma +RpL4 mRpS10 +RpL4 Tcp-1eta +RpL4 Tctp +RpL4 T-cp1 +RpL4 CG7033 +RpL4 Tcp-1zeta +RpL4 ade3 +RpL4 mRpS9 +RpL4 mRpL12 +RpL4 Srp72 +RpL4 mago +RpL4 Srp19 +RpL4 CG5642 +RpL4 CG10576 +RpL4 Srp54k +RpL4 CG9769 +RpL4 eIF3-S10 +RpL4 RpS15Aa +RpL4 RpS10a +RpL4 RpL10Ab +RpL4 Trip1 +RpL4 RpS16 +RpL4 RpS6 +RpL4 RpS12 +RpL4 RpS14a +RpL4 RpS2 +RpL4 RpL12 +RpL4 RpS27 +RpL4 sta +RpL4 RpS13 +RpL4 RpS10b +RpL4 RpS3 +RpL4 RpS17 +RpL4 RpL13 +RpL4 RpL3 +RpL4 RpS4 \ No newline at end of file diff --git a/www/Examples/TP53/string_expression_colors.txt b/www/Examples/TP53/string_expression_colors.txt new file mode 100644 index 0000000..d049f2d --- /dev/null +++ b/www/Examples/TP53/string_expression_colors.txt @@ -0,0 +1,10 @@ +CDKN1A blue +TP53 blue +MDM4 blue +BCL2L1 red +CHEK2 red +ATM red +TP53BP2 red +CDKN2A blue +EP300 red +CREBBP red diff --git a/www/Examples/TP53/string_interactions.txt b/www/Examples/TP53/string_interactions.txt new file mode 100644 index 0000000..14e1e05 --- /dev/null +++ b/www/Examples/TP53/string_interactions.txt @@ -0,0 +1,47 @@ +Source Target Weight +CDKN1A TP53 70 +TP53 MDM2 2 +MDM4 TP53 3 +BCL2L1 TP53 4 +CHEK2 ATM 5 +TP53 EP300 20 +ATM TP53 7 +TP53 CREBB 1 +MDM4 MDM2 1 +CHEK2 TP53 1 +TP53BP2 TP53 1 +CDKN2A TP53 1 +CDKN2A MDM2 1 +ATM MDM2 2 +EP300 CREBBP 1 +MDM4 ATM 2 +CHEK2 MDM4 1 +CHEK2 MDM2 1 +CDKN2A CDKN1A 1 +CDKN2A MDM4 1 +EP300 MDM2 1 +CDKN1A EP300 1 +CREBBP MDM2 1 +CDKN1A MDM2 1 +CDKN2A BCL2L1 1 +BCL2L1 MDM2 2 +CDKN2A ATM 1 +CDKN1A MDM4 1 +CDKN1A CHEK2 1 +CDKN1A ATM 2 +CDKN1A CREBBP 1 +CDKN1A BCL2L1 1 +CDKN2A CHEK2 1 +ATM EP300 1 +TP53BP2 BCL2L1 3 +CDKN2A EP300 3 +BCL2L1 ATM 3 +MDM4 EP300 1 +ATM CREBBP 1 +CHEK2 EP300 1 +TP53BP2 MDM2 1 +CHEK2 BCL2L1 1 +CDKN2A CREBBP 1 +TP53BP2 EP300 1 +BCL2L1 EP300 1 +MDM4 BCL2L1 1 diff --git a/www/Examples/TP53/string_interactions_groups_comma_duplicate.txt b/www/Examples/TP53/string_interactions_groups_comma_duplicate.txt new file mode 100644 index 0000000..1d41847 --- /dev/null +++ b/www/Examples/TP53/string_interactions_groups_comma_duplicate.txt @@ -0,0 +1,5 @@ +Group-1 BCL2L1,MDM4,MDM2,CHEK2 +Group-2 CDKN2A,ATM,TP53BP2,MDM2 +Group-3 CHEK2,MDM2 +Group-4 TP53,EP300 +Group-5 MDM4,MDM2 diff --git a/www/Figures/Banner.png b/www/Figures/Banner.png new file mode 100644 index 0000000..6d5e4d4 Binary files /dev/null and b/www/Figures/Banner.png differ diff --git a/www/Figures/Community_detection_algorithms.png b/www/Figures/Community_detection_algorithms.png new file mode 100644 index 0000000..dfb70d2 Binary files /dev/null and b/www/Figures/Community_detection_algorithms.png differ diff --git a/www/Figures/Convex_Hulls.PNG b/www/Figures/Convex_Hulls.PNG new file mode 100644 index 0000000..c6fb6f4 Binary files /dev/null and b/www/Figures/Convex_Hulls.PNG differ diff --git a/www/Figures/Convex_Hulls_3D.PNG b/www/Figures/Convex_Hulls_3D.PNG new file mode 100644 index 0000000..b35b600 Binary files /dev/null and b/www/Figures/Convex_Hulls_3D.PNG differ diff --git a/www/Figures/Expression_colors.png b/www/Figures/Expression_colors.png new file mode 100644 index 0000000..cf94364 Binary files /dev/null and b/www/Figures/Expression_colors.png differ diff --git a/www/Figures/Network_view_interactive.png b/www/Figures/Network_view_interactive.png new file mode 100644 index 0000000..185500d Binary files /dev/null and b/www/Figures/Network_view_interactive.png differ diff --git a/www/Figures/Pies.PNG b/www/Figures/Pies.PNG new file mode 100644 index 0000000..bdbb98e Binary files /dev/null and b/www/Figures/Pies.PNG differ diff --git a/www/Figures/Strategy_1.png b/www/Figures/Strategy_1.png new file mode 100644 index 0000000..817f303 Binary files /dev/null and b/www/Figures/Strategy_1.png differ diff --git a/www/Figures/Strategy_2.png b/www/Figures/Strategy_2.png new file mode 100644 index 0000000..829d865 Binary files /dev/null and b/www/Figures/Strategy_2.png differ diff --git a/www/Figures/Strategy_3.png b/www/Figures/Strategy_3.png new file mode 100644 index 0000000..bcbf20a Binary files /dev/null and b/www/Figures/Strategy_3.png differ diff --git a/www/Figures/Topology.PNG b/www/Figures/Topology.PNG new file mode 100644 index 0000000..45e9706 Binary files /dev/null and b/www/Figures/Topology.PNG differ diff --git a/www/Figures/Topology_comparisons.PNG b/www/Figures/Topology_comparisons.PNG new file mode 100644 index 0000000..c9f7c2e Binary files /dev/null and b/www/Figures/Topology_comparisons.PNG differ diff --git a/www/Figures/Upload-1.png b/www/Figures/Upload-1.png new file mode 100644 index 0000000..1f26d89 Binary files /dev/null and b/www/Figures/Upload-1.png differ diff --git a/www/Figures/Upload-2.png b/www/Figures/Upload-2.png new file mode 100644 index 0000000..c99c6f0 Binary files /dev/null and b/www/Figures/Upload-2.png differ diff --git a/www/Figures/Upload-3.png b/www/Figures/Upload-3.png new file mode 100644 index 0000000..b552e97 Binary files /dev/null and b/www/Figures/Upload-3.png differ diff --git a/www/Figures/venn.png b/www/Figures/venn.png new file mode 100644 index 0000000..1f7a71a Binary files /dev/null and b/www/Figures/venn.png differ diff --git a/www/annot_table.css b/www/annot_table.css new file mode 100644 index 0000000..ae595c4 --- /dev/null +++ b/www/annot_table.css @@ -0,0 +1,600 @@ +.x1{background-color: #1B9E77;} +table.dataTable tr.selected td.x1{background-color: #1B9E77 !important;} +.x2{background-color: #D95F02;} +table.dataTable tr.selected td.x2{background-color: #D95F02 !important;} +.x3{background-color: #7570B3;} +table.dataTable tr.selected td.x3{background-color: #7570B3 !important;} +.x4{background-color: #E7298A;} +table.dataTable tr.selected td.x4{background-color: #E7298A !important;} +.x5{background-color: #66A61E;} +table.dataTable tr.selected td.x5{background-color: #66A61E !important;} +.x6{background-color: #E6AB02;} +table.dataTable tr.selected td.x6{background-color: #E6AB02 !important;} +.x7{background-color: #A6761D;} +table.dataTable tr.selected td.x7{background-color: #A6761D !important;} +.x8{background-color: #666666;} +table.dataTable tr.selected td.x8{background-color: #666666 !important;} +.x9{background-color: #7FC97F;} +table.dataTable tr.selected td.x9{background-color: #7FC97F !important;} +.x10{background-color: #BEAED4;} +table.dataTable tr.selected td.x10{background-color: #BEAED4 !important;} +.x11{background-color: #FDC086;} +table.dataTable tr.selected td.x11{background-color: #FDC086 !important;} +.x12{background-color: #FFFF99;} +table.dataTable tr.selected td.x12{background-color: #FFFF99 !important;} +.x13{background-color: #386CB0;} +table.dataTable tr.selected td.x13{background-color: #386CB0 !important;} +.x14{background-color: #F0027F;} +table.dataTable tr.selected td.x14{background-color: #F0027F !important;} +.x15{background-color: #BF5B17;} +table.dataTable tr.selected td.x15{background-color: #BF5B17 !important;} +.x16{background-color: #A6CEE3;} +table.dataTable tr.selected td.x16{background-color: #A6CEE3 !important;} +.x17{background-color: #1F78B4;} +table.dataTable tr.selected td.x17{background-color: #1F78B4 !important;} +.x18{background-color: #B2DF8A;} +table.dataTable tr.selected td.x18{background-color: #B2DF8A !important;} +.x19{background-color: #33A02C;} +table.dataTable tr.selected td.x19{background-color: #33A02C !important;} +.x20{background-color: #FB9A99;} +table.dataTable tr.selected td.x20{background-color: #FB9A99 !important;} +.x21{background-color: #E31A1C;} +table.dataTable tr.selected td.x21{background-color: #E31A1C !important;} +.x22{background-color: #FDBF6F;} +table.dataTable tr.selected td.x22{background-color: #FDBF6F !important;} +.x23{background-color: #FF7F00;} +table.dataTable tr.selected td.x23{background-color: #FF7F00 !important;} +.x24{background-color: #CAB2D6;} +table.dataTable tr.selected td.x24{background-color: #CAB2D6 !important;} +.x25{background-color: #a17bc9;} +table.dataTable tr.selected td.x25{background-color: #a17bc9 !important;} +.x26{background-color: #B15928;} +table.dataTable tr.selected td.x26{background-color: #B15928 !important;} +.x27{background-color: #FBB4AE;} +table.dataTable tr.selected td.x27{background-color: #FBB4AE !important;} +.x28{background-color: #B3CDE3;} +table.dataTable tr.selected td.x28{background-color: #B3CDE3 !important;} +.x29{background-color: #CCEBC5;} +table.dataTable tr.selected td.x29{background-color: #CCEBC5 !important;} +.x30{background-color: #a9a9a9;} +table.dataTable tr.selected td.x30{background-color: #a9a9a9 !important;} +.x31{background-color: #dcdcdc;} +table.dataTable tr.selected td.x31{background-color: #dcdcdc !important;} +.x32{background-color: #98fb98;} +table.dataTable tr.selected td.x32{background-color: #98fb98 !important;} +.x33{background-color: #556b2f;} +table.dataTable tr.selected td.x33{background-color: #556b2f !important;} +.x34{background-color: #8b4513;} +table.dataTable tr.selected td.x34{background-color: #8b4513 !important;} +.x35{background-color: #6b8e23;} +table.dataTable tr.selected td.x35{background-color: #6b8e23 !important;} +.x36{background-color: #2e8b57;} +table.dataTable tr.selected td.x36{background-color: #2e8b57 !important;} +.x37{background-color: #800000;} +table.dataTable tr.selected td.x37{background-color: #800000 !important;} +.x38{background-color: #FDDAEC;} +table.dataTable tr.selected td.x38{background-color: #FDDAEC !important;} +.x39{background-color: #006400;} +table.dataTable tr.selected td.x39{background-color: #006400 !important;} +.x40{background-color: #808000;} +table.dataTable tr.selected td.x40{background-color: #808000 !important;} +.x41{background-color: #6c3920;} +table.dataTable tr.selected td.x41{background-color: #6c3920 !important;} +.x42{background-color: #778899;} +table.dataTable tr.selected td.x42{background-color: #778899 !important;} +.x43{background-color: #3cb371;} +table.dataTable tr.selected td.x43{background-color: #3cb371 !important;} +.x44{background-color: #bc8f8f;} +table.dataTable tr.selected td.x44{background-color: #bc8f8f !important;} +.x45{background-color: #b0737c;} +table.dataTable tr.selected td.x45{background-color: #b0737c !important;} +.x46{background-color: #008080;} +table.dataTable tr.selected td.x46{background-color: #008080 !important;} +.x47{background-color: #b8860b;} +table.dataTable tr.selected td.x47{background-color: #b8860b !important;} +.x48{background-color: #bdb76b;} +table.dataTable tr.selected td.x48{background-color: #bdb76b !important;} +.x49{background-color: #cd853f;} +table.dataTable tr.selected td.x49{background-color: #cd853f !important;} +.x50{background-color: #4682b4;} +table.dataTable tr.selected td.x50{background-color: #4682b4 !important;} +.x51{background-color: #d2691e;} +table.dataTable tr.selected td.x51{background-color: #d2691e !important;} +.x52{background-color: #9acd32;} +table.dataTable tr.selected td.x52{background-color: #9acd32 !important;} +.x53{background-color: #20b2aa;} +table.dataTable tr.selected td.x53{background-color: #20b2aa !important;} +.x54{background-color: #008b7b;} +table.dataTable tr.selected td.x54{background-color: #008b7b !important;} +.x55{background-color: #82007e;} +table.dataTable tr.selected td.x55{background-color: #82007e !important;} +.x56{background-color: #32cd32;} +table.dataTable tr.selected td.x56{background-color: #32cd32 !important;} +.x57{background-color: #8fbc8f;} +table.dataTable tr.selected td.x57{background-color: #8fbc8f !important;} +.x58{background-color: #0ba47e;} +table.dataTable tr.selected td.x58{background-color: #0ba47e !important;} +.x59{background-color: #b03060;} +table.dataTable tr.selected td.x59{background-color: #b03060 !important;} +.x60{background-color: #d2b48c;} +table.dataTable tr.selected td.x60{background-color: #d2b48c !important;} +.x61{background-color: #66cdaa;} +table.dataTable tr.selected td.x61{background-color: #66cdaa !important;} +.x62{background-color: #9932cc;} +table.dataTable tr.selected td.x62{background-color: #9932cc !important;} +.x63{background-color: #ff0000;} +table.dataTable tr.selected td.x63{background-color: #ff0000 !important;} +.x64{background-color: #ffa500;} +table.dataTable tr.selected td.x64{background-color: #ffa500 !important;} +.x65{background-color: #ffd700;} +table.dataTable tr.selected td.x65{background-color: #ffd700 !important;} +.x66{background-color: #ffff00;} +table.dataTable tr.selected td.x66{background-color: #ffff00 !important;} +.x67{background-color: #c71585;} +table.dataTable tr.selected td.x67{background-color: #c71585 !important;} +.x68{background-color: #0303ff;} +table.dataTable tr.selected td.x68{background-color: #0303ff !important;} +.x69{background-color: #7fff00;} +table.dataTable tr.selected td.x69{background-color: #7fff00 !important;} +.x70{background-color: #00ff00;} +table.dataTable tr.selected td.x70{background-color: #00ff00 !important;} +.x71{background-color: #ba55d3;} +table.dataTable tr.selected td.x71{background-color: #ba55d3 !important;} +.x72{background-color: #00ff7f;} +table.dataTable tr.selected td.x72{background-color: #00ff7f !important;} +.x73{background-color: #4169e1;} +table.dataTable tr.selected td.x73{background-color: #4169e1 !important;} +.x74{background-color: #e9967a;} +table.dataTable tr.selected td.x74{background-color: #e9967a !important;} +.x75{background-color: #dc143c;} +table.dataTable tr.selected td.x75{background-color: #dc143c !important;} +.x76{background-color: #00ffff;} +table.dataTable tr.selected td.x76{background-color: #00ffff !important;} +.x77{background-color: #00bfff;} +table.dataTable tr.selected td.x77{background-color: #00bfff !important;} +.x78{background-color: #9370db;} +table.dataTable tr.selected td.x78{background-color: #9370db !important;} +.x79{background-color: #0000ff;} +table.dataTable tr.selected td.x79{background-color: #0000ff !important;} +.x80{background-color: #a020f0;} +table.dataTable tr.selected td.x80{background-color: #a020f0 !important;} +.x81{background-color: #adff2f;} +table.dataTable tr.selected td.x81{background-color: #adff2f !important;} +.x82{background-color: #d8bfd8;} +table.dataTable tr.selected td.x82{background-color: #d8bfd8 !important;} +.x83{background-color: #ff7f50;} +table.dataTable tr.selected td.x83{background-color: #ff7f50 !important;} +.x84{background-color: #ff00ff;} +table.dataTable tr.selected td.x84{background-color: #ff00ff !important;} +.x85{background-color: #db7093;} +table.dataTable tr.selected td.x85{background-color: #db7093 !important;} +.x86{background-color: #f0e68c;} +table.dataTable tr.selected td.x86{background-color: #f0e68c !important;} +.x87{background-color: #fa8072;} +table.dataTable tr.selected td.x87{background-color: #fa8072 !important;} +.x88{background-color: #ffff54;} +table.dataTable tr.selected td.x88{background-color: #ffff54 !important;} +.x89{background-color: #6495ed;} +table.dataTable tr.selected td.x89{background-color: #6495ed !important;} +.x90{background-color: #dda0dd;} +table.dataTable tr.selected td.x90{background-color: #dda0dd !important;} +.x91{background-color: #87ceeb;} +table.dataTable tr.selected td.x91{background-color: #87ceeb !important;} +.x92{background-color: #ff1493;} +table.dataTable tr.selected td.x92{background-color: #ff1493 !important;} +.x93{background-color: #afeeee;} +table.dataTable tr.selected td.x93{background-color: #afeeee !important;} +.x94{background-color: #ee82ee;} +table.dataTable tr.selected td.x94{background-color: #ee82ee !important;} +.x95{background-color: #2f4f4f;} +table.dataTable tr.selected td.x95{background-color: #2f4f4f !important;} +.x96{background-color: #7fffd4;} +table.dataTable tr.selected td.x96{background-color: #7fffd4 !important;} +.x97{background-color: #ff69b4;} +table.dataTable tr.selected td.x97{background-color: #ff69b4 !important;} +.x98{background-color: #ffe4c4;} +table.dataTable tr.selected td.x98{background-color: #ffe4c4 !important;} +.x99{background-color: #ffb6c1;} +table.dataTable tr.selected td.x99{background-color: #ffb6c1 !important;} +.x100{background-color: #DECBE4;} +table.dataTable tr.selected td.x100{background-color: #DECBE4 !important;} +.x101{background-color: #FED9A6;} +table.dataTable tr.selected td.x101{background-color: #FED9A6 !important;} +.x102{background-color: #FFFFCC;} +table.dataTable tr.selected td.x102{background-color: #FFFFCC !important;} +.x103{background-color: #E5D8BD;} +table.dataTable tr.selected td.x103{background-color: #E5D8BD !important;} +.x104{background-color: #191970;} +table.dataTable tr.selected td.x104{background-color: #191970 !important;} +.x105{background-color: #F2F2F2;} +table.dataTable tr.selected td.x105{background-color: #F2F2F2 !important;} +.x106{background-color: #B3E2CD;} +table.dataTable tr.selected td.x106{background-color: #B3E2CD !important;} +.x107{background-color: #FDCDAC;} +table.dataTable tr.selected td.x107{background-color: #FDCDAC !important;} +.x108{background-color: #CBD5E8;} +table.dataTable tr.selected td.x108{background-color: #CBD5E8 !important;} +.x109{background-color: #F4CAE4;} +table.dataTable tr.selected td.x109{background-color: #F4CAE4 !important;} +.x110{background-color: #E6F5C9;} +table.dataTable tr.selected td.x110{background-color: #E6F5C9 !important;} +.x111{background-color: #FFF2AE;} +table.dataTable tr.selected td.x111{background-color: #FFF2AE !important;} +.x112{background-color: #F1E2CC;} +table.dataTable tr.selected td.x112{background-color: #F1E2CC !important;} +.x113{background-color: #CCCCCC;} +table.dataTable tr.selected td.x113{background-color: #CCCCCC !important;} +.x114{background-color: #E41A1C;} +table.dataTable tr.selected td.x114{background-color: #E41A1C !important;} +.x115{background-color: #377EB8;} +table.dataTable tr.selected td.x115{background-color: #377EB8 !important;} +.x116{background-color: #4DAF4A;} +table.dataTable tr.selected td.x116{background-color: #4DAF4A !important;} +.x117{background-color: #984EA3;} +table.dataTable tr.selected td.x117{background-color: #984EA3 !important;} +.x118{background-color: #FFFF33;} +table.dataTable tr.selected td.x118{background-color: #FFFF33 !important;} +.x119{background-color: #A65628;} +table.dataTable tr.selected td.x119{background-color: #A65628 !important;} +.x120{background-color: #F781BF;} +table.dataTable tr.selected td.x120{background-color: #F781BF !important;} +.x121{background-color: #999999;} +table.dataTable tr.selected td.x121{background-color: #999999 !important;} +.x122{background-color: #66C2A5;} +table.dataTable tr.selected td.x122{background-color: #66C2A5 !important;} +.x123{background-color: #FC8D62;} +table.dataTable tr.selected td.x123{background-color: #FC8D62 !important;} +.x124{background-color: #8DA0CB;} +table.dataTable tr.selected td.x124{background-color: #8DA0CB !important;} +.x125{background-color: #E78AC3;} +table.dataTable tr.selected td.x125{background-color: #E78AC3 !important;} +.x126{background-color: #A6D854;} +table.dataTable tr.selected td.x126{background-color: #A6D854 !important;} +.x127{background-color: #FFD92F;} +table.dataTable tr.selected td.x127{background-color: #FFD92F !important;} +.x128{background-color: #E5C494;} +table.dataTable tr.selected td.x128{background-color: #E5C494 !important;} +.x129{background-color: #B3B3B3;} +table.dataTable tr.selected td.x129{background-color: #B3B3B3 !important;} +.x130{background-color: #8DD3C7;} +table.dataTable tr.selected td.x130{background-color: #8DD3C7 !important;} +.x131{background-color: #FFFFB3;} +table.dataTable tr.selected td.x131{background-color: #FFFFB3 !important;} +.x132{background-color: #BEBADA;} +table.dataTable tr.selected td.x132{background-color: #BEBADA !important;} +.x133{background-color: #FB8072;} +table.dataTable tr.selected td.x133{background-color: #FB8072 !important;} +.x134{background-color: #80B1D3;} +table.dataTable tr.selected td.x134{background-color: #80B1D3 !important;} +.x135{background-color: #FDB462;} +table.dataTable tr.selected td.x135{background-color: #FDB462 !important;} +.x136{background-color: #B3DE69;} +table.dataTable tr.selected td.x136{background-color: #B3DE69 !important;} +.x137{background-color: #FCCDE5;} +table.dataTable tr.selected td.x137{background-color: #FCCDE5 !important;} +.x138{background-color: #D9D9D9;} +table.dataTable tr.selected td.x138{background-color: #D9D9D9 !important;} +.x139{background-color: #BC80BD;} +table.dataTable tr.selected td.x139{background-color: #BC80BD !important;} +.x140{background-color: #095F02;} +table.dataTable tr.selected td.x140{background-color: #095F02 !important;} +.x141{background-color: #E31A97;} +table.dataTable tr.selected td.x141{background-color: #E31A97 !important;} +.x142{background-color: #A81AD2;} +table.dataTable tr.selected td.x142{background-color: #A81AD2 !important;} +.x143{background-color: #74ED33;} +table.dataTable tr.selected td.x143{background-color: #74ED33 !important;} +.x144{background-color: #38F76B;} +table.dataTable tr.selected td.x144{background-color: #38F76B !important;} +.x145{background-color: #E607E1;} +table.dataTable tr.selected td.x145{background-color: #E607E1 !important;} +.x146{background-color: #17D214;} +table.dataTable tr.selected td.x146{background-color: #17D214 !important;} +.x147{background-color: #E2E749;} +table.dataTable tr.selected td.x147{background-color: #E2E749 !important;} +.x148{background-color: #5522DF;} +table.dataTable tr.selected td.x148{background-color: #5522DF !important;} +.x149{background-color: #1FF696;} +table.dataTable tr.selected td.x149{background-color: #1FF696 !important;} +.x150{background-color: #1406E9;} +table.dataTable tr.selected td.x150{background-color: #1406E9 !important;} +.x151{background-color: #6A43DE;} +table.dataTable tr.selected td.x151{background-color: #6A43DE !important;} +.x152{background-color: #C2F309;} +table.dataTable tr.selected td.x152{background-color: #C2F309 !important;} +.x153{background-color: #E92D57;} +table.dataTable tr.selected td.x153{background-color: #E92D57 !important;} +.x154{background-color: #5004DE;} +table.dataTable tr.selected td.x154{background-color: #5004DE !important;} +.x155{background-color: #9C4AE9;} +table.dataTable tr.selected td.x155{background-color: #9C4AE9 !important;} +.x156{background-color: #2EF613;} +table.dataTable tr.selected td.x156{background-color: #2EF613 !important;} +.x157{background-color: #EDAA2D;} +table.dataTable tr.selected td.x157{background-color: #EDAA2D !important;} +.x158{background-color: #2FB5EA;} +table.dataTable tr.selected td.x158{background-color: #2FB5EA !important;} +.x159{background-color: #47FAB2;} +table.dataTable tr.selected td.x159{background-color: #47FAB2 !important;} +.x160{background-color: #CC9221;} +table.dataTable tr.selected td.x160{background-color: #CC9221 !important;} +.x161{background-color: #5C4AFC;} +table.dataTable tr.selected td.x161{background-color: #5C4AFC !important;} +.x162{background-color: #E843D0;} +table.dataTable tr.selected td.x162{background-color: #E843D0 !important;} +.x163{background-color: #FE3F64;} +table.dataTable tr.selected td.x163{background-color: #FE3F64 !important;} +.x164{background-color: #21DD3A;} +table.dataTable tr.selected td.x164{background-color: #21DD3A !important;} +.x165{background-color: #1988D2;} +table.dataTable tr.selected td.x165{background-color: #1988D2 !important;} +.x166{background-color: #BC11EF;} +table.dataTable tr.selected td.x166{background-color: #BC11EF !important;} +.x167{background-color: #EEFB3D;} +table.dataTable tr.selected td.x167{background-color: #EEFB3D !important;} +.x168{background-color: #1B9E77;} +table.dataTable tr.selected td.x168{background-color: #1B9E77 !important;} +.x169{background-color: #D95F02;} +table.dataTable tr.selected td.x169{background-color: #D95F02 !important;} +.x170{background-color: #7570B3;} +table.dataTable tr.selected td.x170{background-color: #7570B3 !important;} +.x171{background-color: #88e99a;} +table.dataTable tr.selected td.x171{background-color: #88e99a !important;} +.x172{background-color: #ca2dc5;} +table.dataTable tr.selected td.x172{background-color: #ca2dc5 !important;} +.x173{background-color: #bce333;} +table.dataTable tr.selected td.x173{background-color: #bce333 !important;} +.x174{background-color: #643176;} +table.dataTable tr.selected td.x174{background-color: #643176 !important;} +.x175{background-color: #34f50e;} +table.dataTable tr.selected td.x175{background-color: #34f50e !important;} +.x176{background-color: #b22839;} +table.dataTable tr.selected td.x176{background-color: #b22839 !important;} +.x177{background-color: #4be8f9;} +table.dataTable tr.selected td.x177{background-color: #4be8f9 !important;} +.x178{background-color: #6c3920;} +table.dataTable tr.selected td.x178{background-color: #6c3920 !important;} +.x179{background-color: #bfcd8e;} +table.dataTable tr.selected td.x179{background-color: #bfcd8e !important;} +.x180{background-color: #1642cd;} +table.dataTable tr.selected td.x180{background-color: #1642cd !important;} +.x181{background-color: #f4d403;} +table.dataTable tr.selected td.x181{background-color: #f4d403 !important;} +.x182{background-color: #5310f0;} +table.dataTable tr.selected td.x182{background-color: #5310f0 !important;} +.x183{background-color: #609111;} +table.dataTable tr.selected td.x183{background-color: #609111 !important;} +.x184{background-color: #c697f4;} +table.dataTable tr.selected td.x184{background-color: #c697f4 !important;} +.x185{background-color: #34466d;} +table.dataTable tr.selected td.x185{background-color: #34466d !important;} +.x186{background-color: #b3d9fa;} +table.dataTable tr.selected td.x186{background-color: #b3d9fa !important;} +.x187{background-color: #155126;} +table.dataTable tr.selected td.x187{background-color: #155126 !important;} +.x188{background-color: #fab5b5;} +table.dataTable tr.selected td.x188{background-color: #fab5b5 !important;} +.x189{background-color: #0ba47e;} +table.dataTable tr.selected td.x189{background-color: #0ba47e !important;} +.x190{background-color: #ff0087;} +table.dataTable tr.selected td.x190{background-color: #ff0087 !important;} +.x191{background-color: #3d99ce;} +table.dataTable tr.selected td.x191{background-color: #3d99ce !important;} +.x192{background-color: #bf711e;} +table.dataTable tr.selected td.x192{background-color: #bf711e !important;} +.x193{background-color: #fa718e;} +table.dataTable tr.selected td.x193{background-color: #fa718e !important;} +.x194{background-color: #798872;} +table.dataTable tr.selected td.x194{background-color: #798872 !important;} +.x195{background-color: #fe5900;} +table.dataTable tr.selected td.x195{background-color: #fe5900 !important;} +.x196{background-color: #b0737c;} +table.dataTable tr.selected td.x196{background-color: #b0737c !important;} +.x197{background-color: #E31A97;} +table.dataTable tr.selected td.x197{background-color: #E31A97 !important;} +.x198{background-color: #A81AD2;} +table.dataTable tr.selected td.x198{background-color: #A81AD2 !important;} +.x199{background-color: #74ED33;} +table.dataTable tr.selected td.x199{background-color: #74ED33 !important;} +.x200{background-color: #38F76B;} +table.dataTable tr.selected td.x200{background-color: #38F76B !important;} +.x201{background-color: #DECBE4;} +table.dataTable tr.selected td.x201{background-color: #DECBE4 !important;} +.x202{background-color: #FED9A6;} +table.dataTable tr.selected td.x202{background-color: #FED9A6 !important;} +.x203{background-color: #FFFFCC;} +table.dataTable tr.selected td.x203{background-color: #FFFFCC !important;} +.x204{background-color: #E5D8BD;} +table.dataTable tr.selected td.x204{background-color: #E5D8BD !important;} +.x205{background-color: #FDDAEC;} +table.dataTable tr.selected td.x205{background-color: #FDDAEC !important;} +.x206{background-color: #F2F2F2;} +table.dataTable tr.selected td.x206{background-color: #F2F2F2 !important;} +.x207{background-color: #B3E2CD;} +table.dataTable tr.selected td.x207{background-color: #B3E2CD !important;} +.x208{background-color: #FDCDAC;} +table.dataTable tr.selected td.x208{background-color: #FDCDAC !important;} +.x209{background-color: #CBD5E8;} +table.dataTable tr.selected td.x209{background-color: #CBD5E8 !important;} +.x210{background-color: #F4CAE4;} +table.dataTable tr.selected td.x210{background-color: #F4CAE4 !important;} +.x211{background-color: #E6F5C9;} +table.dataTable tr.selected td.x211{background-color: #E6F5C9 !important;} +.x212{background-color: #FFF2AE;} +table.dataTable tr.selected td.x212{background-color: #FFF2AE !important;} +.x213{background-color: #F1E2CC;} +table.dataTable tr.selected td.x213{background-color: #F1E2CC !important;} +.x214{background-color: #CCCCCC;} +table.dataTable tr.selected td.x214{background-color: #CCCCCC !important;} +.x215{background-color: #E41A1C;} +table.dataTable tr.selected td.x215{background-color: #E41A1C !important;} +.x216{background-color: #377EB8;} +table.dataTable tr.selected td.x216{background-color: #377EB8 !important;} +.x217{background-color: #4DAF4A;} +table.dataTable tr.selected td.x217{background-color: #4DAF4A !important;} +.x218{background-color: #984EA3;} +table.dataTable tr.selected td.x218{background-color: #984EA3 !important;} +.x219{background-color: #FFFF33;} +table.dataTable tr.selected td.x219{background-color: #FFFF33 !important;} +.x220{background-color: #A65628;} +table.dataTable tr.selected td.x220{background-color: #A65628 !important;} +.x221{background-color: #F781BF;} +table.dataTable tr.selected td.x221{background-color: #F781BF !important;} +.x222{background-color: #999999;} +table.dataTable tr.selected td.x222{background-color: #999999 !important;} +.x223{background-color: #66C2A5;} +table.dataTable tr.selected td.x223{background-color: #66C2A5 !important;} +.x224{background-color: #FC8D62;} +table.dataTable tr.selected td.x224{background-color: #FC8D62 !important;} +.x225{background-color: #8DA0CB;} +table.dataTable tr.selected td.x225{background-color: #8DA0CB !important;} +.x226{background-color: #E78AC3;} +table.dataTable tr.selected td.x226{background-color: #E78AC3 !important;} +.x227{background-color: #A6D854;} +table.dataTable tr.selected td.x227{background-color: #A6D854 !important;} +.x228{background-color: #FFD92F;} +table.dataTable tr.selected td.x228{background-color: #FFD92F !important;} +.x229{background-color: #E5C494;} +table.dataTable tr.selected td.x229{background-color: #E5C494 !important;} +.x230{background-color: #B3B3B3;} +table.dataTable tr.selected td.x230{background-color: #B3B3B3 !important;} +.x231{background-color: #8DD3C7;} +table.dataTable tr.selected td.x231{background-color: #8DD3C7 !important;} +.x232{background-color: #FFFFB3;} +table.dataTable tr.selected td.x232{background-color: #FFFFB3 !important;} +.x233{background-color: #BEBADA;} +table.dataTable tr.selected td.x233{background-color: #BEBADA !important;} +.x234{background-color: #FB8072;} +table.dataTable tr.selected td.x234{background-color: #FB8072 !important;} +.x235{background-color: #80B1D3;} +table.dataTable tr.selected td.x235{background-color: #80B1D3 !important;} +.x236{background-color: #FDB462;} +table.dataTable tr.selected td.x236{background-color: #FDB462 !important;} +.x237{background-color: #B3DE69;} +table.dataTable tr.selected td.x237{background-color: #B3DE69 !important;} +.x238{background-color: #FCCDE5;} +table.dataTable tr.selected td.x238{background-color: #FCCDE5 !important;} +.x239{background-color: #D9D9D9;} +table.dataTable tr.selected td.x239{background-color: #D9D9D9 !important;} +.x240{background-color: #BC80BD;} +table.dataTable tr.selected td.x240{background-color: #BC80BD !important;} +.x241{background-color: #095F02;} +table.dataTable tr.selected td.x241{background-color: #095F02 !important;} +.x242{background-color: #E31A97;} +table.dataTable tr.selected td.x242{background-color: #E31A97 !important;} +.x243{background-color: #A81AD2;} +table.dataTable tr.selected td.x243{background-color: #A81AD2 !important;} +.x244{background-color: #74ED33;} +table.dataTable tr.selected td.x244{background-color: #74ED33 !important;} +.x245{background-color: #38F76B;} +table.dataTable tr.selected td.x245{background-color: #38F76B !important;} +.x246{background-color: #E607E1;} +table.dataTable tr.selected td.x246{background-color: #E607E1 !important;} +.x247{background-color: #17D214;} +table.dataTable tr.selected td.x247{background-color: #17D214 !important;} +.x248{background-color: #E2E749;} +table.dataTable tr.selected td.x248{background-color: #E2E749 !important;} +.x249{background-color: #5522DF;} +table.dataTable tr.selected td.x249{background-color: #5522DF !important;} +.x250{background-color: #1FF696;} +table.dataTable tr.selected td.x250{background-color: #1FF696 !important;} +.x251{background-color: #1406E9;} +table.dataTable tr.selected td.x251{background-color: #1406E9 !important;} +.x252{background-color: #6A43DE;} +table.dataTable tr.selected td.x252{background-color: #6A43DE !important;} +.x253{background-color: #C2F309;} +table.dataTable tr.selected td.x253{background-color: #C2F309 !important;} +.x254{background-color: #E92D57;} +table.dataTable tr.selected td.x254{background-color: #E92D57 !important;} +.x255{background-color: #5004DE;} +table.dataTable tr.selected td.x255{background-color: #5004DE !important;} +.x256{background-color: #9C4AE9;} +table.dataTable tr.selected td.x256{background-color: #9C4AE9 !important;} +.x257{background-color: #2EF613;} +table.dataTable tr.selected td.x257{background-color: #2EF613 !important;} +.x258{background-color: #EDAA2D;} +table.dataTable tr.selected td.x258{background-color: #EDAA2D !important;} +.x259{background-color: #2FB5EA;} +table.dataTable tr.selected td.x259{background-color: #2FB5EA !important;} +.x260{background-color: #47FAB2;} +table.dataTable tr.selected td.x260{background-color: #47FAB2 !important;} +.x261{background-color: #CC9221;} +table.dataTable tr.selected td.x261{background-color: #CC9221 !important;} +.x262{background-color: #5C4AFC;} +table.dataTable tr.selected td.x262{background-color: #5C4AFC !important;} +.x263{background-color: #E843D0;} +table.dataTable tr.selected td.x263{background-color: #E843D0 !important;} +.x264{background-color: #FE3F64;} +table.dataTable tr.selected td.x264{background-color: #FE3F64 !important;} +.x265{background-color: #21DD3A;} +table.dataTable tr.selected td.x265{background-color: #21DD3A !important;} +.x266{background-color: #1988D2;} +table.dataTable tr.selected td.x266{background-color: #1988D2 !important;} +.x267{background-color: #BC11EF;} +table.dataTable tr.selected td.x267{background-color: #BC11EF !important;} +.x268{background-color: #EEFB3D;} +table.dataTable tr.selected td.x268{background-color: #EEFB3D !important;} +.x269{background-color: #1B9E77;} +table.dataTable tr.selected td.x269{background-color: #1B9E77 !important;} +.x270{background-color: #D95F02;} +table.dataTable tr.selected td.x270{background-color: #D95F02 !important;} +.x271{background-color: #7570B3;} +table.dataTable tr.selected td.x271{background-color: #7570B3 !important;} +.x272{background-color: #88e99a;} +table.dataTable tr.selected td.x272{background-color: #88e99a !important;} +.x273{background-color: #ca2dc5;} +table.dataTable tr.selected td.x273{background-color: #ca2dc5 !important;} +.x274{background-color: #bce333;} +table.dataTable tr.selected td.x274{background-color: #bce333 !important;} +.x275{background-color: #643176;} +table.dataTable tr.selected td.x275{background-color: #643176 !important;} +.x276{background-color: #34f50e;} +table.dataTable tr.selected td.x276{background-color: #34f50e !important;} +.x277{background-color: #b22839;} +table.dataTable tr.selected td.x277{background-color: #b22839 !important;} +.x278{background-color: #4be8f9;} +table.dataTable tr.selected td.x278{background-color: #4be8f9 !important;} +.x279{background-color: #483d8b;} +table.dataTable tr.selected td.x279{background-color: #483d8b !important;} +.x280{background-color: #bfcd8e;} +table.dataTable tr.selected td.x280{background-color: #bfcd8e !important;} +.x281{background-color: #1642cd;} +table.dataTable tr.selected td.x281{background-color: #1642cd !important;} +.x282{background-color: #f4d403;} +table.dataTable tr.selected td.x282{background-color: #f4d403 !important;} +.x283{background-color: #5310f0;} +table.dataTable tr.selected td.x283{background-color: #5310f0 !important;} +.x284{background-color: #609111;} +table.dataTable tr.selected td.x284{background-color: #609111 !important;} +.x285{background-color: #c697f4;} +table.dataTable tr.selected td.x285{background-color: #c697f4 !important;} +.x286{background-color: #34466d;} +table.dataTable tr.selected td.x286{background-color: #34466d !important;} +.x287{background-color: #b3d9fa;} +table.dataTable tr.selected td.x287{background-color: #b3d9fa !important;} +.x288{background-color: #155126;} +table.dataTable tr.selected td.x288{background-color: #155126 !important;} +.x289{background-color: #fab5b5;} +table.dataTable tr.selected td.x289{background-color: #fab5b5 !important;} +.x290{background-color: #800080;} +table.dataTable tr.selected td.x290{background-color: #800080 !important;} +.x291{background-color: #ff0087;} +table.dataTable tr.selected td.x291{background-color: #ff0087 !important;} +.x292{background-color: #3d99ce;} +table.dataTable tr.selected td.x292{background-color: #3d99ce !important;} +.x293{background-color: #bf711e;} +table.dataTable tr.selected td.x293{background-color: #bf711e !important;} +.x294{background-color: #fa718e;} +table.dataTable tr.selected td.x294{background-color: #fa718e !important;} +.x295{background-color: #798872;} +table.dataTable tr.selected td.x295{background-color: #798872 !important;} +.x296{background-color: #fe5900;} +table.dataTable tr.selected td.x296{background-color: #fe5900 !important;} +.x297{background-color: #663399;} +table.dataTable tr.selected td.x297{background-color: #663399 !important;} +.x298{background-color: #E31A97;} +table.dataTable tr.selected td.x298{background-color: #E31A97 !important;} +.x299{background-color: #A81AD2;} +table.dataTable tr.selected td.x299{background-color: #A81AD2 !important;} +.x300{background-color: #74ED33;} +table.dataTable tr.selected td.x300{background-color: #74ED33 !important;} diff --git a/www/annotation_cleaner.R b/www/annotation_cleaner.R new file mode 100644 index 0000000..c87cb52 --- /dev/null +++ b/www/annotation_cleaner.R @@ -0,0 +1,68 @@ +if (!require(tidyverse)) + install.packages('tidyverse') +if (!require(plyr)) + install.packages('plyr') +if (!require(dplyr)) + install.packages('dplyr') +if (!require(purrr)) + install.packages('purrr') +if (!require(stringi)) + install.packages('stringi') +if (!require(stringr)) + install.packages('stringr') + +library(stringi) +library(stringr) +library(plyr) +library(tidyr) +library(dplyr) +library(purrr) + +network<- read.delim(choose.files()) +annotations <- read.delim(choose.files(),header = F) + +column1<- unique(network$Source) +column2<- unique(network$Target) + +df1<- data.frame(V1= column1) +df2<- data.frame(V1= column2) + +unique_nodes_network <- suppressMessages(full_join(df1, df2)) + +annotations2<- as.character(annotations[,2]) +genes <- strsplit(annotations2, ',') + +unique_nodes_annotations<- as.data.frame(unique(unlist(genes))) +colnames(unique_nodes_annotations)<- 'V1' + +merged <- suppressMessages(full_join(unique_nodes_network, unique_nodes_annotations)) + + +words_to_be_removed <- suppressMessages(anti_join(merged,unique_nodes_network)) + +annotations_new<- data.frame() +if(!is.null(words_to_be_removed)){ + for(i in 1:length(annotations[,2])){ + x <- annotations[i,2] + candidates <- strsplit(as.character(x), ',') + line<-'' + for(j in 1:length(candidates[[1]])){ + word<-as.character(c(candidates[[1]])[j]) + if(!(word %in% words_to_be_removed$V1)) + line<-paste(line,word, sep=',') + } + line <- gsub('^,', '', line) + if(nchar(line)>0) + { + annotations_new[i,1] <- annotations[i,1] + annotations_new[i,2] <- line + } + } + annotations_new<-na.omit(annotations_new) +} else { + annotations_new<-annotations +} +colnames(annotations_new)<- c('Annotations','Nodes') + +write.table(annotations_new, file = 'annotations_cleaned.txt', row.names = F, col.names = F, sep = "\t") + diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100644 index 0000000..c7616f2 Binary files /dev/null and b/www/favicon.ico differ diff --git a/www/intro.css b/www/intro.css new file mode 100644 index 0000000..6558874 --- /dev/null +++ b/www/intro.css @@ -0,0 +1,506 @@ +.introjs-overlay { + position: absolute; + box-sizing: content-box; + z-index: 999999; + background-color: #000; + opacity: 0; + background: -moz-radial-gradient(center,ellipse farthest-corner,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%); + background: -webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9))); + background: -webkit-radial-gradient(center,ellipse farthest-corner,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%); + background: -o-radial-gradient(center,ellipse farthest-corner,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%); + background: -ms-radial-gradient(center,ellipse farthest-corner,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%); + background: radial-gradient(center,ellipse farthest-corner,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%); + filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1)"; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +.introjs-fixParent { + z-index: auto !important; + opacity: 1.0 !important; + -webkit-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -o-transform: none !important; + transform: none !important; +} + +.introjs-showElement, +tr.introjs-showElement > td, +tr.introjs-showElement > th { + z-index: 9999999 !important; +} + +.introjs-disableInteraction { + z-index: 99999999 !important; + position: absolute; + background-color: white; + opacity: 0; + filter: alpha(opacity=0); +} + +.introjs-relativePosition, +tr.introjs-showElement > td, +tr.introjs-showElement > th { + position: relative; +} + +.introjs-helperLayer { + box-sizing: content-box; + position: absolute; + z-index: 9999998; + background-color: #FFF; + background-color: rgba(255,255,255,.9); + border: 1px solid #777; + border: 1px solid rgba(0,0,0,.5); + border-radius: 4px; + box-shadow: 0 2px 15px rgba(0,0,0,.4); + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +.introjs-tooltipReferenceLayer { + box-sizing: content-box; + position: absolute; + visibility: hidden; + z-index: 100000000; + background-color: transparent; + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +.introjs-helperLayer *, +.introjs-helperLayer *:before, +.introjs-helperLayer *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + -ms-box-sizing: content-box; + -o-box-sizing: content-box; + box-sizing: content-box; +} + +.introjs-helperNumberLayer { + box-sizing: content-box; + position: absolute; + visibility: visible; + top: -16px; + left: -16px; + z-index: 9999999999 !important; + padding: 2px; + font-family: Arial, verdana, tahoma; + font-size: 13px; + font-weight: bold; + color: white; + text-align: center; + text-shadow: 1px 1px 1px rgba(0,0,0,.3); + background: #ff3019; /* Old browsers */ + background: -webkit-linear-gradient(top, #ff3019 0%, #cf0404 100%); /* Chrome10+,Safari5.1+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ff3019), color-stop(100%, #cf0404)); /* Chrome,Safari4+ */ + background: -moz-linear-gradient(top, #ff3019 0%, #cf0404 100%); /* FF3.6+ */ + background: -ms-linear-gradient(top, #ff3019 0%, #cf0404 100%); /* IE10+ */ + background: -o-linear-gradient(top, #ff3019 0%, #cf0404 100%); /* Opera 11.10+ */ + background: linear-gradient(to bottom, #ff3019 0%, #cf0404 100%); /* W3C */ + width: 20px; + height:20px; + line-height: 20px; + border: 3px solid white; + border-radius: 50%; + filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019', endColorstr='#cf0404', GradientType=0)"; /* IE6-9 */ + filter: "progid:DXImageTransform.Microsoft.Shadow(direction=135, strength=2, color=ff0000)"; /* IE10 text shadows */ + box-shadow: 0 2px 5px rgba(0,0,0,.4); +} + +.introjs-arrow { + border: 5px solid transparent; + content:''; + position: absolute; +} +.introjs-arrow.top { + top: -10px; + border-bottom-color:white; +} +.introjs-arrow.top-right { + top: -10px; + right: 10px; + border-bottom-color:white; +} +.introjs-arrow.top-middle { + top: -10px; + left: 50%; + margin-left: -5px; + border-bottom-color:white; +} +.introjs-arrow.right { + right: -10px; + top: 10px; + border-left-color:white; +} +.introjs-arrow.right-bottom { + bottom:10px; + right: -10px; + border-left-color:white; +} +.introjs-arrow.bottom { + bottom: -10px; + border-top-color:white; +} +.introjs-arrow.bottom-right { + bottom: -10px; + right: 10px; + border-top-color:white; +} +.introjs-arrow.bottom-middle { + bottom: -10px; + left: 50%; + margin-left: -5px; + border-top-color:white; +} +.introjs-arrow.left { + left: -10px; + top: 10px; + border-right-color:white; +} +.introjs-arrow.left-bottom { + left: -10px; + bottom:10px; + border-right-color:white; +} + +.introjs-tooltip { + box-sizing: content-box; + position: absolute; + visibility: visible; + padding: 10px; + background-color: white; + min-width: 200px; + max-width: 300px; + border-radius: 3px; + box-shadow: 0 1px 10px rgba(0,0,0,.4); + -webkit-transition: opacity 0.1s ease-out; + -moz-transition: opacity 0.1s ease-out; + -ms-transition: opacity 0.1s ease-out; + -o-transition: opacity 0.1s ease-out; + transition: opacity 0.1s ease-out; +} + +.introjs-tooltipbuttons { + text-align: right; + white-space: nowrap; +} + +/* + Buttons style by http://nicolasgallagher.com/lab/css3-github-buttons/ + Changed by Afshin Mehrabani +*/ +.introjs-button { + box-sizing: content-box; + position: relative; + overflow: visible; + display: inline-block; + padding: 0.3em 0.8em; + border: 1px solid #d4d4d4; + margin: 0; + text-decoration: none; + text-shadow: 1px 1px 0 #fff; + font: 11px/normal sans-serif; + color: #333; + white-space: nowrap; + cursor: pointer; + outline: none; + background-color: #ececec; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f4f4f4), to(#ececec)); + background-image: -moz-linear-gradient(#f4f4f4, #ececec); + background-image: -o-linear-gradient(#f4f4f4, #ececec); + background-image: linear-gradient(#f4f4f4, #ececec); + -webkit-background-clip: padding; + -moz-background-clip: padding; + -o-background-clip: padding-box; + /*background-clip: padding-box;*/ /* commented out due to Opera 11.10 bug */ + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + /* IE hacks */ + zoom: 1; + *display: inline; + margin-top: 10px; +} + +.introjs-button:hover { + border-color: #bcbcbc; + text-decoration: none; + box-shadow: 0px 1px 1px #e3e3e3; +} + +.introjs-button:focus, +.introjs-button:active { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ececec), to(#f4f4f4)); + background-image: -moz-linear-gradient(#ececec, #f4f4f4); + background-image: -o-linear-gradient(#ececec, #f4f4f4); + background-image: linear-gradient(#ececec, #f4f4f4); +} + +/* overrides extra padding on button elements in Firefox */ +.introjs-button::-moz-focus-inner { + padding: 0; + border: 0; +} + +.introjs-skipbutton { + box-sizing: content-box; + margin-right: 5px; + color: #7a7a7a; +} + +.introjs-prevbutton { + -webkit-border-radius: 0.2em 0 0 0.2em; + -moz-border-radius: 0.2em 0 0 0.2em; + border-radius: 0.2em 0 0 0.2em; + border-right: none; +} + +.introjs-prevbutton.introjs-fullbutton { + border: 1px solid #d4d4d4; + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; +} + +.introjs-nextbutton { + -webkit-border-radius: 0 0.2em 0.2em 0; + -moz-border-radius: 0 0.2em 0.2em 0; + border-radius: 0 0.2em 0.2em 0; +} + +.introjs-nextbutton.introjs-fullbutton { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; +} + +.introjs-disabled, .introjs-disabled:hover, .introjs-disabled:focus { + color: #9a9a9a; + border-color: #d4d4d4; + box-shadow: none; + cursor: default; + background-color: #f4f4f4; + background-image: none; + text-decoration: none; +} + +.introjs-hidden { + display: none; +} + +.introjs-bullets { + text-align: center; +} +.introjs-bullets ul { + box-sizing: content-box; + clear: both; + margin: 15px auto 0; + padding: 0; + display: inline-block; +} +.introjs-bullets ul li { + box-sizing: content-box; + list-style: none; + float: left; + margin: 0 2px; +} +.introjs-bullets ul li a { + box-sizing: content-box; + display: block; + width: 6px; + height: 6px; + background: #ccc; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + text-decoration: none; + cursor: pointer; +} +.introjs-bullets ul li a:hover { + background: #999; +} +.introjs-bullets ul li a.active { + background: #999; +} + +.introjs-progress { + box-sizing: content-box; + overflow: hidden; + height: 10px; + margin: 10px 0 5px 0; + border-radius: 4px; + background-color: #ecf0f1 +} +.introjs-progressbar { + box-sizing: content-box; + float: left; + width: 0%; + height: 100%; + font-size: 10px; + line-height: 10px; + text-align: center; + background-color: #08c; +} + +.introjsFloatingElement { + position: absolute; + height: 0; + width: 0; + left: 50%; + top: 50%; +} + +.introjs-fixedTooltip { + position: fixed; +} + +.introjs-hint { + box-sizing: content-box; + position: absolute; + background: transparent; + width: 20px; + height: 15px; + cursor: pointer; +} +.introjs-hint:focus { + border: 0; + outline: 0; +} +.introjs-hidehint { + display: none; +} + +.introjs-fixedhint { + position: fixed; +} + +.introjs-hint:hover > .introjs-hint-pulse { + border: 5px solid rgba(60, 60, 60, 0.57); +} + +.introjs-hint-pulse { + box-sizing: content-box; + width: 10px; + height: 10px; + border: 5px solid rgba(60, 60, 60, 0.27); + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; + background-color: rgba(136, 136, 136, 0.24); + z-index: 10; + position: absolute; + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} +.introjs-hint-no-anim .introjs-hint-dot { + -webkit-animation: none; + -moz-animation: none; + animation: none; +} +.introjs-hint-dot { + box-sizing: content-box; + border: 10px solid rgba(146, 146, 146, 0.36); + background: transparent; + -webkit-border-radius: 60px; + -moz-border-radius: 60px; + border-radius: 60px; + height: 50px; + width: 50px; + -webkit-animation: introjspulse 3s ease-out; + -moz-animation: introjspulse 3s ease-out; + animation: introjspulse 3s ease-out; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + animation-iteration-count: infinite; + position: absolute; + top: -25px; + left: -25px; + z-index: 1; + opacity: 0; +} + +@-webkit-keyframes introjspulse { + 0% { + -webkit-transform: scale(0); + opacity: 0.0; + } + 25% { + -webkit-transform: scale(0); + opacity: 0.1; + } + 50% { + -webkit-transform: scale(0.1); + opacity: 0.3; + } + 75% { + -webkit-transform: scale(0.5); + opacity: 0.5; + } + 100% { + -webkit-transform: scale(1); + opacity: 0.0; + } +} + +@-moz-keyframes introjspulse { + 0% { + -moz-transform: scale(0); + opacity: 0.0; + } + 25% { + -moz-transform: scale(0); + opacity: 0.1; + } + 50% { + -moz-transform: scale(0.1); + opacity: 0.3; + } + 75% { + -moz-transform: scale(0.5); + opacity: 0.5; + } + 100% { + -moz-transform: scale(1); + opacity: 0.0; + } +} + +@keyframes introjspulse { + 0% { + transform: scale(0); + opacity: 0.0; + } + 25% { + transform: scale(0); + opacity: 0.1; + } + 50% { + transform: scale(0.1); + opacity: 0.3; + } + 75% { + transform: scale(0.5); + opacity: 0.5; + } + 100% { + transform: scale(1); + opacity: 0.0; + } +} \ No newline at end of file diff --git a/www/intro.js b/www/intro.js new file mode 100644 index 0000000..53d4cfe --- /dev/null +++ b/www/intro.js @@ -0,0 +1,2531 @@ +/** + * Intro.js v2.9.3 + * https://github.com/usablica/intro.js + * + * Copyright (C) 2017 Afshin Mehrabani (@afshinmeh) + */ + +(function(f) { + if (typeof exports === "object" && typeof module !== "undefined") { + module.exports = f(); + // deprecated function + // @since 2.8.0 + module.exports.introJs = function () { + console.warn('Deprecated: please use require("intro.js") directly, instead of the introJs method of the function'); + // introJs() + return f().apply(this, arguments); + }; + } else if (typeof define === "function" && define.amd) { + define([], f); + } else { + var g; + if (typeof window !== "undefined") { + g = window; + } else if (typeof global !== "undefined") { + g = global; + } else if (typeof self !== "undefined") { + g = self; + } else { + g = this; + } + g.introJs = f(); + } +})(function () { + //Default config/variables + var VERSION = '2.9.3'; + + /** + * IntroJs main class + * + * @class IntroJs + */ + function IntroJs(obj) { + this._targetElement = obj; + this._introItems = []; + + this._options = { + /* Next button label in tooltip box */ + nextLabel: 'Next →', + /* Previous button label in tooltip box */ + prevLabel: '← Back', + /* Skip button label in tooltip box */ + skipLabel: 'Skip', + /* Done button label in tooltip box */ + doneLabel: 'Done', + /* Hide previous button in the first step? Otherwise, it will be disabled button. */ + hidePrev: false, + /* Hide next button in the last step? Otherwise, it will be disabled button. */ + hideNext: false, + /* Default tooltip box position */ + tooltipPosition: 'bottom', + /* Next CSS class for tooltip boxes */ + tooltipClass: '', + /* CSS class that is added to the helperLayer */ + highlightClass: '', + /* Close introduction when pressing Escape button? */ + exitOnEsc: true, + /* Close introduction when clicking on overlay layer? */ + exitOnOverlayClick: true, + /* Show step numbers in introduction? */ + showStepNumbers: true, + /* Let user use keyboard to navigate the tour? */ + keyboardNavigation: true, + /* Show tour control buttons? */ + showButtons: true, + /* Show tour bullets? */ + showBullets: true, + /* Show tour progress? */ + showProgress: false, + /* Scroll to highlighted element? */ + scrollToElement: true, + /* + * Should we scroll the tooltip or target element? + * + * Options are: 'element' or 'tooltip' + */ + scrollTo: 'element', + /* Padding to add after scrolling when element is not in the viewport (in pixels) */ + scrollPadding: 30, + /* Set the overlay opacity */ + overlayOpacity: 0.8, + /* Precedence of positions, when auto is enabled */ + positionPrecedence: ["bottom", "top", "right", "left"], + /* Disable an interaction with element? */ + disableInteraction: false, + /* Set how much padding to be used around helper element */ + helperElementPadding: 10, + /* Default hint position */ + hintPosition: 'top-middle', + /* Hint button label */ + hintButtonLabel: 'Got it', + /* Adding animation to hints? */ + hintAnimation: true, + /* additional classes to put on the buttons */ + buttonClass: "introjs-button" + }; + } + + /** + * Initiate a new introduction/guide from an element in the page + * + * @api private + * @method _introForElement + * @param {Object} targetElm + * @param {String} group + * @returns {Boolean} Success or not? + */ + function _introForElement(targetElm, group) { + var allIntroSteps = targetElm.querySelectorAll("*[data-intro]"), + introItems = []; + + if (this._options.steps) { + //use steps passed programmatically + _forEach(this._options.steps, function (step) { + var currentItem = _cloneObject(step); + + //set the step + currentItem.step = introItems.length + 1; + + //use querySelector function only when developer used CSS selector + if (typeof (currentItem.element) === 'string') { + //grab the element with given selector from the page + currentItem.element = document.querySelector(currentItem.element); + } + + //intro without element + if (typeof (currentItem.element) === 'undefined' || currentItem.element === null) { + var floatingElementQuery = document.querySelector(".introjsFloatingElement"); + + if (floatingElementQuery === null) { + floatingElementQuery = document.createElement('div'); + floatingElementQuery.className = 'introjsFloatingElement'; + + document.body.appendChild(floatingElementQuery); + } + + currentItem.element = floatingElementQuery; + currentItem.position = 'floating'; + } + + currentItem.scrollTo = currentItem.scrollTo || this._options.scrollTo; + + if (typeof (currentItem.disableInteraction) === 'undefined') { + currentItem.disableInteraction = this._options.disableInteraction; + } + + if (currentItem.element !== null) { + introItems.push(currentItem); + } + }.bind(this)); + + } else { + //use steps from data-* annotations + var elmsLength = allIntroSteps.length; + var disableInteraction; + + //if there's no element to intro + if (elmsLength < 1) { + return false; + } + + _forEach(allIntroSteps, function (currentElement) { + + // PR #80 + // start intro for groups of elements + if (group && (currentElement.getAttribute("data-intro-group") !== group)) { + return; + } + + // skip hidden elements + if (currentElement.style.display === 'none') { + return; + } + + var step = parseInt(currentElement.getAttribute('data-step'), 10); + + if (typeof (currentElement.getAttribute('data-disable-interaction')) !== 'undefined') { + disableInteraction = !!currentElement.getAttribute('data-disable-interaction'); + } else { + disableInteraction = this._options.disableInteraction; + } + + if (step > 0) { + introItems[step - 1] = { + element: currentElement, + intro: currentElement.getAttribute('data-intro'), + step: parseInt(currentElement.getAttribute('data-step'), 10), + tooltipClass: currentElement.getAttribute('data-tooltipclass'), + highlightClass: currentElement.getAttribute('data-highlightclass'), + position: currentElement.getAttribute('data-position') || this._options.tooltipPosition, + scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo, + disableInteraction: disableInteraction + }; + } + }.bind(this)); + + //next add intro items without data-step + //todo: we need a cleanup here, two loops are redundant + var nextStep = 0; + + _forEach(allIntroSteps, function (currentElement) { + + // PR #80 + // start intro for groups of elements + if (group && (currentElement.getAttribute("data-intro-group") !== group)) { + return; + } + + if (currentElement.getAttribute('data-step') === null) { + + while (true) { + if (typeof introItems[nextStep] === 'undefined') { + break; + } else { + nextStep++; + } + } + + if (typeof (currentElement.getAttribute('data-disable-interaction')) !== 'undefined') { + disableInteraction = !!currentElement.getAttribute('data-disable-interaction'); + } else { + disableInteraction = this._options.disableInteraction; + } + + introItems[nextStep] = { + element: currentElement, + intro: currentElement.getAttribute('data-intro'), + step: nextStep + 1, + tooltipClass: currentElement.getAttribute('data-tooltipclass'), + highlightClass: currentElement.getAttribute('data-highlightclass'), + position: currentElement.getAttribute('data-position') || this._options.tooltipPosition, + scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo, + disableInteraction: disableInteraction + }; + } + }.bind(this)); + } + + //removing undefined/null elements + var tempIntroItems = []; + for (var z = 0; z < introItems.length; z++) { + if (introItems[z]) { + // copy non-falsy values to the end of the array + tempIntroItems.push(introItems[z]); + } + } + + introItems = tempIntroItems; + + //Ok, sort all items with given steps + introItems.sort(function (a, b) { + return a.step - b.step; + }); + + //set it to the introJs object + this._introItems = introItems; + + //add overlay layer to the page + if(_addOverlayLayer.call(this, targetElm)) { + //then, start the show + _nextStep.call(this); + + if (this._options.keyboardNavigation) { + DOMEvent.on(window, 'keydown', _onKeyDown, this, true); + } + //for window resize + DOMEvent.on(window, 'resize', _onResize, this, true); + } + return false; + } + + function _onResize () { + this.refresh.call(this); + } + + /** + * on keyCode: + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + * This feature has been removed from the Web standards. + * Though some browsers may still support it, it is in + * the process of being dropped. + * Instead, you should use KeyboardEvent.code, + * if it's implemented. + * + * jQuery's approach is to test for + * (1) e.which, then + * (2) e.charCode, then + * (3) e.keyCode + * https://github.com/jquery/jquery/blob/a6b0705294d336ae2f63f7276de0da1195495363/src/event.js#L638 + * + * @param type var + * @return type + */ + function _onKeyDown (e) { + var code = (e.code === null) ? e.which : e.code; + + // if code/e.which is null + if (code === null) { + code = (e.charCode === null) ? e.keyCode : e.charCode; + } + + if ((code === 'Escape' || code === 27) && this._options.exitOnEsc === true) { + //escape key pressed, exit the intro + //check if exit callback is defined + _exitIntro.call(this, this._targetElement); + } else if (code === 'ArrowLeft' || code === 37) { + //left arrow + _previousStep.call(this); + } else if (code === 'ArrowRight' || code === 39) { + //right arrow + _nextStep.call(this); + } else if (code === 'Enter' || code === 13) { + //srcElement === ie + var target = e.target || e.srcElement; + if (target && target.className.match('introjs-prevbutton')) { + //user hit enter while focusing on previous button + _previousStep.call(this); + } else if (target && target.className.match('introjs-skipbutton')) { + //user hit enter while focusing on skip button + if (this._introItems.length - 1 === this._currentStep && typeof (this._introCompleteCallback) === 'function') { + this._introCompleteCallback.call(this); + } + + _exitIntro.call(this, this._targetElement); + } else if (target && target.getAttribute('data-stepnumber')) { + // user hit enter while focusing on step bullet + target.click(); + } else { + //default behavior for responding to enter + _nextStep.call(this); + } + + //prevent default behaviour on hitting Enter, to prevent steps being skipped in some browsers + if(e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + } + } + + /* + * makes a copy of the object + * @api private + * @method _cloneObject + */ + function _cloneObject(object) { + if (object === null || typeof (object) !== 'object' || typeof (object.nodeType) !== 'undefined') { + return object; + } + var temp = {}; + for (var key in object) { + if (typeof(window.jQuery) !== 'undefined' && object[key] instanceof window.jQuery) { + temp[key] = object[key]; + } else { + temp[key] = _cloneObject(object[key]); + } + } + return temp; + } + /** + * Go to specific step of introduction + * + * @api private + * @method _goToStep + */ + function _goToStep(step) { + //because steps starts with zero + this._currentStep = step - 2; + if (typeof (this._introItems) !== 'undefined') { + _nextStep.call(this); + } + } + + /** + * Go to the specific step of introduction with the explicit [data-step] number + * + * @api private + * @method _goToStepNumber + */ + function _goToStepNumber(step) { + this._currentStepNumber = step; + if (typeof (this._introItems) !== 'undefined') { + _nextStep.call(this); + } + } + + /** + * Go to next step on intro + * + * @api private + * @method _nextStep + */ + function _nextStep() { + this._direction = 'forward'; + + if (typeof (this._currentStepNumber) !== 'undefined') { + _forEach(this._introItems, function (item, i) { + if( item.step === this._currentStepNumber ) { + this._currentStep = i - 1; + this._currentStepNumber = undefined; + } + }.bind(this)); + } + + if (typeof (this._currentStep) === 'undefined') { + this._currentStep = 0; + } else { + ++this._currentStep; + } + + var nextStep = this._introItems[this._currentStep]; + var continueStep = true; + + if (typeof (this._introBeforeChangeCallback) !== 'undefined') { + continueStep = this._introBeforeChangeCallback.call(this, nextStep.element); + } + + // if `onbeforechange` returned `false`, stop displaying the element + if (continueStep === false) { + --this._currentStep; + return false; + } + + if ((this._introItems.length) <= this._currentStep) { + //end of the intro + //check if any callback is defined + if (typeof (this._introCompleteCallback) === 'function') { + this._introCompleteCallback.call(this); + } + _exitIntro.call(this, this._targetElement); + return; + } + + _showElement.call(this, nextStep); + } + + /** + * Go to previous step on intro + * + * @api private + * @method _previousStep + */ + function _previousStep() { + this._direction = 'backward'; + + if (this._currentStep === 0) { + return false; + } + + --this._currentStep; + + var nextStep = this._introItems[this._currentStep]; + var continueStep = true; + + if (typeof (this._introBeforeChangeCallback) !== 'undefined') { + continueStep = this._introBeforeChangeCallback.call(this, nextStep.element); + } + + // if `onbeforechange` returned `false`, stop displaying the element + if (continueStep === false) { + ++this._currentStep; + return false; + } + + _showElement.call(this, nextStep); + } + + /** + * Update placement of the intro objects on the screen + * @api private + */ + function _refresh() { + // re-align intros + _setHelperLayerPosition.call(this, document.querySelector('.introjs-helperLayer')); + _setHelperLayerPosition.call(this, document.querySelector('.introjs-tooltipReferenceLayer')); + _setHelperLayerPosition.call(this, document.querySelector('.introjs-disableInteraction')); + + // re-align tooltip + if(this._currentStep !== undefined && this._currentStep !== null) { + var oldHelperNumberLayer = document.querySelector('.introjs-helperNumberLayer'), + oldArrowLayer = document.querySelector('.introjs-arrow'), + oldtooltipContainer = document.querySelector('.introjs-tooltip'); + _placeTooltip.call(this, this._introItems[this._currentStep].element, oldtooltipContainer, oldArrowLayer, oldHelperNumberLayer); + } + + //re-align hints + _reAlignHints.call(this); + return this; + } + + /** + * Exit from intro + * + * @api private + * @method _exitIntro + * @param {Object} targetElement + * @param {Boolean} force - Setting to `true` will skip the result of beforeExit callback + */ + function _exitIntro(targetElement, force) { + var continueExit = true; + + // calling onbeforeexit callback + // + // If this callback return `false`, it would halt the process + if (this._introBeforeExitCallback !== undefined) { + continueExit = this._introBeforeExitCallback.call(this); + } + + // skip this check if `force` parameter is `true` + // otherwise, if `onbeforeexit` returned `false`, don't exit the intro + if (!force && continueExit === false) return; + + //remove overlay layers from the page + var overlayLayers = targetElement.querySelectorAll('.introjs-overlay'); + + if (overlayLayers && overlayLayers.length) { + _forEach(overlayLayers, function (overlayLayer) { + overlayLayer.style.opacity = 0; + window.setTimeout(function () { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }.bind(overlayLayer), 500); + }.bind(this)); + } + + //remove all helper layers + var helperLayer = targetElement.querySelector('.introjs-helperLayer'); + if (helperLayer) { + helperLayer.parentNode.removeChild(helperLayer); + } + + var referenceLayer = targetElement.querySelector('.introjs-tooltipReferenceLayer'); + if (referenceLayer) { + referenceLayer.parentNode.removeChild(referenceLayer); + } + + //remove disableInteractionLayer + var disableInteractionLayer = targetElement.querySelector('.introjs-disableInteraction'); + if (disableInteractionLayer) { + disableInteractionLayer.parentNode.removeChild(disableInteractionLayer); + } + + //remove intro floating element + var floatingElement = document.querySelector('.introjsFloatingElement'); + if (floatingElement) { + floatingElement.parentNode.removeChild(floatingElement); + } + + _removeShowElement(); + + //remove `introjs-fixParent` class from the elements + var fixParents = document.querySelectorAll('.introjs-fixParent'); + _forEach(fixParents, function (parent) { + _removeClass(parent, /introjs-fixParent/g); + }); + + //clean listeners + DOMEvent.off(window, 'keydown', _onKeyDown, this, true); + DOMEvent.off(window, 'resize', _onResize, this, true); + + //check if any callback is defined + if (this._introExitCallback !== undefined) { + this._introExitCallback.call(this); + } + + //set the step to zero + this._currentStep = undefined; + } + + /** + * Render tooltip box in the page + * + * @api private + * @method _placeTooltip + * @param {HTMLElement} targetElement + * @param {HTMLElement} tooltipLayer + * @param {HTMLElement} arrowLayer + * @param {HTMLElement} helperNumberLayer + * @param {Boolean} hintMode + */ + function _placeTooltip(targetElement, tooltipLayer, arrowLayer, helperNumberLayer, hintMode) { + var tooltipCssClass = '', + currentStepObj, + tooltipOffset, + targetOffset, + windowSize, + currentTooltipPosition; + + hintMode = hintMode || false; + + //reset the old style + tooltipLayer.style.top = null; + tooltipLayer.style.right = null; + tooltipLayer.style.bottom = null; + tooltipLayer.style.left = null; + tooltipLayer.style.marginLeft = null; + tooltipLayer.style.marginTop = null; + + arrowLayer.style.display = 'inherit'; + + if (typeof(helperNumberLayer) !== 'undefined' && helperNumberLayer !== null) { + helperNumberLayer.style.top = null; + helperNumberLayer.style.left = null; + } + + //prevent error when `this._currentStep` is undefined + if (!this._introItems[this._currentStep]) return; + + //if we have a custom css class for each step + currentStepObj = this._introItems[this._currentStep]; + if (typeof (currentStepObj.tooltipClass) === 'string') { + tooltipCssClass = currentStepObj.tooltipClass; + } else { + tooltipCssClass = this._options.tooltipClass; + } + + tooltipLayer.className = ('introjs-tooltip ' + tooltipCssClass).replace(/^\s+|\s+$/g, ''); + tooltipLayer.setAttribute('role', 'dialog'); + + currentTooltipPosition = this._introItems[this._currentStep].position; + + // Floating is always valid, no point in calculating + if (currentTooltipPosition !== "floating") { + currentTooltipPosition = _determineAutoPosition.call(this, targetElement, tooltipLayer, currentTooltipPosition); + } + + var tooltipLayerStyleLeft; + targetOffset = _getOffset(targetElement); + tooltipOffset = _getOffset(tooltipLayer); + windowSize = _getWinSize(); + + _addClass(tooltipLayer, 'introjs-' + currentTooltipPosition); + + switch (currentTooltipPosition) { + case 'top-right-aligned': + arrowLayer.className = 'introjs-arrow bottom-right'; + + var tooltipLayerStyleRight = 0; + _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer); + tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px'; + break; + + case 'top-middle-aligned': + arrowLayer.className = 'introjs-arrow bottom-middle'; + + var tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2; + + // a fix for middle aligned hints + if (hintMode) { + tooltipLayerStyleLeftRight += 5; + } + + if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) { + tooltipLayer.style.right = null; + _checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer); + } + tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px'; + break; + + case 'top-left-aligned': + // top-left-aligned is the same as the default top + case 'top': + arrowLayer.className = 'introjs-arrow bottom'; + + tooltipLayerStyleLeft = (hintMode) ? 0 : 15; + + _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer); + tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px'; + break; + case 'right': + tooltipLayer.style.left = (targetOffset.width + 20) + 'px'; + if (targetOffset.top + tooltipOffset.height > windowSize.height) { + // In this case, right would have fallen below the bottom of the screen. + // Modify so that the bottom of the tooltip connects with the target + arrowLayer.className = "introjs-arrow left-bottom"; + tooltipLayer.style.top = "-" + (tooltipOffset.height - targetOffset.height - 20) + "px"; + } else { + arrowLayer.className = 'introjs-arrow left'; + } + break; + case 'left': + if (!hintMode && this._options.showStepNumbers === true) { + tooltipLayer.style.top = '15px'; + } + + if (targetOffset.top + tooltipOffset.height > windowSize.height) { + // In this case, left would have fallen below the bottom of the screen. + // Modify so that the bottom of the tooltip connects with the target + tooltipLayer.style.top = "-" + (tooltipOffset.height - targetOffset.height - 20) + "px"; + arrowLayer.className = 'introjs-arrow right-bottom'; + } else { + arrowLayer.className = 'introjs-arrow right'; + } + tooltipLayer.style.right = (targetOffset.width + 20) + 'px'; + + break; + case 'floating': + arrowLayer.style.display = 'none'; + + //we have to adjust the top and left of layer manually for intro items without element + tooltipLayer.style.left = '50%'; + tooltipLayer.style.top = '50%'; + tooltipLayer.style.marginLeft = '-' + (tooltipOffset.width / 2) + 'px'; + tooltipLayer.style.marginTop = '-' + (tooltipOffset.height / 2) + 'px'; + + if (typeof(helperNumberLayer) !== 'undefined' && helperNumberLayer !== null) { + helperNumberLayer.style.left = '-' + ((tooltipOffset.width / 2) + 18) + 'px'; + helperNumberLayer.style.top = '-' + ((tooltipOffset.height / 2) + 18) + 'px'; + } + + break; + case 'bottom-right-aligned': + arrowLayer.className = 'introjs-arrow top-right'; + + tooltipLayerStyleRight = 0; + _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer); + tooltipLayer.style.top = (targetOffset.height + 20) + 'px'; + break; + + case 'bottom-middle-aligned': + arrowLayer.className = 'introjs-arrow top-middle'; + + tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2; + + // a fix for middle aligned hints + if (hintMode) { + tooltipLayerStyleLeftRight += 5; + } + + if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) { + tooltipLayer.style.right = null; + _checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer); + } + tooltipLayer.style.top = (targetOffset.height + 20) + 'px'; + break; + + // case 'bottom-left-aligned': + // Bottom-left-aligned is the same as the default bottom + // case 'bottom': + // Bottom going to follow the default behavior + default: + arrowLayer.className = 'introjs-arrow top'; + + tooltipLayerStyleLeft = 0; + _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer); + tooltipLayer.style.top = (targetOffset.height + 20) + 'px'; + } + } + + /** + * Set tooltip left so it doesn't go off the right side of the window + * + * @return boolean true, if tooltipLayerStyleLeft is ok. false, otherwise. + */ + function _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer) { + if (targetOffset.left + tooltipLayerStyleLeft + tooltipOffset.width > windowSize.width) { + // off the right side of the window + tooltipLayer.style.left = (windowSize.width - tooltipOffset.width - targetOffset.left) + 'px'; + return false; + } + tooltipLayer.style.left = tooltipLayerStyleLeft + 'px'; + return true; + } + + /** + * Set tooltip right so it doesn't go off the left side of the window + * + * @return boolean true, if tooltipLayerStyleRight is ok. false, otherwise. + */ + function _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer) { + if (targetOffset.left + targetOffset.width - tooltipLayerStyleRight - tooltipOffset.width < 0) { + // off the left side of the window + tooltipLayer.style.left = (-targetOffset.left) + 'px'; + return false; + } + tooltipLayer.style.right = tooltipLayerStyleRight + 'px'; + return true; + } + + /** + * Determines the position of the tooltip based on the position precedence and availability + * of screen space. + * + * @param {Object} targetElement + * @param {Object} tooltipLayer + * @param {String} desiredTooltipPosition + * @return {String} calculatedPosition + */ + function _determineAutoPosition(targetElement, tooltipLayer, desiredTooltipPosition) { + + // Take a clone of position precedence. These will be the available + var possiblePositions = this._options.positionPrecedence.slice(); + + var windowSize = _getWinSize(); + var tooltipHeight = _getOffset(tooltipLayer).height + 10; + var tooltipWidth = _getOffset(tooltipLayer).width + 20; + var targetElementRect = targetElement.getBoundingClientRect(); + + // If we check all the possible areas, and there are no valid places for the tooltip, the element + // must take up most of the screen real estate. Show the tooltip floating in the middle of the screen. + var calculatedPosition = "floating"; + + /* + * auto determine position + */ + + // Check for space below + if (targetElementRect.bottom + tooltipHeight > windowSize.height) { + _removeEntry(possiblePositions, "bottom"); + } + + // Check for space above + if (targetElementRect.top - tooltipHeight < 0) { + _removeEntry(possiblePositions, "top"); + } + + // Check for space to the right + if (targetElementRect.right + tooltipWidth > windowSize.width) { + _removeEntry(possiblePositions, "right"); + } + + // Check for space to the left + if (targetElementRect.left - tooltipWidth < 0) { + _removeEntry(possiblePositions, "left"); + } + + // @var {String} ex: 'right-aligned' + var desiredAlignment = (function (pos) { + var hyphenIndex = pos.indexOf('-'); + if (hyphenIndex !== -1) { + // has alignment + return pos.substr(hyphenIndex); + } + return ''; + })(desiredTooltipPosition || ''); + + // strip alignment from position + if (desiredTooltipPosition) { + // ex: "bottom-right-aligned" + // should return 'bottom' + desiredTooltipPosition = desiredTooltipPosition.split('-')[0]; + } + + if (possiblePositions.length) { + if (desiredTooltipPosition !== "auto" && + possiblePositions.indexOf(desiredTooltipPosition) > -1) { + // If the requested position is in the list, choose that + calculatedPosition = desiredTooltipPosition; + } else { + // Pick the first valid position, in order + calculatedPosition = possiblePositions[0]; + } + } + + // only top and bottom positions have optional alignments + if (['top', 'bottom'].indexOf(calculatedPosition) !== -1) { + calculatedPosition += _determineAutoAlignment(targetElementRect.left, tooltipWidth, windowSize, desiredAlignment); + } + + return calculatedPosition; + } + + /** + * auto-determine alignment + * @param {Integer} offsetLeft + * @param {Integer} tooltipWidth + * @param {Object} windowSize + * @param {String} desiredAlignment + * @return {String} calculatedAlignment + */ + function _determineAutoAlignment (offsetLeft, tooltipWidth, windowSize, desiredAlignment) { + var halfTooltipWidth = tooltipWidth / 2, + winWidth = Math.min(windowSize.width, window.screen.width), + possibleAlignments = ['-left-aligned', '-middle-aligned', '-right-aligned'], + calculatedAlignment = ''; + + // valid left must be at least a tooltipWidth + // away from right side + if (winWidth - offsetLeft < tooltipWidth) { + _removeEntry(possibleAlignments, '-left-aligned'); + } + + // valid middle must be at least half + // width away from both sides + if (offsetLeft < halfTooltipWidth || + winWidth - offsetLeft < halfTooltipWidth) { + _removeEntry(possibleAlignments, '-middle-aligned'); + } + + // valid right must be at least a tooltipWidth + // width away from left side + if (offsetLeft < tooltipWidth) { + _removeEntry(possibleAlignments, '-right-aligned'); + } + + if (possibleAlignments.length) { + if (possibleAlignments.indexOf(desiredAlignment) !== -1) { + // the desired alignment is valid + calculatedAlignment = desiredAlignment; + } else { + // pick the first valid position, in order + calculatedAlignment = possibleAlignments[0]; + } + } else { + // if screen width is too small + // for ANY alignment, middle is + // probably the best for visibility + calculatedAlignment = '-middle-aligned'; + } + + return calculatedAlignment; + } + + /** + * Remove an entry from a string array if it's there, does nothing if it isn't there. + * + * @param {Array} stringArray + * @param {String} stringToRemove + */ + function _removeEntry(stringArray, stringToRemove) { + if (stringArray.indexOf(stringToRemove) > -1) { + stringArray.splice(stringArray.indexOf(stringToRemove), 1); + } + } + + /** + * Update the position of the helper layer on the screen + * + * @api private + * @method _setHelperLayerPosition + * @param {Object} helperLayer + */ + function _setHelperLayerPosition(helperLayer) { + if (helperLayer) { + //prevent error when `this._currentStep` in undefined + if (!this._introItems[this._currentStep]) return; + + var currentElement = this._introItems[this._currentStep], + elementPosition = _getOffset(currentElement.element), + widthHeightPadding = this._options.helperElementPadding; + + // If the target element is fixed, the tooltip should be fixed as well. + // Otherwise, remove a fixed class that may be left over from the previous + // step. + if (_isFixed(currentElement.element)) { + _addClass(helperLayer, 'introjs-fixedTooltip'); + } else { + _removeClass(helperLayer, 'introjs-fixedTooltip'); + } + + if (currentElement.position === 'floating') { + widthHeightPadding = 0; + } + + //set new position to helper layer + helperLayer.style.cssText = 'width: ' + (elementPosition.width + widthHeightPadding) + 'px; ' + + 'height:' + (elementPosition.height + widthHeightPadding) + 'px; ' + + 'top:' + (elementPosition.top - widthHeightPadding / 2) + 'px;' + + 'left: ' + (elementPosition.left - widthHeightPadding / 2) + 'px;'; + + } + } + + /** + * Add disableinteraction layer and adjust the size and position of the layer + * + * @api private + * @method _disableInteraction + */ + function _disableInteraction() { + var disableInteractionLayer = document.querySelector('.introjs-disableInteraction'); + + if (disableInteractionLayer === null) { + disableInteractionLayer = document.createElement('div'); + disableInteractionLayer.className = 'introjs-disableInteraction'; + this._targetElement.appendChild(disableInteractionLayer); + } + + _setHelperLayerPosition.call(this, disableInteractionLayer); + } + + /** + * Setting anchors to behave like buttons + * + * @api private + * @method _setAnchorAsButton + */ + function _setAnchorAsButton(anchor){ + anchor.setAttribute('role', 'button'); + anchor.tabIndex = 0; + } + + /** + * Show an element on the page + * + * @api private + * @method _showElement + * @param {Object} targetElement + */ + function _showElement(targetElement) { + if (typeof (this._introChangeCallback) !== 'undefined') { + this._introChangeCallback.call(this, targetElement.element); + } + + var self = this, + oldHelperLayer = document.querySelector('.introjs-helperLayer'), + oldReferenceLayer = document.querySelector('.introjs-tooltipReferenceLayer'), + highlightClass = 'introjs-helperLayer', + nextTooltipButton, + prevTooltipButton, + skipTooltipButton, + scrollParent; + + //check for a current step highlight class + if (typeof (targetElement.highlightClass) === 'string') { + highlightClass += (' ' + targetElement.highlightClass); + } + //check for options highlight class + if (typeof (this._options.highlightClass) === 'string') { + highlightClass += (' ' + this._options.highlightClass); + } + + if (oldHelperLayer !== null) { + var oldHelperNumberLayer = oldReferenceLayer.querySelector('.introjs-helperNumberLayer'), + oldtooltipLayer = oldReferenceLayer.querySelector('.introjs-tooltiptext'), + oldArrowLayer = oldReferenceLayer.querySelector('.introjs-arrow'), + oldtooltipContainer = oldReferenceLayer.querySelector('.introjs-tooltip'); + + skipTooltipButton = oldReferenceLayer.querySelector('.introjs-skipbutton'); + prevTooltipButton = oldReferenceLayer.querySelector('.introjs-prevbutton'); + nextTooltipButton = oldReferenceLayer.querySelector('.introjs-nextbutton'); + + //update or reset the helper highlight class + oldHelperLayer.className = highlightClass; + //hide the tooltip + oldtooltipContainer.style.opacity = 0; + oldtooltipContainer.style.display = "none"; + + if (oldHelperNumberLayer !== null) { + var lastIntroItem = this._introItems[(targetElement.step - 2 >= 0 ? targetElement.step - 2 : 0)]; + + if (lastIntroItem !== null && (this._direction === 'forward' && lastIntroItem.position === 'floating') || (this._direction === 'backward' && targetElement.position === 'floating')) { + oldHelperNumberLayer.style.opacity = 0; + } + } + + // scroll to element + scrollParent = _getScrollParent( targetElement.element ); + + if (scrollParent !== document.body) { + // target is within a scrollable element + _scrollParentToElement(scrollParent, targetElement.element); + } + + // set new position to helper layer + _setHelperLayerPosition.call(self, oldHelperLayer); + _setHelperLayerPosition.call(self, oldReferenceLayer); + + //remove `introjs-fixParent` class from the elements + var fixParents = document.querySelectorAll('.introjs-fixParent'); + _forEach(fixParents, function (parent) { + _removeClass(parent, /introjs-fixParent/g); + }); + + //remove old classes if the element still exist + _removeShowElement(); + + //we should wait until the CSS3 transition is competed (it's 0.3 sec) to prevent incorrect `height` and `width` calculation + if (self._lastShowElementTimer) { + window.clearTimeout(self._lastShowElementTimer); + } + + self._lastShowElementTimer = window.setTimeout(function() { + //set current step to the label + if (oldHelperNumberLayer !== null) { + oldHelperNumberLayer.innerHTML = targetElement.step; + } + //set current tooltip text + oldtooltipLayer.innerHTML = targetElement.intro; + //set the tooltip position + oldtooltipContainer.style.display = "block"; + _placeTooltip.call(self, targetElement.element, oldtooltipContainer, oldArrowLayer, oldHelperNumberLayer); + + //change active bullet + if (self._options.showBullets) { + oldReferenceLayer.querySelector('.introjs-bullets li > a.active').className = ''; + oldReferenceLayer.querySelector('.introjs-bullets li > a[data-stepnumber="' + targetElement.step + '"]').className = 'active'; + } + oldReferenceLayer.querySelector('.introjs-progress .introjs-progressbar').style.cssText = 'width:' + _getProgress.call(self) + '%;'; + oldReferenceLayer.querySelector('.introjs-progress .introjs-progressbar').setAttribute('aria-valuenow', _getProgress.call(self)); + + //show the tooltip + oldtooltipContainer.style.opacity = 1; + if (oldHelperNumberLayer) oldHelperNumberLayer.style.opacity = 1; + + //reset button focus + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null && /introjs-donebutton/gi.test(skipTooltipButton.className)) { + // skip button is now "done" button + skipTooltipButton.focus(); + } else if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + //still in the tour, focus on next + nextTooltipButton.focus(); + } + + // change the scroll of the window, if needed + _scrollTo.call(self, targetElement.scrollTo, targetElement, oldtooltipLayer); + }, 350); + + // end of old element if-else condition + } else { + var helperLayer = document.createElement('div'), + referenceLayer = document.createElement('div'), + arrowLayer = document.createElement('div'), + tooltipLayer = document.createElement('div'), + tooltipTextLayer = document.createElement('div'), + bulletsLayer = document.createElement('div'), + progressLayer = document.createElement('div'), + buttonsLayer = document.createElement('div'); + + helperLayer.className = highlightClass; + referenceLayer.className = 'introjs-tooltipReferenceLayer'; + + // scroll to element + scrollParent = _getScrollParent( targetElement.element ); + + if (scrollParent !== document.body) { + // target is within a scrollable element + _scrollParentToElement(scrollParent, targetElement.element); + } + + //set new position to helper layer + _setHelperLayerPosition.call(self, helperLayer); + _setHelperLayerPosition.call(self, referenceLayer); + + //add helper layer to target element + this._targetElement.appendChild(helperLayer); + this._targetElement.appendChild(referenceLayer); + + arrowLayer.className = 'introjs-arrow'; + + tooltipTextLayer.className = 'introjs-tooltiptext'; + tooltipTextLayer.innerHTML = targetElement.intro; + + bulletsLayer.className = 'introjs-bullets'; + + if (this._options.showBullets === false) { + bulletsLayer.style.display = 'none'; + } + + var ulContainer = document.createElement('ul'); + ulContainer.setAttribute('role', 'tablist'); + + var anchorClick = function () { + self.goToStep(this.getAttribute('data-stepnumber')); + }; + + _forEach(this._introItems, function (item, i) { + var innerLi = document.createElement('li'); + var anchorLink = document.createElement('a'); + + innerLi.setAttribute('role', 'presentation'); + anchorLink.setAttribute('role', 'tab'); + + anchorLink.onclick = anchorClick; + + if (i === (targetElement.step-1)) { + anchorLink.className = 'active'; + } + + _setAnchorAsButton(anchorLink); + anchorLink.innerHTML = " "; + anchorLink.setAttribute('data-stepnumber', item.step); + + innerLi.appendChild(anchorLink); + ulContainer.appendChild(innerLi); + }); + + bulletsLayer.appendChild(ulContainer); + + progressLayer.className = 'introjs-progress'; + + if (this._options.showProgress === false) { + progressLayer.style.display = 'none'; + } + var progressBar = document.createElement('div'); + progressBar.className = 'introjs-progressbar'; + progressBar.setAttribute('role', 'progress'); + progressBar.setAttribute('aria-valuemin', 0); + progressBar.setAttribute('aria-valuemax', 100); + progressBar.setAttribute('aria-valuenow', _getProgress.call(this)); + progressBar.style.cssText = 'width:' + _getProgress.call(this) + '%;'; + + progressLayer.appendChild(progressBar); + + buttonsLayer.className = 'introjs-tooltipbuttons'; + if (this._options.showButtons === false) { + buttonsLayer.style.display = 'none'; + } + + tooltipLayer.className = 'introjs-tooltip'; + tooltipLayer.appendChild(tooltipTextLayer); + tooltipLayer.appendChild(bulletsLayer); + tooltipLayer.appendChild(progressLayer); + + //add helper layer number + var helperNumberLayer = document.createElement('span'); + if (this._options.showStepNumbers === true) { + helperNumberLayer.className = 'introjs-helperNumberLayer'; + helperNumberLayer.innerHTML = targetElement.step; + referenceLayer.appendChild(helperNumberLayer); + } + + tooltipLayer.appendChild(arrowLayer); + referenceLayer.appendChild(tooltipLayer); + + //next button + nextTooltipButton = document.createElement('a'); + + nextTooltipButton.onclick = function() { + if (self._introItems.length - 1 !== self._currentStep) { + _nextStep.call(self); + } + }; + + _setAnchorAsButton(nextTooltipButton); + nextTooltipButton.innerHTML = this._options.nextLabel; + + //previous button + prevTooltipButton = document.createElement('a'); + + prevTooltipButton.onclick = function() { + if (self._currentStep !== 0) { + _previousStep.call(self); + } + }; + + _setAnchorAsButton(prevTooltipButton); + prevTooltipButton.innerHTML = this._options.prevLabel; + + //skip button + skipTooltipButton = document.createElement('a'); + skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton '; + _setAnchorAsButton(skipTooltipButton); + skipTooltipButton.innerHTML = this._options.skipLabel; + + skipTooltipButton.onclick = function() { + if (self._introItems.length - 1 === self._currentStep && typeof (self._introCompleteCallback) === 'function') { + self._introCompleteCallback.call(self); + } + + if (self._introItems.length - 1 !== self._currentStep && typeof (self._introExitCallback) === 'function') { + self._introExitCallback.call(self); + } + + if (typeof(self._introSkipCallback) === 'function') { + self._introSkipCallback.call(self); + } + + _exitIntro.call(self, self._targetElement); + }; + + buttonsLayer.appendChild(skipTooltipButton); + + //in order to prevent displaying next/previous button always + if (this._introItems.length > 1) { + buttonsLayer.appendChild(prevTooltipButton); + buttonsLayer.appendChild(nextTooltipButton); + } + + tooltipLayer.appendChild(buttonsLayer); + + //set proper position + _placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer, helperNumberLayer); + + // change the scroll of the window, if needed + _scrollTo.call(this, targetElement.scrollTo, targetElement, tooltipLayer); + + //end of new element if-else condition + } + + // removing previous disable interaction layer + var disableInteractionLayer = self._targetElement.querySelector('.introjs-disableInteraction'); + if (disableInteractionLayer) { + disableInteractionLayer.parentNode.removeChild(disableInteractionLayer); + } + + //disable interaction + if (targetElement.disableInteraction) { + _disableInteraction.call(self); + } + + // when it's the first step of tour + if (this._currentStep === 0 && this._introItems.length > 1) { + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton'; + } + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton'; + } + + if (this._options.hidePrev === true) { + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton introjs-hidden'; + } + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + _addClass(nextTooltipButton, 'introjs-fullbutton'); + } + } else { + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton introjs-disabled'; + } + } + + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.innerHTML = this._options.skipLabel; + } + } else if (this._introItems.length - 1 === this._currentStep || this._introItems.length === 1) { + // last step of tour + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.innerHTML = this._options.doneLabel; + // adding donebutton class in addition to skipbutton + _addClass(skipTooltipButton, 'introjs-donebutton'); + } + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton'; + } + + if (this._options.hideNext === true) { + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton introjs-hidden'; + } + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + _addClass(prevTooltipButton, 'introjs-fullbutton'); + } + } else { + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton introjs-disabled'; + } + } + } else { + // steps between start and end + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton'; + } + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton'; + } + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton'; + } + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.innerHTML = this._options.skipLabel; + } + } + + prevTooltipButton.setAttribute('role', 'button'); + nextTooltipButton.setAttribute('role', 'button'); + skipTooltipButton.setAttribute('role', 'button'); + + //Set focus on "next" button, so that hitting Enter always moves you onto the next step + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.focus(); + } + + _setShowElement(targetElement); + + if (typeof (this._introAfterChangeCallback) !== 'undefined') { + this._introAfterChangeCallback.call(this, targetElement.element); + } + } + + /** + * To change the scroll of `window` after highlighting an element + * + * @api private + * @method _scrollTo + * @param {String} scrollTo + * @param {Object} targetElement + * @param {Object} tooltipLayer + */ + function _scrollTo(scrollTo, targetElement, tooltipLayer) { + if (scrollTo === 'off') return; + var rect; + + if (!this._options.scrollToElement) return; + + if (scrollTo === 'tooltip') { + rect = tooltipLayer.getBoundingClientRect(); + } else { + rect = targetElement.element.getBoundingClientRect(); + } + + if (!_elementInViewport(targetElement.element)) { + var winHeight = _getWinSize().height; + var top = rect.bottom - (rect.bottom - rect.top); + + // TODO (afshinm): do we need scroll padding now? + // I have changed the scroll option and now it scrolls the window to + // the center of the target element or tooltip. + + if (top < 0 || targetElement.element.clientHeight > winHeight) { + window.scrollBy(0, rect.top - ((winHeight / 2) - (rect.height / 2)) - this._options.scrollPadding); // 30px padding from edge to look nice + + //Scroll down + } else { + window.scrollBy(0, rect.top - ((winHeight / 2) - (rect.height / 2)) + this._options.scrollPadding); // 30px padding from edge to look nice + } + } + } + + /** + * To remove all show element(s) + * + * @api private + * @method _removeShowElement + */ + function _removeShowElement() { + var elms = document.querySelectorAll('.introjs-showElement'); + + _forEach(elms, function (elm) { + _removeClass(elm, /introjs-[a-zA-Z]+/g); + }); + } + + /** + * To set the show element + * This function set a relative (in most cases) position and changes the z-index + * + * @api private + * @method _setShowElement + * @param {Object} targetElement + */ + function _setShowElement(targetElement) { + var parentElm; + // we need to add this show element class to the parent of SVG elements + // because the SVG elements can't have independent z-index + if (targetElement.element instanceof SVGElement) { + parentElm = targetElement.element.parentNode; + + while (targetElement.element.parentNode !== null) { + if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break; + + if (parentElm.tagName.toLowerCase() === 'svg') { + _addClass(parentElm, 'introjs-showElement introjs-relativePosition'); + } + + parentElm = parentElm.parentNode; + } + } + + _addClass(targetElement.element, 'introjs-showElement'); + + var currentElementPosition = _getPropValue(targetElement.element, 'position'); + if (currentElementPosition !== 'absolute' && + currentElementPosition !== 'relative' && + currentElementPosition !== 'fixed') { + //change to new intro item + _addClass(targetElement.element, 'introjs-relativePosition'); + } + + parentElm = targetElement.element.parentNode; + while (parentElm !== null) { + if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break; + + //fix The Stacking Context problem. + //More detail: https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context + var zIndex = _getPropValue(parentElm, 'z-index'); + var opacity = parseFloat(_getPropValue(parentElm, 'opacity')); + var transform = _getPropValue(parentElm, 'transform') || _getPropValue(parentElm, '-webkit-transform') || _getPropValue(parentElm, '-moz-transform') || _getPropValue(parentElm, '-ms-transform') || _getPropValue(parentElm, '-o-transform'); + if (/[0-9]+/.test(zIndex) || opacity < 1 || (transform !== 'none' && transform !== undefined)) { + _addClass(parentElm, 'introjs-fixParent'); + } + + parentElm = parentElm.parentNode; + } + } + + /** + * Iterates arrays + * + * @param {Array} arr + * @param {Function} forEachFnc + * @param {Function} completeFnc + * @return {Null} + */ + function _forEach(arr, forEachFnc, completeFnc) { + // in case arr is an empty query selector node list + if (arr) { + for (var i = 0, len = arr.length; i < len; i++) { + forEachFnc(arr[i], i); + } + } + + if (typeof(completeFnc) === 'function') { + completeFnc(); + } + } + + /** + * Mark any object with an incrementing number + * used for keeping track of objects + * + * @param Object obj Any object or DOM Element + * @param String key + * @return Object + */ + var _stamp = (function () { + var keys = {}; + return function stamp (obj, key) { + + // get group key + key = key || 'introjs-stamp'; + + // each group increments from 0 + keys[key] = keys[key] || 0; + + // stamp only once per object + if (obj[key] === undefined) { + // increment key for each new object + obj[key] = keys[key]++; + } + + return obj[key]; + }; + })(); + + /** + * DOMEvent Handles all DOM events + * + * methods: + * + * on - add event handler + * off - remove event + */ + var DOMEvent = (function () { + function DOMEvent () { + var events_key = 'introjs_event'; + + /** + * Gets a unique ID for an event listener + * + * @param Object obj + * @param String type event type + * @param Function listener + * @param Object context + * @return String + */ + this._id = function (obj, type, listener, context) { + return type + _stamp(listener) + (context ? '_' + _stamp(context) : ''); + }; + + /** + * Adds event listener + * + * @param Object obj + * @param String type event type + * @param Function listener + * @param Object context + * @param Boolean useCapture + * @return null + */ + this.on = function (obj, type, listener, context, useCapture) { + var id = this._id.apply(this, arguments), + handler = function (e) { + return listener.call(context || obj, e || window.event); + }; + + if ('addEventListener' in obj) { + obj.addEventListener(type, handler, useCapture); + } else if ('attachEvent' in obj) { + obj.attachEvent('on' + type, handler); + } + + obj[events_key] = obj[events_key] || {}; + obj[events_key][id] = handler; + }; + + /** + * Removes event listener + * + * @param Object obj + * @param String type event type + * @param Function listener + * @param Object context + * @param Boolean useCapture + * @return null + */ + this.off = function (obj, type, listener, context, useCapture) { + var id = this._id.apply(this, arguments), + handler = obj[events_key] && obj[events_key][id]; + + if (!handler) { + return; + } + + if ('removeEventListener' in obj) { + obj.removeEventListener(type, handler, useCapture); + } else if ('detachEvent' in obj) { + obj.detachEvent('on' + type, handler); + } + + obj[events_key][id] = null; + }; + } + + return new DOMEvent(); + })(); + + /** + * Append a class to an element + * + * @api private + * @method _addClass + * @param {Object} element + * @param {String} className + * @returns null + */ + function _addClass(element, className) { + if (element instanceof SVGElement) { + // svg + var pre = element.getAttribute('class') || ''; + + element.setAttribute('class', pre + ' ' + className); + } else { + if (element.classList !== undefined) { + // check for modern classList property + var classes = className.split(' '); + _forEach(classes, function (cls) { + element.classList.add( cls ); + }); + } else if (!element.className.match( className )) { + // check if element doesn't already have className + element.className += ' ' + className; + } + } + } + + /** + * Remove a class from an element + * + * @api private + * @method _removeClass + * @param {Object} element + * @param {RegExp|String} classNameRegex can be regex or string + * @returns null + */ + function _removeClass(element, classNameRegex) { + if (element instanceof SVGElement) { + var pre = element.getAttribute('class') || ''; + + element.setAttribute('class', pre.replace(classNameRegex, '').replace(/^\s+|\s+$/g, '')); + } else { + element.className = element.className.replace(classNameRegex, '').replace(/^\s+|\s+$/g, ''); + } + } + + /** + * Get an element CSS property on the page + * Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml + * + * @api private + * @method _getPropValue + * @param {Object} element + * @param {String} propName + * @returns Element's property value + */ + function _getPropValue (element, propName) { + var propValue = ''; + if (element.currentStyle) { //IE + propValue = element.currentStyle[propName]; + } else if (document.defaultView && document.defaultView.getComputedStyle) { //Others + propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); + } + + //Prevent exception in IE + if (propValue && propValue.toLowerCase) { + return propValue.toLowerCase(); + } else { + return propValue; + } + } + + /** + * Checks to see if target element (or parents) position is fixed or not + * + * @api private + * @method _isFixed + * @param {Object} element + * @returns Boolean + */ + function _isFixed (element) { + var p = element.parentNode; + + if (!p || p.nodeName === 'HTML') { + return false; + } + + if (_getPropValue(element, 'position') === 'fixed') { + return true; + } + + return _isFixed(p); + } + + /** + * Provides a cross-browser way to get the screen dimensions + * via: http://stackoverflow.com/questions/5864467/internet-explorer-innerheight + * + * @api private + * @method _getWinSize + * @returns {Object} width and height attributes + */ + function _getWinSize() { + if (window.innerWidth !== undefined) { + return { width: window.innerWidth, height: window.innerHeight }; + } else { + var D = document.documentElement; + return { width: D.clientWidth, height: D.clientHeight }; + } + } + + /** + * Check to see if the element is in the viewport or not + * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport + * + * @api private + * @method _elementInViewport + * @param {Object} el + */ + function _elementInViewport(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + (rect.bottom+80) <= window.innerHeight && // add 80 to get the text right + rect.right <= window.innerWidth + ); + } + + /** + * Add overlay layer to the page + * + * @api private + * @method _addOverlayLayer + * @param {Object} targetElm + */ + function _addOverlayLayer(targetElm) { + var overlayLayer = document.createElement('div'), + styleText = '', + self = this; + + //set css class name + overlayLayer.className = 'introjs-overlay'; + + //check if the target element is body, we should calculate the size of overlay layer in a better way + if (!targetElm.tagName || targetElm.tagName.toLowerCase() === 'body') { + styleText += 'top: 0;bottom: 0; left: 0;right: 0;position: fixed;'; + overlayLayer.style.cssText = styleText; + } else { + //set overlay layer position + var elementPosition = _getOffset(targetElm); + if (elementPosition) { + styleText += 'width: ' + elementPosition.width + 'px; height:' + elementPosition.height + 'px; top:' + elementPosition.top + 'px;left: ' + elementPosition.left + 'px;'; + overlayLayer.style.cssText = styleText; + } + } + + targetElm.appendChild(overlayLayer); + + overlayLayer.onclick = function() { + if (self._options.exitOnOverlayClick === true) { + _exitIntro.call(self, targetElm); + } + }; + + window.setTimeout(function() { + styleText += 'opacity: ' + self._options.overlayOpacity.toString() + ';'; + overlayLayer.style.cssText = styleText; + }, 10); + + return true; + } + + /** + * Removes open hint (tooltip hint) + * + * @api private + * @method _removeHintTooltip + */ + function _removeHintTooltip() { + var tooltip = document.querySelector('.introjs-hintReference'); + + if (tooltip) { + var step = tooltip.getAttribute('data-step'); + tooltip.parentNode.removeChild(tooltip); + return step; + } + } + + /** + * Start parsing hint items + * + * @api private + * @param {Object} targetElm + * @method _startHint + */ + function _populateHints(targetElm) { + + this._introItems = []; + + if (this._options.hints) { + _forEach(this._options.hints, function (hint) { + var currentItem = _cloneObject(hint); + + if (typeof(currentItem.element) === 'string') { + //grab the element with given selector from the page + currentItem.element = document.querySelector(currentItem.element); + } + + currentItem.hintPosition = currentItem.hintPosition || this._options.hintPosition; + currentItem.hintAnimation = currentItem.hintAnimation || this._options.hintAnimation; + + if (currentItem.element !== null) { + this._introItems.push(currentItem); + } + }.bind(this)); + } else { + var hints = targetElm.querySelectorAll('*[data-hint]'); + + if (!hints || !hints.length) { + return false; + } + + //first add intro items with data-step + _forEach(hints, function (currentElement) { + // hint animation + var hintAnimation = currentElement.getAttribute('data-hintanimation'); + + if (hintAnimation) { + hintAnimation = (hintAnimation === 'true'); + } else { + hintAnimation = this._options.hintAnimation; + } + + this._introItems.push({ + element: currentElement, + hint: currentElement.getAttribute('data-hint'), + hintPosition: currentElement.getAttribute('data-hintposition') || this._options.hintPosition, + hintAnimation: hintAnimation, + tooltipClass: currentElement.getAttribute('data-tooltipclass'), + position: currentElement.getAttribute('data-position') || this._options.tooltipPosition + }); + }.bind(this)); + } + + _addHints.call(this); + + /* + todo: + these events should be removed at some point + */ + DOMEvent.on(document, 'click', _removeHintTooltip, this, false); + DOMEvent.on(window, 'resize', _reAlignHints, this, true); + } + + /** + * Re-aligns all hint elements + * + * @api private + * @method _reAlignHints + */ + function _reAlignHints() { + _forEach(this._introItems, function (item) { + if (typeof(item.targetElement) === 'undefined') { + return; + } + + _alignHintPosition.call(this, item.hintPosition, item.element, item.targetElement); + }.bind(this)); + } + + /** + * Get a queryselector within the hint wrapper + * + * @param {String} selector + * @return {NodeList|Array} + */ + function _hintQuerySelectorAll(selector) { + var hintsWrapper = document.querySelector('.introjs-hints'); + return (hintsWrapper) ? hintsWrapper.querySelectorAll(selector) : []; + } + + /** + * Hide a hint + * + * @api private + * @method _hideHint + */ + function _hideHint(stepId) { + var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0]; + + _removeHintTooltip.call(this); + + if (hint) { + _addClass(hint, 'introjs-hidehint'); + } + + // call the callback function (if any) + if (typeof (this._hintCloseCallback) !== 'undefined') { + this._hintCloseCallback.call(this, stepId); + } + } + + /** + * Hide all hints + * + * @api private + * @method _hideHints + */ + function _hideHints() { + var hints = _hintQuerySelectorAll('.introjs-hint'); + + _forEach(hints, function (hint) { + _hideHint.call(this, hint.getAttribute('data-step')); + }.bind(this)); + } + + /** + * Show all hints + * + * @api private + * @method _showHints + */ + function _showHints() { + var hints = _hintQuerySelectorAll('.introjs-hint'); + + if (hints && hints.length) { + _forEach(hints, function (hint) { + _showHint.call(this, hint.getAttribute('data-step')); + }.bind(this)); + } else { + _populateHints.call(this, this._targetElement); + } + } + + /** + * Show a hint + * + * @api private + * @method _showHint + */ + function _showHint(stepId) { + var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0]; + + if (hint) { + _removeClass(hint, /introjs-hidehint/g); + } + } + + /** + * Removes all hint elements on the page + * Useful when you want to destroy the elements and add them again (e.g. a modal or popup) + * + * @api private + * @method _removeHints + */ + function _removeHints() { + var hints = _hintQuerySelectorAll('.introjs-hint'); + + _forEach(hints, function (hint) { + _removeHint.call(this, hint.getAttribute('data-step')); + }.bind(this)); + } + + /** + * Remove one single hint element from the page + * Useful when you want to destroy the element and add them again (e.g. a modal or popup) + * Use removeHints if you want to remove all elements. + * + * @api private + * @method _removeHint + */ + function _removeHint(stepId) { + var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0]; + + if (hint) { + hint.parentNode.removeChild(hint); + } + } + + /** + * Add all available hints to the page + * + * @api private + * @method _addHints + */ + function _addHints() { + var self = this; + + var hintsWrapper = document.querySelector('.introjs-hints'); + + if (hintsWrapper === null) { + hintsWrapper = document.createElement('div'); + hintsWrapper.className = 'introjs-hints'; + } + + /** + * Returns an event handler unique to the hint iteration + * + * @param {Integer} i + * @return {Function} + */ + var getHintClick = function (i) { + return function(e) { + var evt = e ? e : window.event; + + if (evt.stopPropagation) { + evt.stopPropagation(); + } + + if (evt.cancelBubble !== null) { + evt.cancelBubble = true; + } + + _showHintDialog.call(self, i); + }; + }; + + _forEach(this._introItems, function(item, i) { + // avoid append a hint twice + if (document.querySelector('.introjs-hint[data-step="' + i + '"]')) { + return; + } + + var hint = document.createElement('a'); + _setAnchorAsButton(hint); + + hint.onclick = getHintClick(i); + + hint.className = 'introjs-hint'; + + if (!item.hintAnimation) { + _addClass(hint, 'introjs-hint-no-anim'); + } + + // hint's position should be fixed if the target element's position is fixed + if (_isFixed(item.element)) { + _addClass(hint, 'introjs-fixedhint'); + } + + var hintDot = document.createElement('div'); + hintDot.className = 'introjs-hint-dot'; + var hintPulse = document.createElement('div'); + hintPulse.className = 'introjs-hint-pulse'; + + hint.appendChild(hintDot); + hint.appendChild(hintPulse); + hint.setAttribute('data-step', i); + + // we swap the hint element with target element + // because _setHelperLayerPosition uses `element` property + item.targetElement = item.element; + item.element = hint; + + // align the hint position + _alignHintPosition.call(this, item.hintPosition, hint, item.targetElement); + + hintsWrapper.appendChild(hint); + }.bind(this)); + + // adding the hints wrapper + document.body.appendChild(hintsWrapper); + + // call the callback function (if any) + if (typeof (this._hintsAddedCallback) !== 'undefined') { + this._hintsAddedCallback.call(this); + } + } + + /** + * Aligns hint position + * + * @api private + * @method _alignHintPosition + * @param {String} position + * @param {Object} hint + * @param {Object} element + */ + function _alignHintPosition(position, hint, element) { + // get/calculate offset of target element + var offset = _getOffset.call(this, element); + var iconWidth = 20; + var iconHeight = 20; + + // align the hint element + switch (position) { + default: + case 'top-left': + hint.style.left = offset.left + 'px'; + hint.style.top = offset.top + 'px'; + break; + case 'top-right': + hint.style.left = (offset.left + offset.width - iconWidth) + 'px'; + hint.style.top = offset.top + 'px'; + break; + case 'bottom-left': + hint.style.left = offset.left + 'px'; + hint.style.top = (offset.top + offset.height - iconHeight) + 'px'; + break; + case 'bottom-right': + hint.style.left = (offset.left + offset.width - iconWidth) + 'px'; + hint.style.top = (offset.top + offset.height - iconHeight) + 'px'; + break; + case 'middle-left': + hint.style.left = offset.left + 'px'; + hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px'; + break; + case 'middle-right': + hint.style.left = (offset.left + offset.width - iconWidth) + 'px'; + hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px'; + break; + case 'middle-middle': + hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px'; + hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px'; + break; + case 'bottom-middle': + hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px'; + hint.style.top = (offset.top + offset.height - iconHeight) + 'px'; + break; + case 'top-middle': + hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px'; + hint.style.top = offset.top + 'px'; + break; + } + } + + /** + * Triggers when user clicks on the hint element + * + * @api private + * @method _showHintDialog + * @param {Number} stepId + */ + function _showHintDialog(stepId) { + var hintElement = document.querySelector('.introjs-hint[data-step="' + stepId + '"]'); + var item = this._introItems[stepId]; + + // call the callback function (if any) + if (typeof (this._hintClickCallback) !== 'undefined') { + this._hintClickCallback.call(this, hintElement, item, stepId); + } + + // remove all open tooltips + var removedStep = _removeHintTooltip.call(this); + + // to toggle the tooltip + if (parseInt(removedStep, 10) === stepId) { + return; + } + + var tooltipLayer = document.createElement('div'); + var tooltipTextLayer = document.createElement('div'); + var arrowLayer = document.createElement('div'); + var referenceLayer = document.createElement('div'); + + tooltipLayer.className = 'introjs-tooltip'; + + tooltipLayer.onclick = function (e) { + //IE9 & Other Browsers + if (e.stopPropagation) { + e.stopPropagation(); + } + //IE8 and Lower + else { + e.cancelBubble = true; + } + }; + + tooltipTextLayer.className = 'introjs-tooltiptext'; + + var tooltipWrapper = document.createElement('p'); + tooltipWrapper.innerHTML = item.hint; + + var closeButton = document.createElement('a'); + closeButton.className = this._options.buttonClass; + closeButton.setAttribute('role', 'button'); + closeButton.innerHTML = this._options.hintButtonLabel; + closeButton.onclick = _hideHint.bind(this, stepId); + + tooltipTextLayer.appendChild(tooltipWrapper); + tooltipTextLayer.appendChild(closeButton); + + arrowLayer.className = 'introjs-arrow'; + tooltipLayer.appendChild(arrowLayer); + + tooltipLayer.appendChild(tooltipTextLayer); + + // set current step for _placeTooltip function + this._currentStep = hintElement.getAttribute('data-step'); + + // align reference layer position + referenceLayer.className = 'introjs-tooltipReferenceLayer introjs-hintReference'; + referenceLayer.setAttribute('data-step', hintElement.getAttribute('data-step')); + _setHelperLayerPosition.call(this, referenceLayer); + + referenceLayer.appendChild(tooltipLayer); + document.body.appendChild(referenceLayer); + + //set proper position + _placeTooltip.call(this, hintElement, tooltipLayer, arrowLayer, null, true); + } + + /** + * Get an element position on the page + * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966 + * + * @api private + * @method _getOffset + * @param {Object} element + * @returns Element's position info + */ + function _getOffset(element) { + var body = document.body; + var docEl = document.documentElement; + var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; + var x = element.getBoundingClientRect(); + return { + top: x.top + scrollTop, + width: x.width, + height: x.height, + left: x.left + scrollLeft + }; + } + + /** + * Find the nearest scrollable parent + * copied from https://stackoverflow.com/questions/35939886/find-first-scrollable-parent + * + * @param Element element + * @return Element + */ + function _getScrollParent(element) { + var style = window.getComputedStyle(element); + var excludeStaticParent = (style.position === "absolute"); + var overflowRegex = /(auto|scroll)/; + + if (style.position === "fixed") return document.body; + + for (var parent = element; (parent = parent.parentElement);) { + style = window.getComputedStyle(parent); + if (excludeStaticParent && style.position === "static") { + continue; + } + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; + } + + /** + * scroll a scrollable element to a child element + * + * @param Element parent + * @param Element element + * @return Null + */ + function _scrollParentToElement (parent, element) { + parent.scrollTop = element.offsetTop - parent.offsetTop; + } + + /** + * Gets the current progress percentage + * + * @api private + * @method _getProgress + * @returns current progress percentage + */ + function _getProgress() { + // Steps are 0 indexed + var currentStep = parseInt((this._currentStep + 1), 10); + return ((currentStep / this._introItems.length) * 100); + } + + /** + * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 + * via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically + * + * @param obj1 + * @param obj2 + * @returns obj3 a new object based on obj1 and obj2 + */ + function _mergeOptions(obj1,obj2) { + var obj3 = {}, + attrname; + for (attrname in obj1) { obj3[attrname] = obj1[attrname]; } + for (attrname in obj2) { obj3[attrname] = obj2[attrname]; } + return obj3; + } + + var introJs = function (targetElm) { + var instance; + + if (typeof (targetElm) === 'object') { + //Ok, create a new instance + instance = new IntroJs(targetElm); + + } else if (typeof (targetElm) === 'string') { + //select the target element with query selector + var targetElement = document.querySelector(targetElm); + + if (targetElement) { + instance = new IntroJs(targetElement); + } else { + throw new Error('There is no element with given selector.'); + } + } else { + instance = new IntroJs(document.body); + } + // add instance to list of _instances + // passing group to _stamp to increment + // from 0 onward somewhat reliably + introJs.instances[ _stamp(instance, 'introjs-instance') ] = instance; + + return instance; + }; + + /** + * Current IntroJs version + * + * @property version + * @type String + */ + introJs.version = VERSION; + + /** + * key-val object helper for introJs instances + * + * @property instances + * @type Object + */ + introJs.instances = {}; + + //Prototype + introJs.fn = IntroJs.prototype = { + clone: function () { + return new IntroJs(this); + }, + setOption: function(option, value) { + this._options[option] = value; + return this; + }, + setOptions: function(options) { + this._options = _mergeOptions(this._options, options); + return this; + }, + start: function (group) { + _introForElement.call(this, this._targetElement, group); + return this; + }, + goToStep: function(step) { + _goToStep.call(this, step); + return this; + }, + addStep: function(options) { + if (!this._options.steps) { + this._options.steps = []; + } + + this._options.steps.push(options); + + return this; + }, + addSteps: function(steps) { + if (!steps.length) return; + + for(var index = 0; index < steps.length; index++) { + this.addStep(steps[index]); + } + + return this; + }, + goToStepNumber: function(step) { + _goToStepNumber.call(this, step); + + return this; + }, + nextStep: function() { + _nextStep.call(this); + return this; + }, + previousStep: function() { + _previousStep.call(this); + return this; + }, + exit: function(force) { + _exitIntro.call(this, this._targetElement, force); + return this; + }, + refresh: function() { + _refresh.call(this); + return this; + }, + onbeforechange: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introBeforeChangeCallback = providedCallback; + } else { + throw new Error('Provided callback for onbeforechange was not a function'); + } + return this; + }, + onchange: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introChangeCallback = providedCallback; + } else { + throw new Error('Provided callback for onchange was not a function.'); + } + return this; + }, + onafterchange: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introAfterChangeCallback = providedCallback; + } else { + throw new Error('Provided callback for onafterchange was not a function'); + } + return this; + }, + oncomplete: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introCompleteCallback = providedCallback; + } else { + throw new Error('Provided callback for oncomplete was not a function.'); + } + return this; + }, + onhintsadded: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._hintsAddedCallback = providedCallback; + } else { + throw new Error('Provided callback for onhintsadded was not a function.'); + } + return this; + }, + onhintclick: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._hintClickCallback = providedCallback; + } else { + throw new Error('Provided callback for onhintclick was not a function.'); + } + return this; + }, + onhintclose: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._hintCloseCallback = providedCallback; + } else { + throw new Error('Provided callback for onhintclose was not a function.'); + } + return this; + }, + onexit: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introExitCallback = providedCallback; + } else { + throw new Error('Provided callback for onexit was not a function.'); + } + return this; + }, + onskip: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introSkipCallback = providedCallback; + } else { + throw new Error('Provided callback for onskip was not a function.'); + } + return this; + }, + onbeforeexit: function(providedCallback) { + if (typeof (providedCallback) === 'function') { + this._introBeforeExitCallback = providedCallback; + } else { + throw new Error('Provided callback for onbeforeexit was not a function.'); + } + return this; + }, + addHints: function() { + _populateHints.call(this, this._targetElement); + return this; + }, + hideHint: function (stepId) { + _hideHint.call(this, stepId); + return this; + }, + hideHints: function () { + _hideHints.call(this); + return this; + }, + showHint: function (stepId) { + _showHint.call(this, stepId); + return this; + }, + showHints: function () { + _showHints.call(this); + return this; + }, + removeHints: function () { + _removeHints.call(this); + return this; + }, + removeHint: function (stepId) { + _removeHint.call(this, stepId); + return this; + }, + showHintDialog: function (stepId) { + _showHintDialog.call(this, stepId); + return this; + } + }; + + return introJs; +}); \ No newline at end of file diff --git a/www/introbutton.js b/www/introbutton.js new file mode 100644 index 0000000..78314ff --- /dev/null +++ b/www/introbutton.js @@ -0,0 +1,30 @@ +document.addEventListener('DOMContentLoaded', function() { + var selector = document.getElementById("btnAddNetwork"); + selector.setAttribute("data-step", "1"); + selector.setAttribute("data-intro", "The network file is an obligatory, 2-column (or 3-column for weighted), tab-delimited file, containing all network connections of an undirected network. This file must contain headers, namely: 'Source' and 'Target'. Press this button to upload your network file"); + selector.setAttribute("data-position", "bottom"); + + selector = document.getElementById("uiLoadGraphOptionsOutput"); + selector.setAttribute("data-step", "2"); + selector.setAttribute("data-intro", "If the network is weighted (3 columns), check this box to allow weighted visualization."); + selector.setAttribute("data-position", "bottom"); + + selector = document.getElementById("btnAddNetwork2"); + selector.setAttribute("data-step", "3"); + selector.setAttribute("data-intro", "The annotation file is an obligatory, 2-column, tab-delimited file which contains information about the defined groups. The first column contains the group names whereas the second column contains the node names in a group separated by a comma (,) and without spaces. No headers are allowed. Press this button to upload your annotation file"); + selector.setAttribute("data-position", "bottom"); + + selector = document.getElementById("btnAddExpression"); + selector.setAttribute("data-step", "4"); + selector.setAttribute("data-intro", "The expression file is an optional, 2-column, tab-delimited file to allow node coloring (e.g. gene expressions). The first column contains the node names and the second column the node colors (color names or hex codes (e.g. Green or #00ff00, Red or #ff0000, Yellow or #ffff00)). Press this button to upload your node - coloring file"); + selector.setAttribute("data-position", "bottom"); + + selector = document.getElementById("introButton"); + selector.setAttribute("data-step", "5"); + selector.setAttribute("data-intro", "For more details, please check Help Pages tab."); + selector.setAttribute("data-position", "bottom"); + + document.getElementById("introButton").onclick = function(){ + introJs().start(); + }; +}, false); diff --git a/www/norma.css b/www/norma.css new file mode 100644 index 0000000..e0ba15e --- /dev/null +++ b/www/norma.css @@ -0,0 +1,91 @@ +body{ + text-align: justify; +} + +hr {border-top: 1px solid #000000;} + +td { + line-height: 1.2 !important; +} + +.well{ + margin-bottom: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.box-panel { + border-radius: 0 0 5px 5px; + border-width: 1px; + border-color: #d7d7d7; + border-style: none solid solid solid; +} + +.box-panel-padding { + padding: 20px 20px 20px 20px; +} + +.tabBox-panel { + padding: 20px 20px 20px 20px; + border-width: 1px; + border-color: #d7d7d7; + border-radius: 0px 0px 8px 8px; + border-style: none solid solid solid; + background: #ffffff; +} + +.centerBlock { + float: none; + margin: 0 auto; +} + +.dataTables_wrapper td { + line-height: 80%; +} + +.strategies{ + background-color: #fff7f1; + border: 1px solid #ff9300; + border-radius: 25px; + padding: 5px; + padding-bottom: 0; + font-size: 1.2em; + display: block; +} + +.strategies > div{ + display: inline-block; + width: 45% !important; + margin: 20px; + margin-bottom: 5px; +} + +.strategies > div:nth-child(5){ + position: relative; + top: -87px; +} + +.irs-single{ + background-color: #2C8160 !important; +} + +.irs-bar{ + background: #2C8160 !important; +} + +.layout_strat_img{ + margin-left: auto; + margin-right: auto; + display: block; +} + +#introButton{ + width: 100%; + margin-top: 15px; +} + +@media only screen and (max-width: 991px) { + .strategies > div:nth-child(5){ + top: 0; + } +}