From a5e54c600f588b7ae12a84388011a46eb7dbca33 Mon Sep 17 00:00:00 2001 From: mhkc Date: Tue, 6 Feb 2024 14:21:56 +0100 Subject: [PATCH 001/138] Add results_dev to dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d7b4c86c..733de435 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,6 +62,6 @@ COPY --from=node-builder /usr/src/app/build/css/home.min.css /usr/src/app/build/ COPY --from=node-builder /usr/src/app/build/*/gens.min.* gens/blueprints/gens/static/ # make mountpoints and change ownership of app -RUN mkdir -p /access /fs1/results && chown -R app:app /home/app/app /access /fs1 +RUN mkdir -p /access /fs1/results /fs1/results_dev && chown -R app:app /home/app/app /access /fs1 /fs1/results_dev # Change the user to app USER app From 6b56291d717f730a5d496f6ec285de647bf06eed Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 5 Apr 2024 11:38:53 +0200 Subject: [PATCH 002/138] Removed caching of display_case route --- gens/blueprints/gens/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index 2776e45b..d558329f 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -23,7 +23,6 @@ @gens_bp.route("/", methods=["GET"]) -@cache.cached(timeout=60) def display_case(sample_name): """ Renders the Gens template From 2893b376efd2bdc9089b0b3bce1466d64f647d63 Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 5 Apr 2024 11:39:16 +0200 Subject: [PATCH 003/138] Changed caching type from simple to FileSystem --- gens/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/cache.py b/gens/cache.py index 6efde991..059d7ddb 100644 --- a/gens/cache.py +++ b/gens/cache.py @@ -1,4 +1,4 @@ """Initiate cachig for app.""" from flask_caching import Cache -cache = Cache(config={"CACHE_TYPE": "simple"}) +cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": "/tmp"}) From 219b1f0380d5ff4e316d7db6ad5405893321ef19 Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 5 Apr 2024 11:41:25 +0200 Subject: [PATCH 004/138] Updated CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3884e2..28acb8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [x.x.x] ### Added ### Changed + - Changed cached method from simple to file system as it would be thread safe + ### Fixed + - Fixed cache issue that could result in chromosome information not being updated ## [2.1.1] ### Added From b22cc7a698cd876c040a7a7928a27099ff957b85 Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 5 Apr 2024 14:09:04 +0200 Subject: [PATCH 005/138] Fix error when searching for a gene --- gens/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/api.py b/gens/api.py index 5fc77879..b78019c6 100644 --- a/gens/api.py +++ b/gens/api.py @@ -193,7 +193,7 @@ def search_annotation(query: str, genome_build, annotation_type): response_code = 404 else: start_elem = elements[0] - end_elem = max(elements[1:], key=lambda elem: elem.get("end")) + end_elem = max(elements[0:], key=lambda elem: elem.get("end")) data = { "chromosome": start_elem.get("chrom"), "start_pos": start_elem.get("start"), From 20503fe2a37073fb4e3f188162ce7ab524a680ab Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 5 Apr 2024 14:09:59 +0200 Subject: [PATCH 006/138] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3884e2..62c47500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Added ### Changed ### Fixed +- Fixed max arg error when searching for some genes ## [2.1.1] ### Added From 68fc3c368c24106383052e5de786ab0d265a8e1b Mon Sep 17 00:00:00 2001 From: mhkc Date: Mon, 8 Apr 2024 09:13:45 +0200 Subject: [PATCH 007/138] use temp dir for flask caches --- gens/cache.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gens/cache.py b/gens/cache.py index 059d7ddb..dcf425fe 100644 --- a/gens/cache.py +++ b/gens/cache.py @@ -1,4 +1,6 @@ """Initiate cachig for app.""" from flask_caching import Cache +import tempfile -cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": "/tmp"}) +tmp_dir = tempfile.TemporaryDirectory(prefix="gens_cache_") +cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": tmp_dir.name}) From 2627cb076a25c467d3e6b7bfafba933c3363ee6d Mon Sep 17 00:00:00 2001 From: mhkc Date: Mon, 8 Apr 2024 13:33:51 +0200 Subject: [PATCH 008/138] Updated versions --- gens/__version__.py | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gens/__version__.py b/gens/__version__.py index 5b0431ec..b7775796 100644 --- a/gens/__version__.py +++ b/gens/__version__.py @@ -1 +1 @@ -VERSION = "2.1.1" +VERSION = "2.1.2" diff --git a/package.json b/package.json index 1a12bb7c..b979b489 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "tippy.js": "^6.3.2" }, "name": "gens", - "version": "2.1.0", + "version": "2.1.2", "description": "Interactive tool to visualize genomic copy number profiles from WGS data", "main": "gens.js", "scripts": { From 5b0b19870e4d4dcfdbb8d1778404a873d8dc3ed3 Mon Sep 17 00:00:00 2001 From: mhkc Date: Mon, 8 Apr 2024 13:34:47 +0200 Subject: [PATCH 009/138] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 296374d9..5f15e355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/) About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [x.x.x] + +### Added +### Changed +### Fixed + +## [2.1.2] ### Added ### Changed - Changed cached method from simple to file system as it would be thread safe From 765b6466900a654a106cb4dec12d083dd5e5de89 Mon Sep 17 00:00:00 2001 From: mhkc Date: Fri, 10 May 2024 10:37:38 +0200 Subject: [PATCH 010/138] Fixed bug that prevented updating annotation tracks --- CHANGELOG.md | 1 + gens/db/annotation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f15e355..3f6a246f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Added ### Changed ### Fixed + - Fixed bug that prevented updating annotation tracks ## [2.1.2] ### Added diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 70f41315..31f6671c 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -22,7 +22,7 @@ def register_data_update(track_type, name=None): db = app.config["GENS_DB"][UPDATES] LOG.debug(f"Creating timestamp for {track_type}") track = {"track": track_type, "name": name} - db.remove(track) # remove old track + db.delete_many(track) # remove old track db.insert_one({**track, "timestamp": datetime.datetime.now()}) From cca9caa449527dd1686f41746b594135315d215f Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 22 Sep 2022 10:35:26 +0200 Subject: [PATCH 011/138] Fixes parse_bed to use csv.DictReader correctly --- gens/load/annotations.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/gens/load/annotations.py b/gens/load/annotations.py index 0c54b53b..15309905 100644 --- a/gens/load/annotations.py +++ b/gens/load/annotations.py @@ -21,25 +21,8 @@ class ParserError(Exception): def parse_bed(file, genome_build): """Parse bed file.""" - HEADER = ( - "sequence", - "start", - "end", - "name", - "score", - "strand", - "null", - "null", - "color", - ) with open(file) as bed: - bed_reader = csv.DictReader(bed, delimiter="\t") - # Skip bad header info - next(bed_reader) - - # Load in annotations - for line in bed_reader: - yield dict(zip(HEADER, line)) + return csv.DictReader(bed, delimiter="\t") def parse_aed(file): From bd257d04e8f76cdbbd2ddc266da71f6a22f9d56f Mon Sep 17 00:00:00 2001 From: Emil Bertilsson Date: Wed, 31 Aug 2022 10:09:54 +0200 Subject: [PATCH 012/138] Create stage_docker_push.yml --- .github/workflows/stage_docker_push.yml | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/stage_docker_push.yml diff --git a/.github/workflows/stage_docker_push.yml b/.github/workflows/stage_docker_push.yml new file mode 100644 index 00000000..7b11b235 --- /dev/null +++ b/.github/workflows/stage_docker_push.yml @@ -0,0 +1,37 @@ +name: Publish to Docker stage + +on: + pull_request: + branches: + - main + +jobs: + docker-stage-push: + name: Create staging docker image + runs-on: ubuntu-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v5 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + if: steps.branch-name.outputs.is_default == 'false' + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: "clinicalgenomics/gens-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-stage:latest" From e6ffcbac79f7ed257307e370c25802779a682b38 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 5 Sep 2022 15:26:22 +0200 Subject: [PATCH 013/138] Change main to master in docker workflow --- .github/workflows/stage_docker_push.yml | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/stage_docker_push.yml b/.github/workflows/stage_docker_push.yml index 7b11b235..2ed69f33 100644 --- a/.github/workflows/stage_docker_push.yml +++ b/.github/workflows/stage_docker_push.yml @@ -3,35 +3,35 @@ name: Publish to Docker stage on: pull_request: branches: - - main + - master jobs: docker-stage-push: name: Create staging docker image runs-on: ubuntu-latest steps: - - name: Check out git repository - uses: actions/checkout@v2 + - name: Check out git repository + uses: actions/checkout@v2 - - name: Get branch name - id: branch-name - uses: tj-actions/branch-names@v5 + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v5 - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 - - name: Build and push - if: steps.branch-name.outputs.is_default == 'false' - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./Dockerfile - push: true - tags: "clinicalgenomics/gens-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-stage:latest" + - name: Build and push + if: steps.branch-name.outputs.is_default == 'false' + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: "clinicalgenomics/gens-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-stage:latest" From 0e4c07fd2ecbc77d38e5bae1d19fc6d0b66aabc6 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 19 Sep 2022 12:05:17 +0200 Subject: [PATCH 014/138] db config tweak: host+port->URI & split gens/scout --- gens/config.py | 4 ++-- gens/db/db.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/gens/config.py b/gens/config.py index 824aa61d..d8f38b23 100644 --- a/gens/config.py +++ b/gens/config.py @@ -1,7 +1,7 @@ """Gens default configuration.""" # Database connection -MONGODB_HOST = "mongodb" -MONGODB_PORT = 27017 +MONGODB_GENS_URI = "mongodb" +MONGODB_SCOUT_URI = "mongodb" GENS_DBNAME = "gens" SCOUT_DBNAME = "scout" diff --git a/gens/db/db.py b/gens/db/db.py index b81ccfec..ec338e33 100644 --- a/gens/db/db.py +++ b/gens/db/db.py @@ -31,9 +31,8 @@ def init_database_connection() -> None: ) variables[var_name] = os.environ.get(var_name, app.config.get(var_name)) # connect to database - client = MongoClient( - host=variables["MONGODB_HOST"], port=int(variables["MONGODB_PORT"]) - ) + scout_client = MongoClient(variables["MONGODB_SCOUT_URI"]) + gens_client = MongoClient(variables["MONGODB_GENS_URI"]) # store db handlers in configuration - app.config["SCOUT_DB"] = client[variables["SCOUT_DBNAME"]] - app.config["GENS_DB"] = client[variables["GENS_DBNAME"]] + app.config["SCOUT_DB"] = scout_client[variables["SCOUT_DBNAME"]] + app.config["GENS_DB"] = gens_client[variables["GENS_DBNAME"]] From 5a281a6c10472b5330c130c7fbf00ba28180bbcf Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 26 Sep 2022 11:11:26 +0200 Subject: [PATCH 015/138] Fixes io operation on closed file error --- gens/load/annotations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gens/load/annotations.py b/gens/load/annotations.py index 15309905..a3f1e9e8 100644 --- a/gens/load/annotations.py +++ b/gens/load/annotations.py @@ -22,7 +22,11 @@ class ParserError(Exception): def parse_bed(file, genome_build): """Parse bed file.""" with open(file) as bed: - return csv.DictReader(bed, delimiter="\t") + bed_reader = csv.DictReader(bed, delimiter="\t") + + # Load in annotations + for line in bed_reader: + yield line def parse_aed(file): From d59d4af6a1984607abce18b41eb8e4ac8c4c3606 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 26 Sep 2022 10:32:40 +0200 Subject: [PATCH 016/138] Fixes the list of expected database config vars --- gens/db/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/db/db.py b/gens/db/db.py index ec338e33..438e2e85 100644 --- a/gens/db/db.py +++ b/gens/db/db.py @@ -24,7 +24,7 @@ def init_database_connection() -> None: # verify that database was properly configured LOG.info("Initialize db connection") variables = {} - for var_name in ["MONGODB_HOST", "MONGODB_PORT", "SCOUT_DBNAME", "GENS_DBNAME"]: + for var_name in ["MONGODB_SCOUT_URI", "MONGODB_GENS_URI", "SCOUT_DBNAME", "GENS_DBNAME"]: if not any([var_name in os.environ, var_name in app.config]): raise ConfigurationException( f"Variable {var_name} not defined in either config or env variable" From 443be127f2d85bd7cf643470ee83a84e2f63c759 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Fri, 15 Nov 2024 14:44:01 +0100 Subject: [PATCH 017/138] Gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 21bbffc9..65d90ead 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ tags coverage *.aed +dump/ +frontend/ +docker-compose.override.yml # python *.pyc From 79a4bc9808598d2c52d458f31c7445d6e2aeb8b1 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 26 Sep 2022 12:24:07 +0200 Subject: [PATCH 018/138] Resolve merge --- gens/db/annotation.py | 2 +- gens/load/annotations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 31f6671c..a3b28463 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -22,7 +22,7 @@ def register_data_update(track_type, name=None): db = app.config["GENS_DB"][UPDATES] LOG.debug(f"Creating timestamp for {track_type}") track = {"track": track_type, "name": name} - db.delete_many(track) # remove old track + db.delete_one(track) # remove old track db.insert_one({**track, "timestamp": datetime.datetime.now()}) diff --git a/gens/load/annotations.py b/gens/load/annotations.py index a3f1e9e8..6e3d3ab8 100644 --- a/gens/load/annotations.py +++ b/gens/load/annotations.py @@ -134,7 +134,7 @@ def update_height_order(db, name): while True: if int(annot["start"]) > height_tracker[current_height - 1]: # Add height to DB - db[ANNOTATIONS_COLLECTION].update_one( + db[ANNOTATIONS_COLLECTION].update_many( {"_id": annot["_id"], "source": annot["source"]}, {"$set": {"height_order": current_height}}, ) From 0b0cb383a4e74529a5ff380fe0eac7258e7d5548 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 30 Sep 2022 12:13:18 +0200 Subject: [PATCH 019/138] Adds parsing support for common .bed column names --- gens/load/annotations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gens/load/annotations.py b/gens/load/annotations.py index 6e3d3ab8..cecb7f6c 100644 --- a/gens/load/annotations.py +++ b/gens/load/annotations.py @@ -9,6 +9,13 @@ from gens.db import ANNOTATIONS_COLLECTION LOG = logging.getLogger(__name__) +FIELD_TRANSLATIONS = { + "chromosome": "sequence", + "position": "start", + "stop": "end", + "chromstart": "start", + "chromend": "end" +} CORE_FIELDS = ("sequence", "start", "end", "name", "strand", "color", "score") AED_ENTRY = re.compile(r"[.+:]?(\w+)\(\w+:(\w+)\)", re.I) @@ -52,6 +59,10 @@ def parse_annotation_entry(entry, genome_build, annotation_name): annotation = {} # parse entry and format the values for name, value in entry.items(): + name = name.strip("#") + name = name.lower() + if name in FIELD_TRANSLATIONS: + name = FIELD_TRANSLATIONS[name] if name in CORE_FIELDS: name = "chrom" if name == "sequence" else name # for compatibility try: From d8d241aa0aafd6849cea122946c3ea11615900ae Mon Sep 17 00:00:00 2001 From: Emil Bertilsson Date: Wed, 5 Oct 2022 09:39:53 +0200 Subject: [PATCH 020/138] Adds gunicorn default command to dockerfile --- Dockerfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Dockerfile b/Dockerfile index 733de435..7415030e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,3 +65,21 @@ COPY --from=node-builder /usr/src/app/build/*/gens.min.* gens/blueprints/gens/st RUN mkdir -p /access /fs1/results /fs1/results_dev && chown -R app:app /home/app/app /access /fs1 /fs1/results_dev # Change the user to app USER app + +ENV GUNICORN_WORKERS=1 +ENV GUNICORN_THREADS=1 +ENV GUNICORN_BIND="0.0.0.0:5000" +ENV GUNICORN_TIMEOUT=400 + +CMD gunicorn \ + --workers=$GUNICORN_WORKERS \ + --bind=$GUNICORN_BIND \ + --threads=$GUNICORN_THREADS \ + --timeout=$GUNICORN_TIMEOUT \ + --proxy-protocol \ + --forwarded-allow-ips="10.0.2.100,127.0.0.1" \ + --log-syslog \ + --access-logfile - \ + --error-logfile - \ + --log-level="debug" \ + gens.server.auto:app From 73ce8a5fa9ebc084df670a7a14999e6301fb08cc Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 6 Oct 2022 15:55:08 +0200 Subject: [PATCH 021/138] Adds docker build and push for master branch --- .github/workflows/stage_docker_push.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stage_docker_push.yml b/.github/workflows/stage_docker_push.yml index 2ed69f33..f6e39f10 100644 --- a/.github/workflows/stage_docker_push.yml +++ b/.github/workflows/stage_docker_push.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - master + push: + branches: + - master jobs: docker-stage-push: @@ -28,7 +31,6 @@ jobs: uses: docker/setup-buildx-action@v1 - name: Build and push - if: steps.branch-name.outputs.is_default == 'false' uses: docker/build-push-action@v2 with: context: ./ From 9c9aed1a4147ff83cd883e9c3367257699d1af5b Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 6 Oct 2022 17:21:52 +0200 Subject: [PATCH 022/138] Adds --chdir to gunicorn command --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 7415030e..df3868d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,7 @@ CMD gunicorn \ --bind=$GUNICORN_BIND \ --threads=$GUNICORN_THREADS \ --timeout=$GUNICORN_TIMEOUT \ + --chdir /home/app/app/gens/ \ --proxy-protocol \ --forwarded-allow-ips="10.0.2.100,127.0.0.1" \ --log-syslog \ From f58c68119dce2013b73d18493f0b266c99135fc2 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 6 Oct 2022 17:32:59 +0200 Subject: [PATCH 023/138] Changes to accurately reflect Gens' WSGI setup --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index df3868d4..3a98dc3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,11 +76,11 @@ CMD gunicorn \ --bind=$GUNICORN_BIND \ --threads=$GUNICORN_THREADS \ --timeout=$GUNICORN_TIMEOUT \ - --chdir /home/app/app/gens/ \ + --chdir /home/app/app/ \ --proxy-protocol \ --forwarded-allow-ips="10.0.2.100,127.0.0.1" \ --log-syslog \ --access-logfile - \ --error-logfile - \ --log-level="debug" \ - gens.server.auto:app + gens.wsgi:app From 353d8bbbde3aab688a2a84446402220565b30746 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 23 Nov 2022 14:32:52 +0100 Subject: [PATCH 024/138] Adds case name as an additional database field --- assets/js/gens.js | 4 ++-- assets/js/track/variant.js | 7 +++++-- gens/api.py | 3 ++- gens/blueprints/gens/templates/gens.html | 1 + gens/blueprints/gens/views.py | 1 + gens/commands/load.py | 9 ++++++++- gens/commands/view.py | 2 ++ gens/db/annotation.py | 8 +++++--- gens/db/models.py | 1 + gens/db/samples.py | 5 ++++- gens/openapi/openapi.yaml | 6 ++++++ 11 files changed, 37 insertions(+), 10 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index 33938711..c126fade 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -8,7 +8,7 @@ export { panTracks, zoomIn, zoomOut, parseRegionDesignation, queryRegionOrGene } from './navigation.js' -export function initCanvases({ sampleName, genomeBuild, hgFileDir, uiColors, selectedVariant, annotationFile }) { +export function initCanvases({ sampleName, caseName, genomeBuild, hgFileDir, uiColors, selectedVariant, annotationFile }) { // initialize and return the different canvases // WEBGL values const near = 0.1 @@ -19,7 +19,7 @@ export function initCanvases({ sampleName, genomeBuild, hgFileDir, uiColors, sel // Initiate interactive canvas const ic = new InteractiveCanvas(inputField, lineMargin, near, far, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.variants, selectedVariant) + const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseName, genomeBuild, uiColors.variants, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 81ebbff3..d06c62c8 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -10,7 +10,7 @@ import { createPopper } from '@popperjs/core' const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication' } export class VariantTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, genomeBuild, colorSchema, highlightedVariantId) { + constructor (x, width, near, far, caseName, genomeBuild, colorSchema, highlightedVariantId) { // Dimensions of track canvas const visibleHeight = 100 // Visible height for expanded canvas, overflows for scroll const minHeight = 35 // Minimized height @@ -32,7 +32,10 @@ export class VariantTrack extends BaseAnnotationTrack { // GENS api parameters this.apiEntrypoint = 'get-variant-data' - this.additionalQueryParams = { variant_category: 'sv' } + this.additionalQueryParams = { + variant_category: 'sv', + case_name: caseName + } // Initialize highlighted variant this.highlightedVariantId = highlightedVariantId diff --git a/gens/api.py b/gens/api.py index b78019c6..63dcbff0 100644 --- a/gens/api.py +++ b/gens/api.py @@ -205,7 +205,7 @@ def search_annotation(query: str, genome_build, annotation_type): return jsonify({**data, "status": response_code}) -def get_variant_data(sample_id, variant_category, **optional_kwargs): +def get_variant_data(case_name, sample_id, variant_category, **optional_kwargs): """Search Scout database for variants associated with a case and return info in JSON format.""" default_height_order = 0 base_return = {"status": "ok"} @@ -232,6 +232,7 @@ def get_variant_data(sample_id, variant_category, **optional_kwargs): try: variants = list( query_variants( + case_name, sample_id, cattr.structure(variant_category, VariantCategory), **region_params, diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index 1071a6bc..269d8236 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -158,6 +158,7 @@ // and pass flask values to tracks const {ic, oc, ac, tc, vc} = gens.initCanvases({ sampleName: '{{ sample_name }}', + caseName: '{{ case_name }}', genomeBuild: {{ genome_build }}, uiColors: {{ ui_colors | tojson | safe }}, selectedVariant: '{{ selected_variant }}', diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index d558329f..dfd44f84 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -73,6 +73,7 @@ def display_case(sample_name): start=start_pos, end=end_pos, sample_name=sample_name, + case_name=sample.case_name, genome_build=genome_build, print_page=print_page, annotation=annotation, diff --git a/gens/commands/load.py b/gens/commands/load.py index b58d061c..6ebcfb84 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -46,6 +46,12 @@ def load(): type=click.Path(exists=True), help="File or directory of annotation files to load into the database", ) +@click.option( + "-n", + "--case-name", + required=True, + help="Display name of case", +) @click.option( "-j", "--overview-json", @@ -53,7 +59,7 @@ def load(): help="Json file that contains preprocessed overview coverage", ) @with_appcontext -def sample(sample_id, genome_build, baf, coverage, overview_json): +def sample(sample_id, genome_build, baf, coverage, case_name, overview_json): """Load a sample into Gens database.""" db = app.config["GENS_DB"] # if collection is not indexed, crate index @@ -63,6 +69,7 @@ def sample(sample_id, genome_build, baf, coverage, overview_json): store_sample( db, sample_id=sample_id, + case_name=case_name, genome_build=genome_build, baf=baf, coverage=coverage, diff --git a/gens/commands/view.py b/gens/commands/view.py index 4e06653c..dec75235 100644 --- a/gens/commands/view.py +++ b/gens/commands/view.py @@ -35,6 +35,7 @@ def samples(summary): else: # show all samples columns = ( "Sample Id", + "Case name", "Genome build", "Created at", "baf file", @@ -44,6 +45,7 @@ def samples(summary): sample_tbl = ( ( s.sample_id, + s.case_name, str(s.genome_build), s.created_at.isoformat(), s.baf_file, diff --git a/gens/db/annotation.py b/gens/db/annotation.py index a3b28463..4328a241 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -49,11 +49,12 @@ def get_timestamps(track_type="all"): return results -def query_variants(case_name: str, variant_category: VariantCategory, **kwargs): +def query_variants(case_name: str, sample_name: str, variant_category: VariantCategory, **kwargs): """Search the scout database for variants associated with a case. - case_id :: name for a case (not database uid) - varaint_category :: categories + case_name :: display name for a case + sample_name :: display name for a sample + variant_category :: categories Kwargs are optional search parameters that are passed to db.find(). """ @@ -66,6 +67,7 @@ def query_variants(case_name: str, variant_category: VariantCategory, **kwargs): query = { "case_id": response["_id"], "category": variant_category.value, + "samples.display_name": sample_name, } # add chromosome if "chromosome" in kwargs: diff --git a/gens/db/models.py b/gens/db/models.py index 9570c4a7..99dfcbae 100644 --- a/gens/db/models.py +++ b/gens/db/models.py @@ -19,6 +19,7 @@ class VariantCategory(Enum): @attr.s(frozen=True) class SampleObj: sample_id: str = attr.ib() + case_name: str = attr.ib() baf_file: str = attr.ib() coverage_file: str = attr.ib() genome_build: int = attr.ib( diff --git a/gens/db/samples.py b/gens/db/samples.py index 8e188d3c..c449f0a6 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -20,12 +20,13 @@ def __init__(self, message, sample_id): self.sample_id = sample_id -def store_sample(db, sample_id, genome_build, baf, coverage, overview): +def store_sample(db, sample_id, case_name, genome_build, baf, coverage, overview): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') db[COLLECTION].insert_one( { "sample_id": sample_id, + "case_name": case_name, "baf_file": baf, "coverage_file": coverage, "overview_file": overview, @@ -44,6 +45,7 @@ def get_samples(db, start=0, n_samples=None): results = ( SampleObj( sample_id=r["sample_id"], + case_name=r["case_name"], genome_build=r["genome_build"], baf_file=r["baf_file"], coverage_file=r["coverage_file"], @@ -68,6 +70,7 @@ def query_sample(db, sample_id, genome_build): ) return SampleObj( sample_id=result["sample_id"], + case_name=result["case_name"], genome_build=result["genome_build"], baf_file=result["baf_file"], coverage_file=result["coverage_file"], diff --git a/gens/openapi/openapi.yaml b/gens/openapi/openapi.yaml index 64a036a9..db210d0e 100644 --- a/gens/openapi/openapi.yaml +++ b/gens/openapi/openapi.yaml @@ -216,6 +216,12 @@ paths: description: Get annotation data operationId: gens.api.get_variant_data parameters: + - name: case_name + in: query + description: Case display name + required: true + schema: + type: string - name: sample_id in: query description: Unique sample identifier From e1a4c282cf863012a0df47df3654f8effe9cc9bb Mon Sep 17 00:00:00 2001 From: Emil Date: Tue, 6 Dec 2022 12:52:27 +0100 Subject: [PATCH 025/138] Adds case name to most, if not all, places needed --- gens/api.py | 6 ++++-- gens/blueprints/gens/views.py | 6 ++++-- gens/blueprints/home/templates/home.html | 3 ++- gens/db/index.py | 6 ++++++ gens/db/samples.py | 8 ++++++-- gens/openapi/openapi.yaml | 13 +++++++++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/gens/api.py b/gens/api.py index 63dcbff0..530a57b1 100644 --- a/gens/api.py +++ b/gens/api.py @@ -54,6 +54,7 @@ class ChromCoverageRequest: """Request for getting coverage from multiple chromosome and regions.""" sample_id: str + case_name: str genome_build: int = attr.ib() plot_height: float top_bottom_padding: float @@ -263,7 +264,7 @@ def get_multiple_coverages(): # read sample information db = current_app.config["GENS_DB"] - sample_obj = query_sample(db, data.sample_id, data.genome_build) + sample_obj = query_sample(db, data.sample_id, data.case_name, data.genome_build) # Try to find and load an overview json data file json_data, cov_file, baf_file = None, None, None if sample_obj.overview_file and os.path.isfile(sample_obj.overview_file): @@ -326,6 +327,7 @@ def get_multiple_coverages(): def get_coverage( sample_id, + case_name, region, x_pos, y_pos, @@ -364,7 +366,7 @@ def get_coverage( reduce_data, ) db = current_app.config["GENS_DB"] - sample_obj = query_sample(db, sample_id, genome_build) + sample_obj = query_sample(db, sample_id, case_name, genome_build) cov_file, baf_file = get_tabix_files(sample_obj.coverage_file, sample_obj.baf_file) # Parse region try: diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index dfd44f84..86ebb408 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -28,6 +28,8 @@ def display_case(sample_name): Renders the Gens template Expects sample_id as input to be able to load the sample data """ + case_name = request.args.get("case_name", None) + # get genome build and region region = request.args.get("region", None) print_page = request.args.get("print_page", "false") @@ -45,7 +47,7 @@ def display_case(sample_name): # verify that sample has been loaded db = current_app.config["GENS_DB"] - sample = query_sample(db, sample_name, genome_build) + sample = query_sample(db, sample_name, case_name, genome_build) # Check that BAF and Log2 file exists try: @@ -73,7 +75,7 @@ def display_case(sample_name): start=start_pos, end=end_pos, sample_name=sample_name, - case_name=sample.case_name, + case_name=case_name, genome_build=genome_build, print_page=print_page, annotation=annotation, diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 77ac365e..c99241dd 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -44,8 +44,9 @@

Samples

{% for sample in samples_obj %} {% set genome_build=sample["genome_build"]%} {% set sample_id=sample["sample_id"]%} + {% set case_name=sample["case_name"]%} - {{ sample_id }} + {{ sample_id }} {{ genome_build }} {% if sample["has_overview_file"] %} diff --git a/gens/db/index.py b/gens/db/index.py index fa65cb78..ac008ced 100644 --- a/gens/db/index.py +++ b/gens/db/index.py @@ -68,6 +68,12 @@ background=True, unique=True, ), + IndexModel( + [("sample_id", ASCENDING), ("case_name", ASCENDING), ("genome_build", ASCENDING)], + name="sample__sample_id_case_name_genome_build", + background=True, + unique=True, + ), IndexModel( [("created_at", ASCENDING)], name="sample__creation_date", diff --git a/gens/db/samples.py b/gens/db/samples.py index c449f0a6..4936aff8 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -60,9 +60,13 @@ def get_samples(db, start=0, n_samples=None): return results, db[COLLECTION].count_documents({}) -def query_sample(db, sample_id, genome_build): +def query_sample(db, sample_id, case_name, genome_build): """Get a sample with id.""" - result = db[COLLECTION].find_one({"sample_id": sample_id}) + result = None + if case_name is None: + result = db[COLLECTION].find_one({"sample_id": sample_id}) + else: + result = db[COLLECTION].find_one({"sample_id": sample_id, "case_name": case_name}) if result is None: raise SampleNotFoundError( diff --git a/gens/openapi/openapi.yaml b/gens/openapi/openapi.yaml index db210d0e..579197df 100644 --- a/gens/openapi/openapi.yaml +++ b/gens/openapi/openapi.yaml @@ -22,7 +22,10 @@ paths: type: object properties: sample_id: - description: Sample id + description: Sample/individual displayname + type: string + case_name: + description: Case display name type: string baf_y_start: description: Y coordinate from where to draw BAF track. @@ -94,7 +97,13 @@ paths: parameters: - name: sample_id in: query - description: Region selector + description: Sample/individual displayname + required: true + schema: + type: string + - name: case_name + in: query + description: Case display name required: true schema: type: string From 034d70ba04a8447884f7b8d3c3b0129fd65b02ec Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:13:49 +0100 Subject: [PATCH 026/138] Merge --- gens/db/annotation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 4328a241..2b5f4429 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -49,7 +49,7 @@ def get_timestamps(track_type="all"): return results -def query_variants(case_name: str, sample_name: str, variant_category: VariantCategory, **kwargs): +def query_variants(case_id: str, sample_name: str, variant_category: VariantCategory, **kwargs): """Search the scout database for variants associated with a case. case_name :: display name for a case @@ -58,14 +58,13 @@ def query_variants(case_name: str, sample_name: str, variant_category: VariantCa Kwargs are optional search parameters that are passed to db.find(). """ - # lookup case_id from the displayed name db = app.config["SCOUT_DB"] - response = db.case.find_one({"display_name": case_name}) + response = db.case.find_one({"case_id": case_id}) if response is None: - raise ValueError(f"No case with name: {case_name}") + raise ValueError(f"No case with name: {case_id}") # build query query = { - "case_id": response["_id"], + "case_id": case_id, "category": variant_category.value, "samples.display_name": sample_name, } From fe3234f557956d6ff56356dcb92e286842c37c3b Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 11:24:58 +0100 Subject: [PATCH 027/138] Replaces the last case name --- gens/commands/load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/commands/load.py b/gens/commands/load.py index 6ebcfb84..169b038c 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -48,7 +48,7 @@ def load(): ) @click.option( "-n", - "--case-name", + "--case-id", required=True, help="Display name of case", ) From 17505cc887e5113ae5dc6e2250907fded3d0acd9 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 24 Feb 2023 09:42:11 +0100 Subject: [PATCH 028/138] merge --- gens/db/samples.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index 4936aff8..5c9f43b8 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -5,6 +5,7 @@ import logging from pymongo import DESCENDING +from pymongo.errors import DuplicateKeyError from .models import SampleObj @@ -23,17 +24,21 @@ def __init__(self, message, sample_id): def store_sample(db, sample_id, case_name, genome_build, baf, coverage, overview): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') - db[COLLECTION].insert_one( - { - "sample_id": sample_id, - "case_name": case_name, - "baf_file": baf, - "coverage_file": coverage, - "overview_file": overview, - "genome_build": genome_build, - "created_at": datetime.datetime.now(), - } - ) + + try: + db[COLLECTION].insert_one( + { + "sample_id": sample_id, + "case_id": case_id, + "baf_file": baf, + "coverage_file": coverage, + "overview_file": overview, + "genome_build": genome_build, + "created_at": datetime.datetime.now(), + } + ) + except DuplicateKeyError: + LOG.warning(exc_info=True) def get_samples(db, start=0, n_samples=None): From cfb1e115ebed22eedc1e60c58e841126d926bc00 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 24 Feb 2023 10:04:46 +0100 Subject: [PATCH 029/138] Adds msg to LOG.warning --- gens/db/samples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index 5c9f43b8..d858d979 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -38,7 +38,7 @@ def store_sample(db, sample_id, case_name, genome_build, baf, coverage, overview } ) except DuplicateKeyError: - LOG.warning(exc_info=True) + LOG.warning(f'DuplicateKeyError while storing sample "{sample_id}" in database, skipping.', exc_info=True) def get_samples(db, start=0, n_samples=None): From 39085d105a3442360a37f4093faa147f75c86b57 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 27 Feb 2023 15:05:12 +0100 Subject: [PATCH 030/138] Adds github action for prod docker image --- .github/workflows/docker_push.yml | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/docker_push.yml diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml new file mode 100644 index 00000000..420cc817 --- /dev/null +++ b/.github/workflows/docker_push.yml @@ -0,0 +1,36 @@ +name: Publish to Docker + +on: + push: + branches: + - master + +jobs: + docker-stage-push: + name: Create docker image + runs-on: ubuntu-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v5 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: "clinicalgenomics/gens:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens:latest" From 6dc5e6cc536f4a3f94116064a8dbdb350988117b Mon Sep 17 00:00:00 2001 From: Emil Date: Tue, 4 Apr 2023 10:15:37 +0200 Subject: [PATCH 031/138] Merge --- gens/blueprints/gens/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index 86ebb408..7bd72593 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -28,7 +28,8 @@ def display_case(sample_name): Renders the Gens template Expects sample_id as input to be able to load the sample data """ - case_name = request.args.get("case_name", None) + case_id = request.args.get("case_id", None) + individual_id = request.args.get("individual_id", sample_name) # get genome build and region region = request.args.get("region", None) @@ -47,7 +48,7 @@ def display_case(sample_name): # verify that sample has been loaded db = current_app.config["GENS_DB"] - sample = query_sample(db, sample_name, case_name, genome_build) + sample = query_sample(db, individual_id, case_id, genome_build) # Check that BAF and Log2 file exists try: @@ -74,8 +75,8 @@ def display_case(sample_name): chrom=chrom, start=start_pos, end=end_pos, - sample_name=sample_name, - case_name=case_name, + sample_name=individual_id, + case_id=case_id, genome_build=genome_build, print_page=print_page, annotation=annotation, From 898a96b95f80316dc7622bb3860e093f46d79d00 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:12:24 +0100 Subject: [PATCH 032/138] Merge --- assets/js/gens.js | 4 +-- assets/js/track/variant.js | 4 +-- gens/api.py | 12 ++++---- gens/blueprints/gens/templates/gens.html | 2 +- gens/blueprints/gens/views.py | 5 ++-- gens/blueprints/home/templates/home.html | 4 +-- gens/commands/load.py | 4 +-- gens/commands/view.py | 2 +- gens/db/annotation.py | 6 ++-- gens/db/index.py | 4 +-- gens/db/models.py | 2 +- gens/db/samples.py | 38 +++++++++++------------- gens/openapi/openapi.yaml | 6 ++-- 13 files changed, 44 insertions(+), 49 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index c126fade..4a26758a 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -8,7 +8,7 @@ export { panTracks, zoomIn, zoomOut, parseRegionDesignation, queryRegionOrGene } from './navigation.js' -export function initCanvases({ sampleName, caseName, genomeBuild, hgFileDir, uiColors, selectedVariant, annotationFile }) { +export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiColors, selectedVariant, annotationFile }) { // initialize and return the different canvases // WEBGL values const near = 0.1 @@ -19,7 +19,7 @@ export function initCanvases({ sampleName, caseName, genomeBuild, hgFileDir, uiC // Initiate interactive canvas const ic = new InteractiveCanvas(inputField, lineMargin, near, far, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseName, genomeBuild, uiColors.variants, selectedVariant) + const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index d06c62c8..75ad0646 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -10,7 +10,7 @@ import { createPopper } from '@popperjs/core' const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication' } export class VariantTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, caseName, genomeBuild, colorSchema, highlightedVariantId) { + constructor (x, width, near, far, caseId, genomeBuild, colorSchema, highlightedVariantId) { // Dimensions of track canvas const visibleHeight = 100 // Visible height for expanded canvas, overflows for scroll const minHeight = 35 // Minimized height @@ -34,7 +34,7 @@ export class VariantTrack extends BaseAnnotationTrack { this.apiEntrypoint = 'get-variant-data' this.additionalQueryParams = { variant_category: 'sv', - case_name: caseName + case_id: caseId } // Initialize highlighted variant diff --git a/gens/api.py b/gens/api.py index 530a57b1..f0e18c67 100644 --- a/gens/api.py +++ b/gens/api.py @@ -54,7 +54,7 @@ class ChromCoverageRequest: """Request for getting coverage from multiple chromosome and regions.""" sample_id: str - case_name: str + case_id: str genome_build: int = attr.ib() plot_height: float top_bottom_padding: float @@ -206,7 +206,7 @@ def search_annotation(query: str, genome_build, annotation_type): return jsonify({**data, "status": response_code}) -def get_variant_data(case_name, sample_id, variant_category, **optional_kwargs): +def get_variant_data(case_id, sample_id, variant_category, **optional_kwargs): """Search Scout database for variants associated with a case and return info in JSON format.""" default_height_order = 0 base_return = {"status": "ok"} @@ -233,7 +233,7 @@ def get_variant_data(case_name, sample_id, variant_category, **optional_kwargs): try: variants = list( query_variants( - case_name, + case_id, sample_id, cattr.structure(variant_category, VariantCategory), **region_params, @@ -264,7 +264,7 @@ def get_multiple_coverages(): # read sample information db = current_app.config["GENS_DB"] - sample_obj = query_sample(db, data.sample_id, data.case_name, data.genome_build) + sample_obj = query_sample(db, data.sample_id, data.case_id, data.genome_build) # Try to find and load an overview json data file json_data, cov_file, baf_file = None, None, None if sample_obj.overview_file and os.path.isfile(sample_obj.overview_file): @@ -327,7 +327,7 @@ def get_multiple_coverages(): def get_coverage( sample_id, - case_name, + case_id, region, x_pos, y_pos, @@ -366,7 +366,7 @@ def get_coverage( reduce_data, ) db = current_app.config["GENS_DB"] - sample_obj = query_sample(db, sample_id, case_name, genome_build) + sample_obj = query_sample(db, sample_id, case_id, genome_build) cov_file, baf_file = get_tabix_files(sample_obj.coverage_file, sample_obj.baf_file) # Parse region try: diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index 269d8236..e7fc3c61 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -158,7 +158,7 @@ // and pass flask values to tracks const {ic, oc, ac, tc, vc} = gens.initCanvases({ sampleName: '{{ sample_name }}', - caseName: '{{ case_name }}', + caseId: '{{ case_id }}', genomeBuild: {{ genome_build }}, uiColors: {{ ui_colors | tojson | safe }}, selectedVariant: '{{ selected_variant }}', diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index 7bd72593..73cf9e96 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -29,7 +29,6 @@ def display_case(sample_name): Expects sample_id as input to be able to load the sample data """ case_id = request.args.get("case_id", None) - individual_id = request.args.get("individual_id", sample_name) # get genome build and region region = request.args.get("region", None) @@ -48,7 +47,7 @@ def display_case(sample_name): # verify that sample has been loaded db = current_app.config["GENS_DB"] - sample = query_sample(db, individual_id, case_id, genome_build) + sample = query_sample(db, sample_name, case_id, genome_build) # Check that BAF and Log2 file exists try: @@ -75,7 +74,7 @@ def display_case(sample_name): chrom=chrom, start=start_pos, end=end_pos, - sample_name=individual_id, + sample_name=sample_name, case_id=case_id, genome_build=genome_build, print_page=print_page, diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index c99241dd..7826ed9e 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -44,9 +44,9 @@

Samples

{% for sample in samples_obj %} {% set genome_build=sample["genome_build"]%} {% set sample_id=sample["sample_id"]%} - {% set case_name=sample["case_name"]%} + {% set case_id=sample["case_id"]%} - {{ sample_id }} + {{ sample_id }} {{ genome_build }} {% if sample["has_overview_file"] %} diff --git a/gens/commands/load.py b/gens/commands/load.py index 169b038c..07295f9c 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -59,7 +59,7 @@ def load(): help="Json file that contains preprocessed overview coverage", ) @with_appcontext -def sample(sample_id, genome_build, baf, coverage, case_name, overview_json): +def sample(sample_id, genome_build, baf, coverage, case_id, overview_json): """Load a sample into Gens database.""" db = app.config["GENS_DB"] # if collection is not indexed, crate index @@ -69,7 +69,7 @@ def sample(sample_id, genome_build, baf, coverage, case_name, overview_json): store_sample( db, sample_id=sample_id, - case_name=case_name, + case_id=case_id, genome_build=genome_build, baf=baf, coverage=coverage, diff --git a/gens/commands/view.py b/gens/commands/view.py index dec75235..fd7b5e52 100644 --- a/gens/commands/view.py +++ b/gens/commands/view.py @@ -45,7 +45,7 @@ def samples(summary): sample_tbl = ( ( s.sample_id, - s.case_name, + s.case_id, str(s.genome_build), s.created_at.isoformat(), s.baf_file, diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 2b5f4429..98d2643c 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -52,16 +52,16 @@ def get_timestamps(track_type="all"): def query_variants(case_id: str, sample_name: str, variant_category: VariantCategory, **kwargs): """Search the scout database for variants associated with a case. - case_name :: display name for a case + case_id :: display name for a case sample_name :: display name for a sample variant_category :: categories Kwargs are optional search parameters that are passed to db.find(). """ db = app.config["SCOUT_DB"] - response = db.case.find_one({"case_id": case_id}) + response = db.case.find_one({"_id": case_id}) if response is None: - raise ValueError(f"No case with name: {case_id}") + raise ValueError(f"No case with id: {case_id}") # build query query = { "case_id": case_id, diff --git a/gens/db/index.py b/gens/db/index.py index ac008ced..12743c2f 100644 --- a/gens/db/index.py +++ b/gens/db/index.py @@ -69,8 +69,8 @@ unique=True, ), IndexModel( - [("sample_id", ASCENDING), ("case_name", ASCENDING), ("genome_build", ASCENDING)], - name="sample__sample_id_case_name_genome_build", + [("sample_id", ASCENDING), ("case_id", ASCENDING), ("genome_build", ASCENDING)], + name="sample__sample_id_case_id_genome_build", background=True, unique=True, ), diff --git a/gens/db/models.py b/gens/db/models.py index 99dfcbae..1948a98e 100644 --- a/gens/db/models.py +++ b/gens/db/models.py @@ -19,7 +19,7 @@ class VariantCategory(Enum): @attr.s(frozen=True) class SampleObj: sample_id: str = attr.ib() - case_name: str = attr.ib() + case_id: str = attr.ib() baf_file: str = attr.ib() coverage_file: str = attr.ib() genome_build: int = attr.ib( diff --git a/gens/db/samples.py b/gens/db/samples.py index d858d979..661b26b7 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -21,24 +21,20 @@ def __init__(self, message, sample_id): self.sample_id = sample_id -def store_sample(db, sample_id, case_name, genome_build, baf, coverage, overview): +def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') - - try: - db[COLLECTION].insert_one( - { - "sample_id": sample_id, - "case_id": case_id, - "baf_file": baf, - "coverage_file": coverage, - "overview_file": overview, - "genome_build": genome_build, - "created_at": datetime.datetime.now(), - } - ) - except DuplicateKeyError: - LOG.warning(f'DuplicateKeyError while storing sample "{sample_id}" in database, skipping.', exc_info=True) + db[COLLECTION].insert_one( + { + "sample_id": sample_id, + "case_id": case_id, + "baf_file": baf, + "coverage_file": coverage, + "overview_file": overview, + "genome_build": genome_build, + "created_at": datetime.datetime.now(), + } + ) def get_samples(db, start=0, n_samples=None): @@ -50,7 +46,7 @@ def get_samples(db, start=0, n_samples=None): results = ( SampleObj( sample_id=r["sample_id"], - case_name=r["case_name"], + case_id=r["case_id"], genome_build=r["genome_build"], baf_file=r["baf_file"], coverage_file=r["coverage_file"], @@ -65,13 +61,13 @@ def get_samples(db, start=0, n_samples=None): return results, db[COLLECTION].count_documents({}) -def query_sample(db, sample_id, case_name, genome_build): +def query_sample(db, sample_id, case_id, genome_build): """Get a sample with id.""" result = None - if case_name is None: + if case_id is None: result = db[COLLECTION].find_one({"sample_id": sample_id}) else: - result = db[COLLECTION].find_one({"sample_id": sample_id, "case_name": case_name}) + result = db[COLLECTION].find_one({"sample_id": sample_id, "case_id": case_id}) if result is None: raise SampleNotFoundError( @@ -79,7 +75,7 @@ def query_sample(db, sample_id, case_name, genome_build): ) return SampleObj( sample_id=result["sample_id"], - case_name=result["case_name"], + case_id=result["case_id"], genome_build=result["genome_build"], baf_file=result["baf_file"], coverage_file=result["coverage_file"], diff --git a/gens/openapi/openapi.yaml b/gens/openapi/openapi.yaml index 579197df..9f19bdb2 100644 --- a/gens/openapi/openapi.yaml +++ b/gens/openapi/openapi.yaml @@ -24,7 +24,7 @@ paths: sample_id: description: Sample/individual displayname type: string - case_name: + case_id: description: Case display name type: string baf_y_start: @@ -101,7 +101,7 @@ paths: required: true schema: type: string - - name: case_name + - name: case_id in: query description: Case display name required: true @@ -225,7 +225,7 @@ paths: description: Get annotation data operationId: gens.api.get_variant_data parameters: - - name: case_name + - name: case_id in: query description: Case display name required: true From 014eec70eaabfa49c376341c72527141c9926035 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:13:49 +0100 Subject: [PATCH 033/138] Removes a now obsolete query --- gens/db/annotation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 98d2643c..5f07390a 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -59,9 +59,6 @@ def query_variants(case_id: str, sample_name: str, variant_category: VariantCate Kwargs are optional search parameters that are passed to db.find(). """ db = app.config["SCOUT_DB"] - response = db.case.find_one({"_id": case_id}) - if response is None: - raise ValueError(f"No case with id: {case_id}") # build query query = { "case_id": case_id, From 92c32bf617b4d6004ed1846a9abee41866f0e4c9 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:19:43 +0100 Subject: [PATCH 034/138] Fixes some descriptions --- gens/commands/load.py | 2 +- gens/commands/view.py | 2 +- gens/db/annotation.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gens/commands/load.py b/gens/commands/load.py index 07295f9c..0a97138e 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -50,7 +50,7 @@ def load(): "-n", "--case-id", required=True, - help="Display name of case", + help="Id of case", ) @click.option( "-j", diff --git a/gens/commands/view.py b/gens/commands/view.py index fd7b5e52..e5ea1adb 100644 --- a/gens/commands/view.py +++ b/gens/commands/view.py @@ -35,7 +35,7 @@ def samples(summary): else: # show all samples columns = ( "Sample Id", - "Case name", + "Case Id", "Genome build", "Created at", "baf file", diff --git a/gens/db/annotation.py b/gens/db/annotation.py index 5f07390a..e2abf821 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -52,7 +52,7 @@ def get_timestamps(track_type="all"): def query_variants(case_id: str, sample_name: str, variant_category: VariantCategory, **kwargs): """Search the scout database for variants associated with a case. - case_id :: display name for a case + case_id :: id for a case sample_name :: display name for a sample variant_category :: categories From 410be87baf0bbbf0d20fb6b2e31183b5d4f882ae Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:22:16 +0100 Subject: [PATCH 035/138] Fixes a typo --- assets/js/track/annotation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/track/annotation.js b/assets/js/track/annotation.js index 338fb366..237b2347 100644 --- a/assets/js/track/annotation.js +++ b/assets/js/track/annotation.js @@ -63,7 +63,7 @@ export class AnnotationTrack extends BaseAnnotationTrack { } // Fills the list with source files - annotSourceList (defaultAnntotation) { + annotSourceList (defaultAnnotation) { get('get-annotation-sources', { genome_build: this.genomeBuild }) .then(result => { if (result.sources.length > 0) { @@ -76,7 +76,7 @@ export class AnnotationTrack extends BaseAnnotationTrack { opt.innerHTML = fileName // Set mimisbrunnr as default file - if (fileName.match(defaultAnntotation)) { + if (fileName.match(defaultAnnotation)) { opt.setAttribute('selected', true) } this.sourceList.appendChild(opt) From 9d785c85b059ae28f6e8c3bba2b13a9e3275fd2b Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:23:30 +0100 Subject: [PATCH 036/138] Fixes a few descriptions --- gens/openapi/openapi.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gens/openapi/openapi.yaml b/gens/openapi/openapi.yaml index 9f19bdb2..b6e79074 100644 --- a/gens/openapi/openapi.yaml +++ b/gens/openapi/openapi.yaml @@ -25,7 +25,7 @@ paths: description: Sample/individual displayname type: string case_id: - description: Case display name + description: Case id type: string baf_y_start: description: Y coordinate from where to draw BAF track. @@ -103,7 +103,7 @@ paths: type: string - name: case_id in: query - description: Case display name + description: Case id required: true schema: type: string @@ -227,7 +227,7 @@ paths: parameters: - name: case_id in: query - description: Case display name + description: Case id required: true schema: type: string From d68081883c7509e25c94986fab2df5bfc18d8650 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 9 Dec 2022 10:48:52 +0100 Subject: [PATCH 037/138] Adds case id to where it's missing --- assets/js/gens.js | 4 ++-- assets/js/interactive.js | 5 +++-- assets/js/overview.js | 5 +++-- assets/js/track/base.js | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index 4a26758a..1dbe322d 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -17,13 +17,13 @@ export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiCol // Listener values const inputField = document.getElementById('region-field') // Initiate interactive canvas - const ic = new InteractiveCanvas(inputField, lineMargin, near, far, sampleName, genomeBuild, hgFileDir) + const ic = new InteractiveCanvas(inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas - const oc = new OverviewCanvas(ic.x, ic.plotWidth, lineMargin, near, far, sampleName, genomeBuild, hgFileDir) + const oc = new OverviewCanvas(ic.x, ic.plotWidth, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) // Draw cytogenetic ideogram figure const cg = new CytogeneticIdeogram({ targetId: 'cytogenetic-ideogram', diff --git a/assets/js/interactive.js b/assets/js/interactive.js index 880f492b..8dafcf96 100644 --- a/assets/js/interactive.js +++ b/assets/js/interactive.js @@ -6,8 +6,8 @@ import { get } from './fetch.js' import { BaseScatterTrack } from './track.js' export class InteractiveCanvas extends BaseScatterTrack { - constructor (inputField, lineMargin, near, far, sampleName, genomeBuild, hgFileDir) { - super({ sampleName, genomeBuild, hgFileDir }) + constructor (inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) { + super({ caseId, sampleName, genomeBuild, hgFileDir }) // The canvas input field to display and fetch chromosome range from this.inputField = inputField // Plot variable @@ -230,6 +230,7 @@ export class InteractiveCanvas extends BaseScatterTrack { console.time('getcoverage') get('get-coverage', { region: `${chrom}:${start}-${end}`, + case_id: this.caseId, sample_id: this.sampleName, genome_build: this.genomeBuild, hg_filedir: this.hgFileDir, diff --git a/assets/js/overview.js b/assets/js/overview.js index 18db17af..8006f52e 100644 --- a/assets/js/overview.js +++ b/assets/js/overview.js @@ -7,9 +7,9 @@ import { createGraph, drawPoints, drawGraphLines, drawText, drawRotatedText } fr import { drawTrack } from './navigation.js' export class OverviewCanvas extends BaseScatterTrack { - constructor (xPos, fullPlotWidth, lineMargin, near, far, sampleName, + constructor (xPos, fullPlotWidth, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) { - super({ sampleName, genomeBuild, hgFileDir }) + super({ caseId, sampleName, genomeBuild, hgFileDir }) // Plot variables this.fullPlotWidth = fullPlotWidth // Width for all chromosomes to fit in @@ -225,6 +225,7 @@ export class OverviewCanvas extends BaseScatterTrack { await this.getOverviewChromDim() // query gens for coverage values const covData = await create('get-multiple-coverages', { + case_id: this.caseId, sample_id: this.sampleName, genome_build: this.genomeBuild, plot_height: this.plotHeight, diff --git a/assets/js/track/base.js b/assets/js/track/base.js index aca38df7..d9a65d68 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -24,8 +24,9 @@ export function lightenColor (color, percent) { }; export class BaseScatterTrack { - constructor ({ sampleName, genomeBuild, hgFileDir }) { + constructor ({ caseId, sampleName, genomeBuild, hgFileDir }) { // setup IO + this.caseId = caseId // Case id to use for querying data this.sampleName = sampleName // File name to load data from this.genomeBuild = genomeBuild // Whether to load HG37 or HG38, default is HG38 this.hgFileDir = hgFileDir // File directory From 9175c163cac45d12cc0aef16a6dfcff5a9f3b57b Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 10:48:50 +0200 Subject: [PATCH 038/138] Merge --- CHANGELOG.md | 1 + gens/blueprints/home/templates/home.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6a246f..230c272b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Fixed annotation tracks being hidden behind other elements - Increased contrast of region selector - Chromosome bands are displayed properly + - Use sample id as individual id to link out from Gens home sample list ## [2.1.0] ### Added diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 7826ed9e..069b4986 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -46,7 +46,7 @@

Samples

{% set sample_id=sample["sample_id"]%} {% set case_id=sample["case_id"]%} - {{ sample_id }} + {{ sample_id }} {{ genome_build }} {% if sample["has_overview_file"] %} From 4b5c677d6e24faa61931b509cc8d9722d62962a6 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 11:07:16 +0200 Subject: [PATCH 039/138] Merge --- setup.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c89393f0..b161e805 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,14 @@ packages=find_packages(), zip_safe=False, install_requires=[ - "Click", - "Flask", - "flask-caching", + "Click>=7.0", + "Flask>=1.1.2, <2.3", + "flask-debugtoolbar>=0.11.0", + "flask-caching>=1.9.0", + "itsdangerous>=1.1.0", + "Jinja2>=2.11.1", + "MarkupSafe>=1.1.1", + "Werkzeug>=1.0.0", "pymongo>=3.9.0", "gtfparse>=1.2.0", "pysam>=0.15.4", From c01adc7eb18f78d78e6a981b90cef3f5e0e4d1fc Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 11:26:22 +0200 Subject: [PATCH 040/138] fetch case_id.. --- gens/blueprints/home/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gens/blueprints/home/views.py b/gens/blueprints/home/views.py index 96901331..8e9c8774 100644 --- a/gens/blueprints/home/views.py +++ b/gens/blueprints/home/views.py @@ -55,6 +55,7 @@ def home(): samples = [ { "sample_id": smp.sample_id, + "case_id": smp.case_id, "genome_build": smp.genome_build, "has_overview_file": smp.overview_file is not None, "files_present": os.path.isfile(smp.baf_file) From f255370e62158ae3c562e06dbc21457b9165ed31 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 11:38:58 +0200 Subject: [PATCH 041/138] make that conditional on if case_id is set --- gens/blueprints/home/templates/home.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 069b4986..ac6e2542 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -46,7 +46,11 @@

Samples

{% set sample_id=sample["sample_id"]%} {% set case_id=sample["case_id"]%} - {{ sample_id }} + {{ sample_id }} {{ genome_build }} {% if sample["has_overview_file"] %} From c5b9307a6464fcc3eb7b7fb45a0c26390d54e32c Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 11:45:37 +0200 Subject: [PATCH 042/138] typo, and use latest github actions --- .github/workflows/docker_push.yml | 10 +++++----- .github/workflows/stage_docker_push.yml | 10 +++++----- gens/blueprints/home/templates/home.html | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml index 420cc817..314f5015 100644 --- a/.github/workflows/docker_push.yml +++ b/.github/workflows/docker_push.yml @@ -11,24 +11,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5 + uses: tj-actions/branch-names@v6 - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/stage_docker_push.yml b/.github/workflows/stage_docker_push.yml index f6e39f10..2551c56e 100644 --- a/.github/workflows/stage_docker_push.yml +++ b/.github/workflows/stage_docker_push.yml @@ -14,24 +14,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5 + uses: tj-actions/branch-names@v6 - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index ac6e2542..c7aacd5d 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -50,7 +50,7 @@

Samples

{{ url_for('gens.display_case', sample_name=False, individual_id=sample_id, case_id=case_id, genome_build=genome_build) }} {% else %} {{ url_for('gens.display_case', sample_name=sample_id, genome_build=genome_build) }} - {{ endif }}" target="_blank">{{ sample_id }} + {% endif %}" target="_blank">{{ sample_id }} {{ genome_build }} {% if sample["has_overview_file"] %} From 89908cbc7ed24f2393d6f1440e5a6c07541ffc0c Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 11:57:49 +0200 Subject: [PATCH 043/138] A few more action versions plus slightly better compatibility with old gens --- .github/workflows/keep_a_changelog.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- gens/blueprints/home/templates/home.html | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/keep_a_changelog.yml b/.github/workflows/keep_a_changelog.yml index 99e3bdc3..b370de25 100644 --- a/.github/workflows/keep_a_changelog.yml +++ b/.github/workflows/keep_a_changelog.yml @@ -8,7 +8,7 @@ jobs: changelog: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: dangoslen/changelog-enforcer@v1.4.0 with: changeLogPath: 'CHANGELOG.md' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba4008ae..a1744395 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,11 +15,11 @@ jobs: steps: # Check out Scout code - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # Set up python - name: Set up Python ${{ matrix.python-version}} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version}} @@ -49,9 +49,9 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm ci diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index c7aacd5d..0a49fb5e 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -47,7 +47,7 @@

Samples

{% set case_id=sample["case_id"]%} {{ sample_id }} From cdd9a1d3db96dc6e15ef6973ea602533d079bd9b Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 12:04:51 +0200 Subject: [PATCH 044/138] also use latest changelog enforcer action --- .github/workflows/keep_a_changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keep_a_changelog.yml b/.github/workflows/keep_a_changelog.yml index b370de25..bf589822 100644 --- a/.github/workflows/keep_a_changelog.yml +++ b/.github/workflows/keep_a_changelog.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dangoslen/changelog-enforcer@v1.4.0 + - uses: dangoslen/changelog-enforcer@v3 with: changeLogPath: 'CHANGELOG.md' skipLabel: 'Skip-Changelog' From f604d3b2f3a411164d7df12e6057c89ea02d93c3 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 12:13:19 +0200 Subject: [PATCH 045/138] Jest config --- jest.config.js | 194 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..24d28ebd --- /dev/null +++ b/jest.config.js @@ -0,0 +1,194 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/trannel/tmp/jest_s7", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + moduleNameMapper: {"\\.(css|less)$": "identity-obj-proxy"}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "jsdom", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; From 0351e82de4143e32b914a20e5608dedeab128f5d Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 8 May 2023 12:26:13 +0200 Subject: [PATCH 046/138] yarn that identity module --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b979b489..67155807 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "gulp-rename": "^2.0.0", "gulp-sass": "^5.0.0", "gulp-sourcemaps": "^3.0.0", + "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "process": "^0.11.10", "regenerator-runtime": "^0.13.7", From 891c43c6f1a75664b2df345fd0fb8c5fc7dd5515 Mon Sep 17 00:00:00 2001 From: Chiara Rasi Date: Wed, 6 Dec 2023 09:51:11 +0100 Subject: [PATCH 047/138] Merge --- .github/workflows/docker_push.yml | 10 +++++----- .github/workflows/keep_a_changelog.yml | 2 +- .github/workflows/preproc_docker_push.yml | 10 +++++----- .github/workflows/stage_docker_push.yml | 10 +++++----- .github/workflows/stale.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- .gitignore | 1 + CHANGELOG.md | 3 +++ 8 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml index 314f5015..55819e0c 100644 --- a/.github/workflows/docker_push.yml +++ b/.github/workflows/docker_push.yml @@ -11,24 +11,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v6 + uses: tj-actions/branch-names@v7 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/keep_a_changelog.yml b/.github/workflows/keep_a_changelog.yml index bf589822..1d56d9fe 100644 --- a/.github/workflows/keep_a_changelog.yml +++ b/.github/workflows/keep_a_changelog.yml @@ -8,7 +8,7 @@ jobs: changelog: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dangoslen/changelog-enforcer@v3 with: changeLogPath: 'CHANGELOG.md' diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index 7f1606ef..897919d4 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -11,25 +11,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out git repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5 + uses: tj-actions/branch-names@v7 - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Build and push if: steps.branch-name.outputs.is_default == 'false' - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./utils/ file: ./utils/Dockerfile diff --git a/.github/workflows/stage_docker_push.yml b/.github/workflows/stage_docker_push.yml index 2551c56e..8ac55dc7 100644 --- a/.github/workflows/stage_docker_push.yml +++ b/.github/workflows/stage_docker_push.yml @@ -14,24 +14,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v6 + uses: tj-actions/branch-names@v7 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b671fc09..a44616c1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Stale issue message' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1744395..4fba2c88 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] - mongodb-version: ["3.6"] + python-version: [3.8] + mongodb-version: ["7"] steps: # Check out Scout code - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Set up python - name: Set up Python ${{ matrix.python-version}} @@ -51,7 +51,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci diff --git a/.gitignore b/.gitignore index 65d90ead..0e02d154 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ utils/hg38_annotations/ venv/ *.egg-info .eggs +.idea # node node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 230c272b..9ec5b763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Added ### Changed + - Changes the main view's page title to be `sample_name` and adds `sample_name` and `case_id` to the header title + - Updated external images used in GitHub actions, including tj-actions/branch-names to v7 (fixes a security issue) + - Updated Python and MongoDB version used in tests workflow to 3.8 and 7 respectively ### Fixed - Fixed bug that prevented updating annotation tracks From 6c06fd6958a1fed28099dc811ee6f2aaeb7d6fe3 Mon Sep 17 00:00:00 2001 From: Emil Date: Tue, 21 Nov 2023 17:39:27 +0100 Subject: [PATCH 048/138] Fixes titles, var names still mixed up in spots --- gens/blueprints/gens/templates/gens.html | 6 +++--- gens/blueprints/gens/views.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index e7fc3c61..400fb1a0 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -13,7 +13,7 @@ {% endmacro %} {% extends "layout.html" %} -{% block title %}Visualization{% endblock title %} +{% block title %}{{ sample_name }}{% endblock title %} {% block css_style %} {{ super() }} @@ -46,7 +46,7 @@ {{todays_date}}
- {{sample_name}} + {{sample_name}} | {{case_id}} | {{individual_id}} (Genome build: {{genome_build}})
@@ -157,7 +157,7 @@ // Initiate variant, annotation and transcript canvases // and pass flask values to tracks const {ic, oc, ac, tc, vc} = gens.initCanvases({ - sampleName: '{{ sample_name }}', + sampleName: '{{ individual_id }}', caseId: '{{ case_id }}', genomeBuild: {{ genome_build }}, uiColors: {{ ui_colors | tojson | safe }}, diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index 7bd72593..08c9ac51 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -75,7 +75,8 @@ def display_case(sample_name): chrom=chrom, start=start_pos, end=end_pos, - sample_name=individual_id, + sample_name=sample_name, + individual_id=individual_id, case_id=case_id, genome_build=genome_build, print_page=print_page, From 80ce8bf9f647be703cf4387ee43101f8b816d528 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Thu, 21 Dec 2023 15:03:41 +0100 Subject: [PATCH 049/138] Add some docs info for track loading --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 97bec1e6..f11cb6ed 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,42 @@ The **o** resolution is used only for the whole genome overview plot. The number We're using all SNPs in gnomAD with an total allele frequency > 5%, which in gnomAD 2.1 is approximately 7.5 million SNPs. +### Loading reference tracks + +Gens allows adding multiple tracks, most easily provided in one directory. As an illustration, here is how to format a UCSC DGV bb track for Gens display. + +Download the DGV bb track from [UCSC](https://genome.ucsc.edu/cgi-bin/hgTables?db=hg19&hgta_group=varRep&hgta_track=dgvPlus&hgta_table=dgvMerged&hgta_doSchema=describe+table+schema). +Convert bigBed to Bed, cut relevant columns and name them according to Gens standard. +``` +./bigBedToBed /home/proj/stage/rare-disease/gens-tracks/dgvMerged.bb dgvMerged.bed +cut -f1,2,3,4,9 dgvMerged.bed > dgvMerged.fivecol.bed +cat > header +Chromosome Start Stop Name Color +cat header dgvMerged.fivecol.bed > /home/proj/stage/rare-disease/gens-tracks/DGV_UCSC_2023-03-09.bed +``` + +``` +us +conda activate S_gens +gens load annotations -b 37 -f /home/proj/stage/rare-disease/gens-tracks +``` + +This should result in something like: +``` +[2023-12-15 14:45:06,959] INFO in app: Using default Gens configuration +[2023-12-15 14:45:06,959] INFO in db: Initialize db connection +[2023-12-15 14:45:07,111] INFO in load: Processing files +[2023-12-15 14:45:07,112] INFO in load: Processing /home/proj/stage/rare-disease/gens-tracks/Final_common_CNV_clusters_0.bed +[2023-12-15 14:45:07,144] INFO in load: Remove old entry in the database +[2023-12-15 14:45:07,230] INFO in load: Load annoatations in the database +[2023-12-15 14:45:07,309] INFO in load: Update height order +[2023-12-15 14:45:10,792] INFO in load: Processing /home/proj/stage/rare-disease/gens-tracks/DGV_UCSC_2023-03-09.bed +[2023-12-15 14:45:16,170] INFO in load: Remove old entry in the database +[2023-12-15 14:45:16,173] INFO in load: Load annoatations in the database +[2023-12-15 14:45:41,873] INFO in load: Update height order +Finished loading annotations ✔ +``` + ## Limitations - Currently no efforts have been made to make it work for non-human organisms. Chromosome names are currently hardcoded to 1-23,X,Y. From a25c2c38a083f3b35459796917982a52ec1b2524 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Fri, 15 Nov 2024 15:27:57 +0100 Subject: [PATCH 050/138] Always this changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ec5b763..3aec4907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [x.x.x] ### Added +- Document track processing and loading + ### Changed - Changes the main view's page title to be `sample_name` and adds `sample_name` and `case_id` to the header title - Updated external images used in GitHub actions, including tj-actions/branch-names to v7 (fixes a security issue) From 3b9653e6b8f25b14408457810a55c8c6951771b2 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:07:06 +0100 Subject: [PATCH 051/138] Clear visual clutter by hiding balanced variants --- assets/js/track/variant.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 75ad0646..e9532b20 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -7,7 +7,7 @@ import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateV import { createPopper } from '@popperjs/core' // Draw variants -const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication' } +const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication', cnv: 'copy number variation', inv: 'inversion', bnd: 'break end' } export class VariantTrack extends BaseAnnotationTrack { constructor (x, width, near, far, caseId, genomeBuild, colorSchema, highlightedVariantId) { @@ -132,7 +132,7 @@ export class VariantTrack extends BaseAnnotationTrack { scale: this.offscreenPosition.scale }) // create a tooltip html element and append to DOM - if ( drawTooltips ) { + if (drawTooltips && ['dup', 'del', 'cnv'].includes(variantCategory)) { const tooltip = createTooltipElement({ id: `popover-${variantObj.id}`, title: `${variantType.toUpperCase()}: ${variant.category} - ${VARIANT_TR_TABLE[variantCategory]}`, @@ -189,6 +189,7 @@ export class VariantTrack extends BaseAnnotationTrack { color }) break + case 'cnv': case 'dup': drawLine({ ctx: this.drawCtx, @@ -207,6 +208,10 @@ export class VariantTrack extends BaseAnnotationTrack { color }) break + case 'bnd': + case 'inv': + // no support for balanced events + break default: // other types of elements drawLine({ ctx: this.drawCtx, From c1fc29bdb800a779d786aaa89d467bda1d0d9cf1 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:11:35 +0100 Subject: [PATCH 052/138] Changelog merge --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22413d84..aca06845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Added - Document track processing and loading +- OAuth authentication +### Changed +- Hide balanced variants ### Changed - Changes the main view's page title to be `sample_name` and adds `sample_name` and `case_id` to the header title From 615968248ee86d2371237414a5c057745a42fe6b Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:20:06 +0100 Subject: [PATCH 053/138] also hide text --- assets/js/track/variant.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index e9532b20..a36cc4b1 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -228,15 +228,17 @@ export class VariantTrack extends BaseAnnotationTrack { this.drawHighlight(variantObj.x1, variantObj.x2) } - const textYPos = this.tracksYPos(heightOrder) - // Draw variant type - drawText({ - ctx: this.drawCtx, - text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, - x: scale * ((variantObj.start + variantObj.end) / 2 - this.offscreenPosition.start), - y: textYPos + this.featureHeight, - fontProp: textSize - }) + if (['dup', 'del', 'cnv'].includes(variantCategory)) { + const textYPos = this.tracksYPos(heightOrder) + // Draw variant type + drawText({ + ctx: this.drawCtx, + text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, + x: scale * ((variantObj.start + variantObj.end) / 2 - this.offscreenPosition.start), + y: textYPos + this.featureHeight, + fontProp: textSize + }) + } } } } From e653f75427010bc7a28e077174832f1c755732be Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:31:14 +0100 Subject: [PATCH 054/138] Variant tooltip includes length --- assets/js/track/variant.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index a36cc4b1..46b7466f 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -142,6 +142,7 @@ export class VariantTrack extends BaseAnnotationTrack { { title: 'Ref', value: `${variant.reference}` }, { title: 'Alt', value: `${variant.alternative}` }, { title: 'Cytoband start/end', value: `${variant.cytoband_start}/${variant.cytoband_end}` }, + { title: 'Length': value: `${variant.length}` } { title: 'Quality', value: `${variant.quality}` }, { title: 'Rank score', value: `${variant.rank_score}` } ] From 37f7a4543aa394880fdfa2a3e1a56a94d14407a8 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:32:45 +0100 Subject: [PATCH 055/138] lint --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 46b7466f..5ff66c19 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -142,7 +142,7 @@ export class VariantTrack extends BaseAnnotationTrack { { title: 'Ref', value: `${variant.reference}` }, { title: 'Alt', value: `${variant.alternative}` }, { title: 'Cytoband start/end', value: `${variant.cytoband_start}/${variant.cytoband_end}` }, - { title: 'Length': value: `${variant.length}` } + { title: 'Length': value: `${variant.length}` }, { title: 'Quality', value: `${variant.quality}` }, { title: 'Rank score', value: `${variant.rank_score}` } ] From 8fce2095f228fd733237485def5608fa7b091421 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 21 Feb 2024 14:33:08 +0100 Subject: [PATCH 056/138] lint --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 5ff66c19..ed07cee1 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -142,7 +142,7 @@ export class VariantTrack extends BaseAnnotationTrack { { title: 'Ref', value: `${variant.reference}` }, { title: 'Alt', value: `${variant.alternative}` }, { title: 'Cytoband start/end', value: `${variant.cytoband_start}/${variant.cytoband_end}` }, - { title: 'Length': value: `${variant.length}` }, + { title: 'Length', value: `${variant.length}` }, { title: 'Quality', value: `${variant.quality}` }, { title: 'Rank score', value: `${variant.rank_score}` } ] From de90203114ff0a32571c3417a53c310604814de9 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Thu, 22 Feb 2024 11:52:40 +0100 Subject: [PATCH 057/138] Fix #58 - dont shrink distance on pan over chr start --- CHANGELOG.md | 2 +- assets/js/navigation.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14442e41..44aa4fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Use sample id instead of display name for variant retrieval - Hide balanced variants - Keyboard pan speed increased - +- Dont shrink pan window when attemting to pan over start ## [2.1.1] ### Added diff --git a/assets/js/navigation.js b/assets/js/navigation.js index c35b7a95..1e50d7bd 100644 --- a/assets/js/navigation.js +++ b/assets/js/navigation.js @@ -159,8 +159,8 @@ export function panTracks(direction = 'left', speed = 0.1) { } // drawTrack will correct the window eventually, but let us not go negative at least if (pos.start < 1) { + pos.end = distance - pos.start pos.start = 1 - pos.end = distance } drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, drawTitle: false, exclude: ['cytogenetic-ideogram'] }) } From ebd7e0f35a40f85a20083bab7d2c511cc1c55024 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Thu, 22 Feb 2024 12:05:40 +0100 Subject: [PATCH 058/138] addition is hard :) --- assets/js/navigation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/navigation.js b/assets/js/navigation.js index 1e50d7bd..72718556 100644 --- a/assets/js/navigation.js +++ b/assets/js/navigation.js @@ -158,8 +158,8 @@ export function panTracks(direction = 'left', speed = 0.1) { pos.end += distance } // drawTrack will correct the window eventually, but let us not go negative at least - if (pos.start < 1) { - pos.end = distance - pos.start + if (pos.start < 0) { + pos.end = pos.end + distance - pos.start pos.start = 1 } drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, drawTitle: false, exclude: ['cytogenetic-ideogram'] }) From 9e814b5fd4a6c7a2dae23a4663cf5934352d569d Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Thu, 22 Feb 2024 12:12:57 +0100 Subject: [PATCH 059/138] indeed --- assets/js/navigation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/navigation.js b/assets/js/navigation.js index 72718556..d0ace2e9 100644 --- a/assets/js/navigation.js +++ b/assets/js/navigation.js @@ -159,7 +159,7 @@ export function panTracks(direction = 'left', speed = 0.1) { } // drawTrack will correct the window eventually, but let us not go negative at least if (pos.start < 0) { - pos.end = pos.end + distance - pos.start + pos.end = pos.end - pos.start pos.start = 1 } drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, drawTitle: false, exclude: ['cytogenetic-ideogram'] }) From 43cf816e06df6758cc09a40e6b3b27c58e7ae309 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 28 Feb 2024 09:32:04 +0100 Subject: [PATCH 060/138] Version 2.2 merge --- CHANGELOG.md | 25 ++++++------------------- gens/__version__.py | 4 ++++ setup.cfg | 2 +- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44aa4fe5..d794ae3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,28 +6,15 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [x.x.x] +## [2.2] (Changes from Stockholm) ### Added -- Document track processing and loading -- OAuth authentication + - Document track processing and loading + - OAuth authentication ### Changed -- Hide balanced variants - -### Changed - - Changes the main view's page title to be `sample_name` and adds `sample_name` and `case_id` to the header title - - Updated external images used in GitHub actions, including tj-actions/branch-names to v7 (fixes a security issue) - - Updated Python and MongoDB version used in tests workflow to 3.8 and 7 respectively -### Fixed + - Use sample id instead of display name for variant retrieval + - Hide balanced variants - Fixed bug that prevented updating annotation tracks - -## [2.1.2] -### Added -- Document track processing and loading -- OAuth authentication -### Changed -- Use sample id instead of display name for variant retrieval -- Hide balanced variants -- Keyboard pan speed increased -- Dont shrink pan window when attemting to pan over start + - Don't shrink pan window when attemting to pan over start ## [2.1.1] ### Added diff --git a/gens/__version__.py b/gens/__version__.py index b7775796..d3c1e1a4 100644 --- a/gens/__version__.py +++ b/gens/__version__.py @@ -1 +1,5 @@ +<<<<<<< HEAD VERSION = "2.1.2" +======= +VERSION = "2.2" +>>>>>>> 669d1f9 (version 2.2) diff --git a/setup.cfg b/setup.cfg index 97afa0c7..fe3b5b1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.1.1 +current_version = 2.2 [metadata] description-file = README.md From 396f623b1f527730c749cc6e64640d1e87750871 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 09:05:36 +0100 Subject: [PATCH 061/138] Remove markus from docker compose --- docker-compose.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 128aef71..6ff76d68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,14 @@ version: "3.8" services: - markus_mongodb: - container_name: markus_mongodb + mongodb: + container_name: mongodb image: mongo volumes: - ./volumes/mongo_db/data:/data/db networks: - gens-net - markus_gens: - container_name: markus_gens + gens: + container_name: gens build: . volumes: - ./utils:/home/app/app/utils @@ -20,7 +20,7 @@ services: environment: - FLASK_APP=gens/app.py - FLASK_ENV=development - - MONGODB_HOST=markus_mongodb + - MONGODB_HOST=mongodb - MONGODB_PORT=27017 - SCOUT_DBNAME=scout - GENS_DBNAME=gens @@ -31,7 +31,7 @@ services: networks: - gens-net depends_on: - - markus_mongodb + - mongodb command: "flask run --host 0.0.0.0" networks: gens-net: From 2ad4be70329408738404d8e394940212444d45d9 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 09:06:38 +0100 Subject: [PATCH 062/138] Fix leftover diff --- gens/__version__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gens/__version__.py b/gens/__version__.py index d3c1e1a4..64a5af4d 100644 --- a/gens/__version__.py +++ b/gens/__version__.py @@ -1,5 +1 @@ -<<<<<<< HEAD -VERSION = "2.1.2" -======= VERSION = "2.2" ->>>>>>> 669d1f9 (version 2.2) From f22ae74db52d0598a5bd3a10833de4390e5b237c Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 1 Mar 2024 10:22:42 +0100 Subject: [PATCH 063/138] Fix #48 - produce prod docker image copy with branch tag, and update actions. --- .github/workflows/docker_push.yml | 2 +- .github/workflows/preproc_docker_push.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml index 55819e0c..d165f2e9 100644 --- a/.github/workflows/docker_push.yml +++ b/.github/workflows/docker_push.yml @@ -33,4 +33,4 @@ jobs: context: ./ file: ./Dockerfile push: true - tags: "clinicalgenomics/gens:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens:latest" + tags: "clinicalgenomics/gens:${{ github.event.release.tag_name }}, ${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens:latest" diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index 897919d4..d0c82f9c 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -34,4 +34,4 @@ jobs: context: ./utils/ file: ./utils/Dockerfile push: true - tags: "clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest, clinicalgenomics/gens-preproc:1.0.0" + tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a44616c1..168b8731 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Stale issue message' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4fba2c88..aa109e59 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: # Set up python - name: Set up Python ${{ matrix.python-version}} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version}} @@ -49,7 +49,7 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: From 0d6988d3cad73b1ddec5c45277a3ba853558252a Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 1 Mar 2024 10:34:29 +0100 Subject: [PATCH 064/138] Changelog --- .github/workflows/preproc_docker_push.yml | 9 ++--- .../workflows/preproc_stage_docker_push.yml | 37 +++++++++++++++++++ CHANGELOG.md | 6 ++- 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/preproc_stage_docker_push.yml diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index d0c82f9c..a1c85128 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -1,13 +1,13 @@ -name: Publish preproc to Docker stage +name: Publish preproc to Docker on: - pull_request: + push: branches: - master jobs: docker-preproc-push: - name: Create preproc docker image + name: Create preproc Docker image runs-on: ubuntu-latest steps: - name: Check out git repository @@ -28,10 +28,9 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build and push - if: steps.branch-name.outputs.is_default == 'false' uses: docker/build-push-action@v5 with: context: ./utils/ file: ./utils/Dockerfile push: true - tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" + tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc-stage:latest" diff --git a/.github/workflows/preproc_stage_docker_push.yml b/.github/workflows/preproc_stage_docker_push.yml new file mode 100644 index 00000000..b484c143 --- /dev/null +++ b/.github/workflows/preproc_stage_docker_push.yml @@ -0,0 +1,37 @@ +name: Publish preproc to Docker stage + +on: + pull_request: + branches: + - master + +jobs: + docker-preproc-stage-push: + name: Create preproc stage Docker image + runs-on: ubuntu-latest + steps: + - name: Check out git repository + uses: actions/checkout@v4 + + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v7 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + if: steps.branch-name.outputs.is_default == 'false' + uses: docker/build-push-action@v5 + with: + context: ./utils/ + file: ./utils/Dockerfile + push: true + tags: "clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" diff --git a/CHANGELOG.md b/CHANGELOG.md index d794ae3f..cf08bc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [x.x.x] -## [2.2] (Changes from Stockholm) +## [unreleased] +### Changed +- Archive prod docker image with release tag name. Update action versions. + +## [2.2] ### Added - Document track processing and loading - OAuth authentication From 5cffa70011f54bc75b59fbf89b890a947d919e2b Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 1 Mar 2024 10:41:28 +0100 Subject: [PATCH 065/138] and switch --- .github/workflows/preproc_docker_push.yml | 2 +- .github/workflows/preproc_stage_docker_push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index a1c85128..b86d9887 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -33,4 +33,4 @@ jobs: context: ./utils/ file: ./utils/Dockerfile push: true - tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc-stage:latest" + tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" diff --git a/.github/workflows/preproc_stage_docker_push.yml b/.github/workflows/preproc_stage_docker_push.yml index b484c143..c316a3fd 100644 --- a/.github/workflows/preproc_stage_docker_push.yml +++ b/.github/workflows/preproc_stage_docker_push.yml @@ -34,4 +34,4 @@ jobs: context: ./utils/ file: ./utils/Dockerfile push: true - tags: "clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" + tags: "clinicalgenomics/gens-preproc-stage:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc-stage:latest" From c999121fea409c007c3244df1bb9e54e5a64fd8c Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 1 Mar 2024 10:46:53 +0100 Subject: [PATCH 066/138] changelog action setting --- .github/workflows/keep_a_changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keep_a_changelog.yml b/.github/workflows/keep_a_changelog.yml index 1d56d9fe..e13f9372 100644 --- a/.github/workflows/keep_a_changelog.yml +++ b/.github/workflows/keep_a_changelog.yml @@ -12,4 +12,4 @@ jobs: - uses: dangoslen/changelog-enforcer@v3 with: changeLogPath: 'CHANGELOG.md' - skipLabel: 'Skip-Changelog' + skipLabels: 'Skip-Changelog' From 3d2a087bbd9571c75e120b49b89ad8edd0d46bd2 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 4 Mar 2024 08:54:22 +0100 Subject: [PATCH 067/138] Broken DNA backround static path --- gens/templates/generic_abort_error.html | 2 +- gens/templates/generic_exception_error.html | 2 +- gens/templates/sample_not_found.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gens/templates/generic_abort_error.html b/gens/templates/generic_abort_error.html index 6b9fa98c..a0306060 100644 --- a/gens/templates/generic_abort_error.html +++ b/gens/templates/generic_abort_error.html @@ -10,7 +10,7 @@ {% block body %}

{{ error_code }}

- broken DNA + broken DNA

Something went wrong

Gens encountered and error. Please contact the administrator to resolve this issue.

diff --git a/gens/templates/generic_exception_error.html b/gens/templates/generic_exception_error.html index 4b83d1f0..96b59ce2 100644 --- a/gens/templates/generic_exception_error.html +++ b/gens/templates/generic_exception_error.html @@ -9,7 +9,7 @@ {% block body %}
- broken DNA + broken DNA

Error - {{ error_type }}

{% for row in message %} diff --git a/gens/templates/sample_not_found.html b/gens/templates/sample_not_found.html index 1a6929f4..9ee6ded2 100644 --- a/gens/templates/sample_not_found.html +++ b/gens/templates/sample_not_found.html @@ -9,7 +9,7 @@ {% block body %}
- broken DNA + broken DNA

Missing sample

The sample {{ file_name }} has not been added to Gens database. Please contact the administrator to resolve this issue.

From 6cee8b0070ba40953c7953c54bbaa3ddf90b54a6 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 4 Mar 2024 08:59:56 +0100 Subject: [PATCH 068/138] Merge --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf08bc94..2d116d6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [unreleased] ### Changed - Archive prod docker image with release tag name. Update action versions. +## [unreleased] +### Fixed +- Error image background static path ## [2.2] ### Added From a6a4e99d4712554c4f726bccb06f4d44427ded1e Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 4 Mar 2024 09:04:30 +0100 Subject: [PATCH 069/138] reading and spelling --- gens/templates/generic_abort_error.html | 2 +- gens/templates/generic_exception_error.html | 2 +- gens/templates/sample_not_found.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gens/templates/generic_abort_error.html b/gens/templates/generic_abort_error.html index a0306060..8a249fc7 100644 --- a/gens/templates/generic_abort_error.html +++ b/gens/templates/generic_abort_error.html @@ -10,7 +10,7 @@ {% block body %}

{{ error_code }}

- broken DNA + broken DNA

Something went wrong

Gens encountered and error. Please contact the administrator to resolve this issue.

diff --git a/gens/templates/generic_exception_error.html b/gens/templates/generic_exception_error.html index 96b59ce2..3cd48af0 100644 --- a/gens/templates/generic_exception_error.html +++ b/gens/templates/generic_exception_error.html @@ -9,7 +9,7 @@ {% block body %}
- broken DNA + broken DNA

Error - {{ error_type }}

{% for row in message %} diff --git a/gens/templates/sample_not_found.html b/gens/templates/sample_not_found.html index 9ee6ded2..217d93d6 100644 --- a/gens/templates/sample_not_found.html +++ b/gens/templates/sample_not_found.html @@ -9,7 +9,7 @@ {% block body %}
- broken DNA + broken DNA

Missing sample

The sample {{ file_name }} has not been added to Gens database. Please contact the administrator to resolve this issue.

From 2c1384b50542df964a486eafb7be3c11fc405606 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 10:05:03 +0100 Subject: [PATCH 070/138] Changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d116d6b..81b2454d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org/) About changelog [here](https://keepachangelog.com/en/1.0.0/) -## [x.x.x] - ## [unreleased] +### Added +- Link out to Scout: introduce config variable for base URL +- Link out to Scout: case links on home sample list +- Link out to Scout: click variant to open Scout page ### Changed - Archive prod docker image with release tag name. Update action versions. -## [unreleased] ### Fixed - Error image background static path From 565fe00b3d96c1582969851aa8b63c62a33f1de8 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 10:05:58 +0100 Subject: [PATCH 071/138] Sync package lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ffef9ca..2601a2bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gens", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gens", - "version": "2.1.0", + "version": "2.2.0", "dependencies": { "@popperjs/core": "^2.9.2", "npm": "^10.2.3", From 2be2b5f67ac2f844b91708b4bf37ce8d42620c66 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 28 Feb 2024 09:27:23 +0100 Subject: [PATCH 072/138] linkout preparation --- assets/js/gens.js | 4 ++-- assets/js/track/variant.js | 10 +++++++++- gens/api.py | 5 ++--- gens/blueprints/gens/templates/gens.html | 5 +++-- gens/blueprints/gens/views.py | 1 + gens/config.py | 3 +++ 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index 1dbe322d..d3dee942 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -8,7 +8,7 @@ export { panTracks, zoomIn, zoomOut, parseRegionDesignation, queryRegionOrGene } from './navigation.js' -export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiColors, selectedVariant, annotationFile }) { +export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiColors, scoutBaseURL, selectedVariant, annotationFile }) { // initialize and return the different canvases // WEBGL values const near = 0.1 @@ -19,7 +19,7 @@ export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiCol // Initiate interactive canvas const ic = new InteractiveCanvas(inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, selectedVariant) + const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, scoutBaseURL, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index ed07cee1..ab6dc42e 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -10,7 +10,7 @@ import { createPopper } from '@popperjs/core' const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication', cnv: 'copy number variation', inv: 'inversion', bnd: 'break end' } export class VariantTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, caseId, genomeBuild, colorSchema, highlightedVariantId) { + constructor (x, width, near, far, caseId, genomeBuild, colorSchema, scoutBaseURL, highlightedVariantId) { // Dimensions of track canvas const visibleHeight = 100 // Visible height for expanded canvas, overflows for scroll const minHeight = 35 // Minimized height @@ -167,6 +167,14 @@ export class VariantTrack extends BaseAnnotationTrack { tooltip: tooltip, isDisplayed: false } + // add context menu event listener to same virtual hitbox + virtualElement.addEventListener("click", () => { + url = scoutBaseURL + "inst/case/sv/variants/" + variant.id + var win = window.open(url, '_blank'); + win.focus(); + }, false) + virtualElement.addEventListener("dblclick", pinVariant, false) + virtualElement.addEventListener("contextmenu", classifyVariant, false) } this.geneticElements.push(variantObj) diff --git a/gens/api.py b/gens/api.py index f0e18c67..5bec0180 100644 --- a/gens/api.py +++ b/gens/api.py @@ -76,7 +76,6 @@ def valid_perc(self, attribute, value): if not 0 <= value <= 1: raise ValueError(f"{value} is not within 0-1") - def get_overview_chrom_dim(x_pos, y_pos, plot_width, genome_build): """ Returns the dimensions of all chromosome graphs in screen coordinates @@ -107,7 +106,7 @@ def get_annotation_data(region, source, genome_build, collapsed): if region == "" or source == "": msg = "Could not find annotation data in DB" LOG.error(msg) - retrun (jsonify({"detail": msg}), 404) + return (jsonify({"detail": msg}), 404) genome_build = request.args.get("genome_build", "38") res, chrom, start_pos, end_pos = parse_region_str(region, genome_build) @@ -149,7 +148,7 @@ def get_transcript_data(region, genome_build, collapsed): if region == "": msg = "Could not find transcript in database" LOG.error(msg) - retrun (jsonify({"detail": msg}), 404) + return (jsonify({"detail": msg}), 404) # Get transcripts within span [start_pos, end_pos] or transcripts that go over the span transcripts = list( diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index 1e7a1a59..afee63fd 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -76,7 +76,7 @@
- -
@@ -161,6 +161,7 @@ caseId: '{{ case_id }}', genomeBuild: {{ genome_build }}, uiColors: {{ ui_colors | tojson | safe }}, + scoutBaseURL: {{ scout_base_url | safe }}, selectedVariant: '{{ selected_variant }}', annotationFile: '{{ annotation }}', }) diff --git a/gens/blueprints/gens/views.py b/gens/blueprints/gens/views.py index a34ed502..1902d69b 100644 --- a/gens/blueprints/gens/views.py +++ b/gens/blueprints/gens/views.py @@ -72,6 +72,7 @@ def display_case(sample_name): return render_template( "gens.html", ui_colors=current_app.config["UI_COLORS"], + scout_base_url=current_app.config.get("SCOUT_BASE_URL"), chrom=chrom, start=start_pos, end=end_pos, diff --git a/gens/config.py b/gens/config.py index f2156c93..854637a3 100644 --- a/gens/config.py +++ b/gens/config.py @@ -5,6 +5,9 @@ GENS_DBNAME = "gens" SCOUT_DBNAME = "scout" +# Scout browser base URL for link out and API +SCOUT_BASE_URL = "http://localhost:8000" + # Annotation DEFAULT_ANNOTATION_TRACK = ( "Mimisbrunnr_databank_plausibly_pathogenic_CNVs_Lund_hg38.aed" From 789a8b3ccee9dfdf048644d51990fb27726e5e28 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Tue, 5 Mar 2024 08:16:41 +0100 Subject: [PATCH 073/138] Merge --- CHANGELOG.md | 1 + gens/blueprints/home/templates/home.html | 3 +++ gens/blueprints/home/views.py | 7 ++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b2454d..999323b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Link out to Scout: introduce config variable for base URL - Link out to Scout: case links on home sample list - Link out to Scout: click variant to open Scout page + ### Changed - Archive prod docker image with release tag name. Update action versions. ### Fixed diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 54bb4eae..1d91bfc6 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -34,6 +34,7 @@

Samples

Sample id + Case id Genome build Overivew file BAM/BAF found @@ -51,6 +52,8 @@

Samples

{% else %} {{ url_for('gens.display_case', sample_name=sample_id, genome_build=genome_build) }} {% endif %}" target="_blank">{{ sample_id }} + {% if case_id %}{{ case_id }} {{ genome_build }} {% if sample["has_overview_file"] %} diff --git a/gens/blueprints/home/views.py b/gens/blueprints/home/views.py index dab58dce..bb2864b2 100644 --- a/gens/blueprints/home/views.py +++ b/gens/blueprints/home/views.py @@ -66,8 +66,9 @@ def home(): ] return render_template( "home.html", - samples=samples, pagination=pagination_info, + samples=samples, + scout_base_url=current_app.config.get("SCOUT_BASE_URL"), version=version, ) @@ -80,10 +81,10 @@ def about(): ui_colors = current_app.config.get("UI_COLORS") return render_template( "about.html", - timestamps=timestamps, - version=version, config=config, + timestamps=timestamps, ui_colors=ui_colors, + version=version, ) From 1b7168de37fb5410500f0448731a5d028308156c Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 08:33:37 +0100 Subject: [PATCH 074/138] slight mod to scout endpoint uri --- gens/blueprints/home/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 1d91bfc6..493bfc99 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -53,7 +53,7 @@

Samples

{{ url_for('gens.display_case', sample_name=sample_id, genome_build=genome_build) }} {% endif %}" target="_blank">{{ sample_id }} {% if case_id %}{{ case_id }} + {{ scout_base_url + "/case/case_id/" + case_id }}">{{ case_id }} {{ genome_build }} {% if sample["has_overview_file"] %} From c1a0547b5f0b6e6e991df429668a19942785badd Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 14:57:50 +0100 Subject: [PATCH 075/138] add interaction for pinning and variant link out --- assets/js/track/variant.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index ab6dc42e..e0c60933 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -36,7 +36,7 @@ export class VariantTrack extends BaseAnnotationTrack { variant_category: 'sv', case_id: caseId } - + this.scoutBaseURL = scoutBaseURL // Initialize highlighted variant this.highlightedVariantId = highlightedVariantId initTrackTooltips(this) @@ -168,13 +168,16 @@ export class VariantTrack extends BaseAnnotationTrack { isDisplayed: false } // add context menu event listener to same virtual hitbox - virtualElement.addEventListener("click", () => { - url = scoutBaseURL + "inst/case/sv/variants/" + variant.id - var win = window.open(url, '_blank'); - win.focus(); - }, false) - virtualElement.addEventListener("dblclick", pinVariant, false) - virtualElement.addEventListener("contextmenu", classifyVariant, false) + virtualElement.addEventListener('click', () => { + var url = this.scoutBaseURL + '/document_id/' + variant.id + var win = window.open(url, '_blank') + win.focus() + }, false) + virtualElement.addEventListener('dblclick', () => { + var url = this.scoutBaseURL + '/' + variant.id + '/pin' + var win = window.open(url, '_blank') + }, false) + // virtualElement.addEventListener('contextmenu', classifyVariant, false) } this.geneticElements.push(variantObj) From 60d835bdc10385faa92a84ec863aa19c05220099 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 15:12:07 +0100 Subject: [PATCH 076/138] missing closing if --- gens/blueprints/home/templates/home.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 493bfc99..099bcae7 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -52,8 +52,9 @@

Samples

{% else %} {{ url_for('gens.display_case', sample_name=sample_id, genome_build=genome_build) }} {% endif %}" target="_blank">{{ sample_id }} - {% if case_id %}{{ case_id }} + {% if case_id %} + {{ case_id }} + {% endif %} {{ genome_build }} {% if sample["has_overview_file"] %} From d32d351e131bc247fcccb1093d106a1566d6cc76 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 15:14:47 +0100 Subject: [PATCH 077/138] quote url --- assets/js/track/variant.js | 2 +- gens/blueprints/gens/templates/gens.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index e0c60933..c58f7007 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -175,7 +175,7 @@ export class VariantTrack extends BaseAnnotationTrack { }, false) virtualElement.addEventListener('dblclick', () => { var url = this.scoutBaseURL + '/' + variant.id + '/pin' - var win = window.open(url, '_blank') + window.open(url, '_blank') }, false) // virtualElement.addEventListener('contextmenu', classifyVariant, false) } diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index afee63fd..43e0f2d4 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -161,7 +161,7 @@ caseId: '{{ case_id }}', genomeBuild: {{ genome_build }}, uiColors: {{ ui_colors | tojson | safe }}, - scoutBaseURL: {{ scout_base_url | safe }}, + scoutBaseURL: '{{ scout_base_url | safe }}', selectedVariant: '{{ selected_variant }}', annotationFile: '{{ annotation }}', }) From 77357db0c01e1079d7507254d67e19b8811cc092 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 16:19:30 +0100 Subject: [PATCH 078/138] Changelog --- assets/js/track/variant.js | 2 ++ gens/blueprints/home/templates/home.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index c58f7007..8c6cb1d1 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -170,11 +170,13 @@ export class VariantTrack extends BaseAnnotationTrack { // add context menu event listener to same virtual hitbox virtualElement.addEventListener('click', () => { var url = this.scoutBaseURL + '/document_id/' + variant.id + console.log(`Visit ${url}: scout variant`) var win = window.open(url, '_blank') win.focus() }, false) virtualElement.addEventListener('dblclick', () => { var url = this.scoutBaseURL + '/' + variant.id + '/pin' + console.log(`Visit ${url}: scout PIN variant`) window.open(url, '_blank') }, false) // virtualElement.addEventListener('contextmenu', classifyVariant, false) diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 099bcae7..4078858d 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -53,7 +53,7 @@

Samples

{{ url_for('gens.display_case', sample_name=sample_id, genome_build=genome_build) }} {% endif %}" target="_blank">{{ sample_id }} {% if case_id %} - {{ case_id }} + {{ case_id }} {% endif %} {{ genome_build }} From dc0a3e65d3df6623a4f97a2217429d29999e4b46 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 16:27:48 +0100 Subject: [PATCH 079/138] a little less anonymous listener --- assets/js/track/variant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 8c6cb1d1..5386027e 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -168,13 +168,13 @@ export class VariantTrack extends BaseAnnotationTrack { isDisplayed: false } // add context menu event listener to same virtual hitbox - virtualElement.addEventListener('click', () => { + virtualElement.addEventListener('click', function () { var url = this.scoutBaseURL + '/document_id/' + variant.id console.log(`Visit ${url}: scout variant`) var win = window.open(url, '_blank') win.focus() }, false) - virtualElement.addEventListener('dblclick', () => { + virtualElement.addEventListener('dblclick', function () { var url = this.scoutBaseURL + '/' + variant.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) window.open(url, '_blank') From 4a7a0dc948e90f7291f069d9503165bbd3f2b42a Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 8 Mar 2024 16:56:25 +0100 Subject: [PATCH 080/138] move event handler to object --- assets/js/track/variant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 5386027e..9c265ab8 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -168,13 +168,13 @@ export class VariantTrack extends BaseAnnotationTrack { isDisplayed: false } // add context menu event listener to same virtual hitbox - virtualElement.addEventListener('click', function () { + variantObj.addEventListener('click', function () { var url = this.scoutBaseURL + '/document_id/' + variant.id console.log(`Visit ${url}: scout variant`) var win = window.open(url, '_blank') win.focus() }, false) - virtualElement.addEventListener('dblclick', function () { + variantObj.addEventListener('dblclick', function () { var url = this.scoutBaseURL + '/' + variant.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) window.open(url, '_blank') From 3bff5e5e3236547c1ca146d7e49d84dce4677421 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 14:52:34 +0100 Subject: [PATCH 081/138] click and dblclick coord events, and en passant spello --- assets/js/track/annotation.js | 4 ++-- assets/js/track/base.js | 29 ++++++++++++++++++++++++++++- assets/js/track/tooltip.js | 8 ++++---- assets/js/track/transcript.js | 4 ++-- assets/js/track/variant.js | 17 ++--------------- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/assets/js/track/annotation.js b/assets/js/track/annotation.js index 237b2347..47523dbd 100644 --- a/assets/js/track/annotation.js +++ b/assets/js/track/annotation.js @@ -5,7 +5,7 @@ import { isElementOverlapping } from './utils.js' import { get } from '../fetch.js' import { parseRegionDesignation } from '../navigation.js' import { drawRect, drawText } from '../draw.js' -import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisableElementCoordinates } from './tooltip.js' +import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' import { createPopper } from '@popperjs/core' // Convert to 32bit integer @@ -176,7 +176,7 @@ export class AnnotationTrack extends BaseAnnotationTrack { open: false }) // get onscreen positions for offscreen xy coordinates - updateVisableElementCoordinates({ + updateVisibleElementCoordinates({ element: annotationObj, screenPosition: this.onscreenPosition, scale: this.offscreenPosition.scale diff --git a/assets/js/track/base.js b/assets/js/track/base.js index d9a65d68..9cd069fd 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -2,6 +2,7 @@ import { get } from '../fetch.js' import { hideTooltip } from './tooltip.js' +import { isWithinElementBbox } from './utils'; // Calculate offscreen position export function calculateOffscreenWindowPos ({ start, end, multiplier }) { @@ -110,7 +111,7 @@ export class BaseAnnotationTrack { this.trackContainer.parentElement.addEventListener('draw', (event) => { console.log('track recived draw', event.detail.region) - this.drawTrack({ ...event.detail.region }) + this.drawTrack({...event.detail.region}) }) // Setup context menu this.trackContainer.addEventListener('contextmenu', @@ -136,6 +137,32 @@ export class BaseAnnotationTrack { }) this.blitCanvas(this.onscreenPosition.start, this.onscreenPosition.end) }, false) + // add context menu event listener to same virtual hitbox + this.trackContainer.addEventListener('click', async (event) => { + for (const element of this.geneticElements) { + const rect = this.contentCanvas.getBoundingClientRect() + const point = {x: event.clientX - rect.left, y: event.clientY - rect.top} + console.log('x: ' + point.x + ' y: ' + point.y) + if (isWithinElementBbox(element.virtualElement, point)) { + var url = this.scoutBaseURL + '/document_id/' + variant.id + console.log(`Visit ${url}: scout variant`) + var win = window.open(url, '_blank') + win.focus() + } + } + }, false) + this.trackContainer.addEventListener('dblclick', async (event) => { + for (const element of this.geneticElements) { + const rect = this.contentCanvas.getBoundingClientRect() + const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } + console.log('x: ' + point.x + ' y: ' + point.y) + if (isWithinElementBbox(element.virtualElement, point)) { + var url = this.scoutBaseURL + '/' + variant.id + '/pin' + console.log(`Visit ${url}: scout PIN variant`) + window.open(url, '_blank') + } + } + }, false) } // Clears previous tracks diff --git a/assets/js/track/tooltip.js b/assets/js/track/tooltip.js index e7f34cc9..03eac1c7 100644 --- a/assets/js/track/tooltip.js +++ b/assets/js/track/tooltip.js @@ -1,7 +1,7 @@ // functions for handling tooltips import { getVisibleXCoordinates, getVisibleYCoordinates, isWithinElementBbox } from './utils.js' -// make virtual DOM element that represents a annoatation element +// make virtual DOM element that represents a annotation element export function makeVirtualDOMElement ({ x1, x2, y1, y2, canvas }) { return { getBoundingClientRect: generateGetBoundingClientRect(x1, x2, y1, y2, canvas) } } @@ -20,7 +20,7 @@ export function generateGetBoundingClientRect (x1, x2, y1, y2, canvas) { }) } -export function updateVisableElementCoordinates ({ element, screenPosition, scale }) { +export function updateVisibleElementCoordinates ({ element, screenPosition, scale }) { const { x1, x2 } = getVisibleXCoordinates({ canvas: screenPosition, feature: element, scale: scale }) const { y1, y2 } = getVisibleYCoordinates({ element }) // update coordinates @@ -148,7 +148,7 @@ function updateTooltipPos (track) { continue } // update coordinates for the main element - updateVisableElementCoordinates({ + updateVisibleElementCoordinates({ element, canvas: track.contentCanvas, screenPosition: track.onscreenPosition, @@ -156,7 +156,7 @@ function updateTooltipPos (track) { }) // update coordinates for features on element for (const feature of element.features) { - updateVisableElementCoordinates({ + updateVisibleElementCoordinates({ element: feature, canvas: track.contentCanvas, screenPosition: track.onscreenPosition, diff --git a/assets/js/track/transcript.js b/assets/js/track/transcript.js index c46b437d..a53be210 100644 --- a/assets/js/track/transcript.js +++ b/assets/js/track/transcript.js @@ -1,7 +1,7 @@ // Transcript definition import { BaseAnnotationTrack, lightenColor } from './base.js' -import { initTrackTooltips, createTooltipElement, createHtmlList, makeVirtualDOMElement, updateVisableElementCoordinates } from './tooltip.js' +import { initTrackTooltips, createTooltipElement, createHtmlList, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' import { createPopper } from '@popperjs/core' import { drawRect, drawLine, drawArrow, drawText } from '../draw.js' import { getVisibleXCoordinates, isElementOverlapping } from './utils.js' @@ -187,7 +187,7 @@ export class TranscriptTrack extends BaseAnnotationTrack { } // adapt coordinates to global screen coordinates from coorinates local to canvas - updateVisableElementCoordinates({ + updateVisibleElementCoordinates({ element: transcriptObj, screenPosition: this.onscreenPosition, scale: this.offscreenPosition.scale diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 9c265ab8..9a55ac6b 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -3,7 +3,7 @@ import { BaseAnnotationTrack } from './base.js' import { isElementOverlapping } from './utils.js' import { drawRect, drawLine, drawWaveLine, drawText } from '../draw.js' -import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisableElementCoordinates } from './tooltip.js' +import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' import { createPopper } from '@popperjs/core' // Draw variants @@ -126,7 +126,7 @@ export class VariantTrack extends BaseAnnotationTrack { tooltip: false, } // get onscreen positions for offscreen xy coordinates - updateVisableElementCoordinates({ + updateVisibleElementCoordinates({ element: variantObj, screenPosition: this.onscreenPosition, scale: this.offscreenPosition.scale @@ -167,19 +167,6 @@ export class VariantTrack extends BaseAnnotationTrack { tooltip: tooltip, isDisplayed: false } - // add context menu event listener to same virtual hitbox - variantObj.addEventListener('click', function () { - var url = this.scoutBaseURL + '/document_id/' + variant.id - console.log(`Visit ${url}: scout variant`) - var win = window.open(url, '_blank') - win.focus() - }, false) - variantObj.addEventListener('dblclick', function () { - var url = this.scoutBaseURL + '/' + variant.id + '/pin' - console.log(`Visit ${url}: scout PIN variant`) - window.open(url, '_blank') - }, false) - // virtualElement.addEventListener('contextmenu', classifyVariant, false) } this.geneticElements.push(variantObj) From 62e43e5055a02962baa248e5a2fe3d0a20e1d4dd Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 15:29:44 +0100 Subject: [PATCH 082/138] await url --- assets/js/track/base.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/track/base.js b/assets/js/track/base.js index 9cd069fd..25f4b88c 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -141,12 +141,12 @@ export class BaseAnnotationTrack { this.trackContainer.addEventListener('click', async (event) => { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() - const point = {x: event.clientX - rect.left, y: event.clientY - rect.top} + const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } console.log('x: ' + point.x + ' y: ' + point.y) if (isWithinElementBbox(element.virtualElement, point)) { var url = this.scoutBaseURL + '/document_id/' + variant.id console.log(`Visit ${url}: scout variant`) - var win = window.open(url, '_blank') + var win = await window.open(url, '_blank') win.focus() } } @@ -159,7 +159,7 @@ export class BaseAnnotationTrack { if (isWithinElementBbox(element.virtualElement, point)) { var url = this.scoutBaseURL + '/' + variant.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) - window.open(url, '_blank') + await window.open(url, '_blank') } } }, false) From 571ae687c6d83e467173902a4e8b9bb2e0467dba Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 15:49:52 +0100 Subject: [PATCH 083/138] make that a bit more variant specific --- assets/js/gens.js | 2 +- assets/js/track/base.js | 29 +---------------------------- assets/js/track/variant.js | 32 +++++++++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index d3dee942..e821ee33 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -19,7 +19,7 @@ export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiCol // Initiate interactive canvas const ic = new InteractiveCanvas(inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, scoutBaseURL, selectedVariant) + const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas diff --git a/assets/js/track/base.js b/assets/js/track/base.js index 25f4b88c..79d753d9 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -54,7 +54,7 @@ export class BaseAnnotationTrack { this.colorSchema = colorSchema // errors preventing fetching of data this.preventDrawingTrack = false - + this.scoutBaseURL = scoutBaseURL // Dimensions of track canvas this.width = Math.round(width) // Width of displayed canvas this.drawCanvasMultiplier = 4 @@ -110,7 +110,6 @@ export class BaseAnnotationTrack { this.trackTitle.style.height = this.minHeight + 'px' this.trackContainer.parentElement.addEventListener('draw', (event) => { - console.log('track recived draw', event.detail.region) this.drawTrack({...event.detail.region}) }) // Setup context menu @@ -137,32 +136,6 @@ export class BaseAnnotationTrack { }) this.blitCanvas(this.onscreenPosition.start, this.onscreenPosition.end) }, false) - // add context menu event listener to same virtual hitbox - this.trackContainer.addEventListener('click', async (event) => { - for (const element of this.geneticElements) { - const rect = this.contentCanvas.getBoundingClientRect() - const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('x: ' + point.x + ' y: ' + point.y) - if (isWithinElementBbox(element.virtualElement, point)) { - var url = this.scoutBaseURL + '/document_id/' + variant.id - console.log(`Visit ${url}: scout variant`) - var win = await window.open(url, '_blank') - win.focus() - } - } - }, false) - this.trackContainer.addEventListener('dblclick', async (event) => { - for (const element of this.geneticElements) { - const rect = this.contentCanvas.getBoundingClientRect() - const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('x: ' + point.x + ' y: ' + point.y) - if (isWithinElementBbox(element.virtualElement, point)) { - var url = this.scoutBaseURL + '/' + variant.id + '/pin' - console.log(`Visit ${url}: scout PIN variant`) - await window.open(url, '_blank') - } - } - }, false) } // Clears previous tracks diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 9a55ac6b..a89be2d8 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -1,7 +1,7 @@ // Variant track definition import { BaseAnnotationTrack } from './base.js' -import { isElementOverlapping } from './utils.js' +import {isElementOverlapping, isWithinElementBbox} from './utils.js' import { drawRect, drawLine, drawWaveLine, drawText } from '../draw.js' import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' import { createPopper } from '@popperjs/core' @@ -22,8 +22,35 @@ export class VariantTrack extends BaseAnnotationTrack { this.contentCanvas = document.getElementById('variant-content') this.trackTitle = document.getElementById('variant-titles') this.trackContainer = document.getElementById('variant-track-container') + this.scoutBaseURL = scoutBaseURL + // add context menu event listener to same virtual hitbox + this.trackContainer.addEventListener('click', async (event) => { + for (const element of this.geneticElements) { + const rect = this.contentCanvas.getBoundingClientRect() + const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } + console.log('x: ' + point.x + ' y: ' + point.y) + if (isWithinElementBbox(element.virtualElement, point)) { + console.log('In bounding box ') + var url = this.scoutBaseURL + '/document_id/' + element.id + console.log(`Visit ${url}: scout variant`) + var win = await window.open(url, '_blank') + win.focus() + } + } + }, false) + this.trackContainer.addEventListener('dblclick', async (event) => { + for (const element of this.geneticElements) { + const rect = this.contentCanvas.getBoundingClientRect() + const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } + console.log('x: ' + point.x + ' y: ' + point.y) + if (isWithinElementBbox(element.virtualElement, point)) { + var url = this.scoutBaseURL + '/' + element.id + '/pin' + console.log(`Visit ${url}: scout PIN variant`) + await window.open(url, '_blank') + } + } + }, false) this.featureHeight = 18 - // Setup html objects now that we have gotten the canvas and div elements this.setupHTML(x + 1) @@ -36,7 +63,6 @@ export class VariantTrack extends BaseAnnotationTrack { variant_category: 'sv', case_id: caseId } - this.scoutBaseURL = scoutBaseURL // Initialize highlighted variant this.highlightedVariantId = highlightedVariantId initTrackTooltips(this) From 74f6fdd0f4181775152818df7a0d476c991e7cbb Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 15:56:16 +0100 Subject: [PATCH 084/138] moved most.. --- assets/js/track/base.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/track/base.js b/assets/js/track/base.js index 79d753d9..0fd5573f 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -54,7 +54,6 @@ export class BaseAnnotationTrack { this.colorSchema = colorSchema // errors preventing fetching of data this.preventDrawingTrack = false - this.scoutBaseURL = scoutBaseURL // Dimensions of track canvas this.width = Math.round(width) // Width of displayed canvas this.drawCanvasMultiplier = 4 From 2b91c602c362a681be3dd1888aae7c0ae22331b4 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 16:34:10 +0100 Subject: [PATCH 085/138] Merge --- assets/js/track/variant.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index a89be2d8..4f671de0 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -28,8 +28,8 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('x: ' + point.x + ' y: ' + point.y) - if (isWithinElementBbox(element.virtualElement, point)) { + console.log('click x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) + if (isWithinElementBbox(element.tooltip.virtualElement, point)) { console.log('In bounding box ') var url = this.scoutBaseURL + '/document_id/' + element.id console.log(`Visit ${url}: scout variant`) @@ -42,8 +42,8 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('x: ' + point.x + ' y: ' + point.y) - if (isWithinElementBbox(element.virtualElement, point)) { + console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) + if (isWithinElementBbox(element.tooltip.virtualElement, point)) { var url = this.scoutBaseURL + '/' + element.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) await window.open(url, '_blank') From 309c5d8d4359871aa49848222f5877fe217ee303 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 17:15:26 +0100 Subject: [PATCH 086/138] curlies --- assets/js/track/base.js | 1 - assets/js/track/variant.js | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assets/js/track/base.js b/assets/js/track/base.js index 0fd5573f..c7b04ea0 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -2,7 +2,6 @@ import { get } from '../fetch.js' import { hideTooltip } from './tooltip.js' -import { isWithinElementBbox } from './utils'; // Calculate offscreen position export function calculateOffscreenWindowPos ({ start, end, multiplier }) { diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 4f671de0..e9c38dc3 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -29,11 +29,12 @@ export class VariantTrack extends BaseAnnotationTrack { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } console.log('click x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) - if (isWithinElementBbox(element.tooltip.virtualElement, point)) { + const ve = element.tooltip.virtualElement + if (isWithinElementBbox({ ve, point })) { console.log('In bounding box ') - var url = this.scoutBaseURL + '/document_id/' + element.id + const url = this.scoutBaseURL + '/document_id/' + element.id console.log(`Visit ${url}: scout variant`) - var win = await window.open(url, '_blank') + const win = await window.open(url, '_blank') win.focus() } } @@ -43,8 +44,9 @@ export class VariantTrack extends BaseAnnotationTrack { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) - if (isWithinElementBbox(element.tooltip.virtualElement, point)) { - var url = this.scoutBaseURL + '/' + element.id + '/pin' + ve = element.tooltip.virtualElement + if (isWithinElementBbox( {ve, point })) { + const url = this.scoutBaseURL + '/' + element.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) await window.open(url, '_blank') } From 171d44d7cdcd667848f51701007eb25bf3543e0a Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 17:24:49 +0100 Subject: [PATCH 087/138] test for existence --- assets/js/track/variant.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index e9c38dc3..c758da63 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -28,8 +28,10 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('click x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) const ve = element.tooltip.virtualElement + if (ve) { + console.log('click x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + ve.x1 + ' vE x2:' + ve.x2) + } if (isWithinElementBbox({ ve, point })) { console.log('In bounding box ') const url = this.scoutBaseURL + '/document_id/' + element.id @@ -43,9 +45,11 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + element.tooltip.virtualElement.x1 + ' vE x2:' + element.tooltip.virtualElement.x2) - ve = element.tooltip.virtualElement - if (isWithinElementBbox( {ve, point })) { + const ve = element.tooltip.virtualElement + if (element.tooltip) { + console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + ve.x1 + ' vE x2:' + ve.x2) + } + if (isWithinElementBbox({ ve, point })) { const url = this.scoutBaseURL + '/' + element.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) await window.open(url, '_blank') From 68e872be8f7b1e011650dc3f89e5671a36edd11b Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 17:42:46 +0100 Subject: [PATCH 088/138] debug.. --- assets/js/track/tooltip.js | 1 - assets/js/track/variant.js | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/assets/js/track/tooltip.js b/assets/js/track/tooltip.js index 03eac1c7..ba3fef52 100644 --- a/assets/js/track/tooltip.js +++ b/assets/js/track/tooltip.js @@ -6,7 +6,6 @@ export function makeVirtualDOMElement ({ x1, x2, y1, y2, canvas }) { return { getBoundingClientRect: generateGetBoundingClientRect(x1, x2, y1, y2, canvas) } } - // Make a virtual DOM element from a genetic element object export function generateGetBoundingClientRect (x1, x2, y1, y2, canvas) { const track = canvas diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index c758da63..24e8b9b5 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -45,11 +45,10 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - const ve = element.tooltip.virtualElement - if (element.tooltip) { - console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + ve.x1 + ' vE x2:' + ve.x2) + if (element.id) { + console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) } - if (isWithinElementBbox({ ve, point })) { + if (isWithinElementBbox({ element, point })) { const url = this.scoutBaseURL + '/' + element.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) await window.open(url, '_blank') From dcf154c95961af814eba1363bf46920a22ceeabc Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 17:54:33 +0100 Subject: [PATCH 089/138] re-pass scoutBaseURL --- assets/js/gens.js | 2 +- assets/js/track/variant.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/assets/js/gens.js b/assets/js/gens.js index e821ee33..d3dee942 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -19,7 +19,7 @@ export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiCol // Initiate interactive canvas const ic = new InteractiveCanvas(inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, selectedVariant) + const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, scoutBaseURL, selectedVariant) const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) // Initiate and draw overview canvas diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 24e8b9b5..7ab97d68 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -28,11 +28,10 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - const ve = element.tooltip.virtualElement - if (ve) { - console.log('click x: ' + point.x + ' y: ' + point.y + ' vE x1: ' + ve.x1 + ' vE x2:' + ve.x2) + if (element.id) { + console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) } - if (isWithinElementBbox({ ve, point })) { + if (isWithinElementBbox({ element, point })) { console.log('In bounding box ') const url = this.scoutBaseURL + '/document_id/' + element.id console.log(`Visit ${url}: scout variant`) From 89ae5791c78962a0bd587d9968d57255e496f294 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Mon, 11 Mar 2024 18:12:25 +0100 Subject: [PATCH 090/138] use document id.. --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 7ab97d68..5fc8c492 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -143,7 +143,7 @@ export class VariantTrack extends BaseAnnotationTrack { // create variant object const featureHeight = variantCategory === 'del' ? 7 : 8 const variantObj = { - id: variant.variant_id, + id: variant.document_id, name: variant.display_name, start: variant.position, end: variant.end, From 89f60eaf1c15aebeeb34af08c816276556148dca Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 13 Mar 2024 14:03:21 +0100 Subject: [PATCH 091/138] correct log --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 5fc8c492..4ea1e447 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -29,7 +29,7 @@ export class VariantTrack extends BaseAnnotationTrack { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } if (element.id) { - console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) + console.log('click x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) } if (isWithinElementBbox({ element, point })) { console.log('In bounding box ') From 130936670dee8468cc10591cc25448aca824dacd Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Tue, 19 Mar 2024 08:54:55 +0100 Subject: [PATCH 092/138] package.json conflict --- assets/js/track/utils.js | 4 ++++ assets/js/track/variant.js | 21 ++++++++------------- package.json | 2 +- setup.py | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/assets/js/track/utils.js b/assets/js/track/utils.js index 9b878b0e..621676a8 100644 --- a/assets/js/track/utils.js +++ b/assets/js/track/utils.js @@ -39,3 +39,7 @@ export function isElementOverlapping (first, second) { export function isWithinElementBbox ({ element, point }) { return (element.x1 < point.x && point.x < element.x2) && (element.y1 < point.y && point.y < element.y2) } + +export function isWithinElementVisibleBbox ({ element, point }) { + return (element.visibleX1 < point.x && point.x < element.visibleX2) && (element.visibleY1 < point.y && point.y < element.visibleY2) +} diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 4ea1e447..5214a3de 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -1,7 +1,7 @@ // Variant track definition import { BaseAnnotationTrack } from './base.js' -import {isElementOverlapping, isWithinElementBbox} from './utils.js' +import { isElementOverlapping, isWithinElementBbox, isWithinElementVisibleBbox } from './utils.js' import { drawRect, drawLine, drawWaveLine, drawText } from '../draw.js' import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' import { createPopper } from '@popperjs/core' @@ -27,15 +27,11 @@ export class VariantTrack extends BaseAnnotationTrack { this.trackContainer.addEventListener('click', async (event) => { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() - const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - if (element.id) { - console.log('click x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) - } - if (isWithinElementBbox({ element, point })) { - console.log('In bounding box ') + const point = { x: (event.clientX - rect.left), y: event.clientY - rect.top } + if (isWithinElementVisibleBbox({ element, point })) { const url = this.scoutBaseURL + '/document_id/' + element.id console.log(`Visit ${url}: scout variant`) - const win = await window.open(url, '_blank') + const win = window.open(url, '_blank') win.focus() } } @@ -44,10 +40,7 @@ export class VariantTrack extends BaseAnnotationTrack { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - if (element.id) { - console.log('dblclick x: ' + point.x + ' y: ' + point.y + ' E x1: ' + element.x1 + ' E x2:' + element.x2 + ' vis x1: ' + element.visibleX1 + ' vis x2:' + element.visibleX2) - } - if (isWithinElementBbox({ element, point })) { + if (isWithinElementVisibleBbox({ element, point })) { const url = this.scoutBaseURL + '/' + element.id + '/pin' console.log(`Visit ${url}: scout PIN variant`) await window.open(url, '_blank') @@ -198,7 +191,9 @@ export class VariantTrack extends BaseAnnotationTrack { isDisplayed: false } } - this.geneticElements.push(variantObj) + if (['dup', 'del', 'cnv'].includes(variantCategory)) { + this.geneticElements.push(variantObj) + } // Keep track of latest track if (this.heightOrderRecord.latestHeight !== heightOrder) { diff --git a/package.json b/package.json index 67155807..7fe17106 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "tippy.js": "^6.3.2" }, "name": "gens", - "version": "2.1.2", + "version": "2.2.0", "description": "Interactive tool to visualize genomic copy number profiles from WGS data", "main": "gens.js", "scripts": { diff --git a/setup.py b/setup.py index b161e805..662b2d32 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="gens", - version="2.1.1", + version="2.2", description="Gens is a web-based interactive tool to visualize genomic copy number profiles from WGS data.", license="MIT", author="Ronja, Markus Johansson", From b38082d7bd3d9a7618ed125e28725e63fef9141a Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 20 Mar 2024 14:16:04 +0100 Subject: [PATCH 093/138] lets wait with pinning and classifying - this is a lot already --- assets/js/track/variant.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 5214a3de..1fe42e87 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -23,30 +23,19 @@ export class VariantTrack extends BaseAnnotationTrack { this.trackTitle = document.getElementById('variant-titles') this.trackContainer = document.getElementById('variant-track-container') this.scoutBaseURL = scoutBaseURL - // add context menu event listener to same virtual hitbox + // Add click menu event listener linking out to the Scout variant this.trackContainer.addEventListener('click', async (event) => { for (const element of this.geneticElements) { const rect = this.contentCanvas.getBoundingClientRect() const point = { x: (event.clientX - rect.left), y: event.clientY - rect.top } if (isWithinElementVisibleBbox({ element, point })) { const url = this.scoutBaseURL + '/document_id/' + element.id - console.log(`Visit ${url}: scout variant`) + console.log(`Visit ${url}: Scout variant`) const win = window.open(url, '_blank') win.focus() } } }, false) - this.trackContainer.addEventListener('dblclick', async (event) => { - for (const element of this.geneticElements) { - const rect = this.contentCanvas.getBoundingClientRect() - const point = { x: event.clientX - rect.left, y: event.clientY - rect.top } - if (isWithinElementVisibleBbox({ element, point })) { - const url = this.scoutBaseURL + '/' + element.id + '/pin' - console.log(`Visit ${url}: scout PIN variant`) - await window.open(url, '_blank') - } - } - }, false) this.featureHeight = 18 // Setup html objects now that we have gotten the canvas and div elements this.setupHTML(x + 1) From da4427baffbdccf39bbf50d4cc00688612b1a20c Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 10:18:33 +0100 Subject: [PATCH 094/138] Bump version to 2.3 --- CHANGELOG.md | 2 +- gens/__version__.py | 2 +- package-lock.json | 4 ++-- package.json | 2 +- setup.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 999323b2..c84d9f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/) About changelog [here](https://keepachangelog.com/en/1.0.0/) -## [unreleased] +## [2.3] ### Added - Link out to Scout: introduce config variable for base URL - Link out to Scout: case links on home sample list diff --git a/gens/__version__.py b/gens/__version__.py index 64a5af4d..2f5c4285 100644 --- a/gens/__version__.py +++ b/gens/__version__.py @@ -1 +1 @@ -VERSION = "2.2" +VERSION = "2.3" diff --git a/package-lock.json b/package-lock.json index 2601a2bd..a61530e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gens", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gens", - "version": "2.2.0", + "version": "2.3.0", "dependencies": { "@popperjs/core": "^2.9.2", "npm": "^10.2.3", diff --git a/package.json b/package.json index 7fe17106..66103eb8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "tippy.js": "^6.3.2" }, "name": "gens", - "version": "2.2.0", + "version": "2.3.0", "description": "Interactive tool to visualize genomic copy number profiles from WGS data", "main": "gens.js", "scripts": { diff --git a/setup.py b/setup.py index 662b2d32..acb5b2dc 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="gens", - version="2.2", + version="2.3", description="Gens is a web-based interactive tool to visualize genomic copy number profiles from WGS data.", license="MIT", author="Ronja, Markus Johansson", From 86acb2d0092f42e8ea82c36de649a323fa1e009a Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 27 Mar 2024 14:15:21 +0100 Subject: [PATCH 095/138] fix docker push --- .github/workflows/docker_push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml index d165f2e9..654886ab 100644 --- a/.github/workflows/docker_push.yml +++ b/.github/workflows/docker_push.yml @@ -6,7 +6,7 @@ on: - master jobs: - docker-stage-push: + docker-push: name: Create docker image runs-on: ubuntu-latest steps: @@ -33,4 +33,4 @@ jobs: context: ./ file: ./Dockerfile push: true - tags: "clinicalgenomics/gens:${{ github.event.release.tag_name }}, ${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens:latest" + tags: "clinicalgenomics/gens:${{ github.event.release.tag_name }}, clinicalgenomics/gens:latest" From 8dd8683b347c6d3335b758084ccf6602771da894 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 27 Mar 2024 14:22:13 +0100 Subject: [PATCH 096/138] docker preproc push as well --- .github/workflows/preproc_docker_push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index b86d9887..8a23a270 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -33,4 +33,4 @@ jobs: context: ./utils/ file: ./utils/Dockerfile push: true - tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc:${{steps.branch-name.outputs.current_branch}}, clinicalgenomics/gens-preproc:latest" + tags: "clinicalgenomics/gens-preproc:${{ github.event.release.tag_name }}, clinicalgenomics/gens-preproc:latest" From b6cb1a131bcf2feb112b33eef84ede761645c0aa Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 10:20:15 +0100 Subject: [PATCH 097/138] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c84d9f91..b438333b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Archive prod docker image with release tag name. Update action versions. ### Fixed - Error image background static path +- GitHub action DockerHub push ## [2.2] ### Added From 65b8e8f1e1e87398b5b932d79645b7ebc609c0c4 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Wed, 27 Mar 2024 14:27:18 +0100 Subject: [PATCH 098/138] again... --- .github/workflows/docker_push.yml | 6 +++--- .github/workflows/preproc_docker_push.yml | 6 +++--- CHANGELOG.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker_push.yml b/.github/workflows/docker_push.yml index 654886ab..f22ef682 100644 --- a/.github/workflows/docker_push.yml +++ b/.github/workflows/docker_push.yml @@ -1,9 +1,9 @@ name: Publish to Docker on: - push: - branches: - - master + release: + types: + - created jobs: docker-push: diff --git a/.github/workflows/preproc_docker_push.yml b/.github/workflows/preproc_docker_push.yml index 8a23a270..6fddd14c 100644 --- a/.github/workflows/preproc_docker_push.yml +++ b/.github/workflows/preproc_docker_push.yml @@ -1,9 +1,9 @@ name: Publish preproc to Docker on: - push: - branches: - - master + release: + types: + - created jobs: docker-preproc-push: diff --git a/CHANGELOG.md b/CHANGELOG.md index b438333b..50243b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Archive prod docker image with release tag name. Update action versions. ### Fixed - Error image background static path -- GitHub action DockerHub push +- GitHub action DockerHub push on release ## [2.2] ### Added From 9067974ee643a3ced209544471d0bdc2915b12e1 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 30 May 2024 09:02:07 +0200 Subject: [PATCH 099/138] Makes `genomeBuild` get passed on in `drawTrack` --- assets/js/navigation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/navigation.js b/assets/js/navigation.js index d0ace2e9..0b8164c0 100644 --- a/assets/js/navigation.js +++ b/assets/js/navigation.js @@ -98,7 +98,7 @@ export async function drawTrack({ exclude = [], force = false, ...kwargs }) { // update input field - const region = await limitRegionToChromosome({ chrom, start, end }) + const region = await limitRegionToChromosome({ chrom, start, end, genomeBuild }) updateInputField({ ...region }) const trackContainer = document.getElementById('visualization-container') trackContainer.dispatchEvent( From 91c52b3be207054ba79be66c6863fc8f3b3bbc23 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 30 May 2024 10:52:49 +0200 Subject: [PATCH 100/138] adds to changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50243b5c..b61d0732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) About changelog [here](https://keepachangelog.com/en/1.0.0/) +## [Unreleased] +### Fixed +- Pan able to exit chrosome when using genome build 17 + ## [2.3] ### Added - Link out to Scout: introduce config variable for base URL From 9427bbf07bcec7dec5bd4d1d0b18bde5080042d5 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 30 May 2024 14:44:56 +0200 Subject: [PATCH 101/138] Reraises DuplicateKeyError when storing sample, and better error message --- gens/db/samples.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index 97f921e4..2cc3de4c 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -37,7 +37,8 @@ def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview): } ) except DuplicateKeyError: - LOG.warning(f'DuplicateKeyError while storing sample "{sample_id}" in database, skipping.', exc_info=True) + LOG.error(f'DuplicateKeyError while storing sample with sample_id="{sample_id}" and case_id="{case_id}" in database.') + raise def get_samples(db, start=0, n_samples=None): From ecccecffee92722f1d3a9a5d7436a1a915590e3f Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 30 May 2024 14:48:18 +0200 Subject: [PATCH 102/138] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61d0732..358036d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Fixed - Pan able to exit chrosome when using genome build 17 +- No longer effectively silently fails to upload samples with a duplicate key ## [2.3] ### Added From 519e184abdc9b6a83149d36b95c2c6d2d7dc8593 Mon Sep 17 00:00:00 2001 From: raysloks Date: Wed, 5 Jun 2024 13:37:54 +0200 Subject: [PATCH 103/138] Adds nonfunctional --force flag --- gens/commands/load.py | 8 +++++++- gens/db/samples.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/gens/commands/load.py b/gens/commands/load.py index 0a97138e..fc79f796 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -58,8 +58,13 @@ def load(): type=click.Path(exists=True), help="Json file that contains preprocessed overview coverage", ) +@click.option( + "--force", + is_flag=True, + help="Overwrite any existing sample with the same key.", +) @with_appcontext -def sample(sample_id, genome_build, baf, coverage, case_id, overview_json): +def sample(sample_id, genome_build, baf, coverage, case_id, overview_json, force): """Load a sample into Gens database.""" db = app.config["GENS_DB"] # if collection is not indexed, crate index @@ -74,6 +79,7 @@ def sample(sample_id, genome_build, baf, coverage, case_id, overview_json): baf=baf, coverage=coverage, overview=overview_json, + force=force, ) click.secho("Finished adding a new sample to database ✔", fg="green") diff --git a/gens/db/samples.py b/gens/db/samples.py index 2cc3de4c..e2aefc1f 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -21,7 +21,7 @@ def __init__(self, message, sample_id): self.sample_id = sample_id -def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview): +def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, force): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') try: From 8e1c0e09ee725ea76b696055bcdc72244e244719 Mon Sep 17 00:00:00 2001 From: raysloks Date: Fri, 7 Jun 2024 10:19:01 +0200 Subject: [PATCH 104/138] Adds --force flag functionality --- gens/db/samples.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index e2aefc1f..f2fa6a61 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -24,21 +24,45 @@ def __init__(self, message, sample_id): def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, force): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') - try: - db[COLLECTION].insert_one( + if force: + db[COLLECTION].update_one( + filter= { "sample_id": sample_id, "case_id": case_id, - "baf_file": baf, - "coverage_file": coverage, - "overview_file": overview, "genome_build": genome_build, - "created_at": datetime.datetime.now(), - } + }, + new_values= + { + "$set": + { + "sample_id": sample_id, + "case_id": case_id, + "baf_file": baf, + "coverage_file": coverage, + "overview_file": overview, + "genome_build": genome_build, + "created_at": datetime.datetime.now(), + } + }, + upsert=True ) - except DuplicateKeyError: - LOG.error(f'DuplicateKeyError while storing sample with sample_id="{sample_id}" and case_id="{case_id}" in database.') - raise + else: + try: + db[COLLECTION].insert_one( + { + "sample_id": sample_id, + "case_id": case_id, + "baf_file": baf, + "coverage_file": coverage, + "overview_file": overview, + "genome_build": genome_build, + "created_at": datetime.datetime.now(), + } + ) + except DuplicateKeyError: + LOG.error(f'DuplicateKeyError while storing sample with sample_id="{sample_id}" and case_id="{case_id}" in database.') + raise def get_samples(db, start=0, n_samples=None): From 46e8d9c2b933d520794d78c42cebdac81baa8d93 Mon Sep 17 00:00:00 2001 From: raysloks Date: Fri, 7 Jun 2024 10:19:36 +0200 Subject: [PATCH 105/138] Disables re-raising of DuplicateKeyError --- gens/db/samples.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index f2fa6a61..365d7115 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -62,7 +62,6 @@ def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, ) except DuplicateKeyError: LOG.error(f'DuplicateKeyError while storing sample with sample_id="{sample_id}" and case_id="{case_id}" in database.') - raise def get_samples(db, start=0, n_samples=None): From d773eae3162a04f9d964088af993e1d649d5f238 Mon Sep 17 00:00:00 2001 From: raysloks Date: Fri, 7 Jun 2024 10:25:56 +0200 Subject: [PATCH 106/138] Updates changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358036d1..8a649933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added +- `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. ### Fixed - Pan able to exit chrosome when using genome build 17 -- No longer effectively silently fails to upload samples with a duplicate key ## [2.3] ### Added From 0b9b5f8616d7bb8dc232e042a673514ecaed461c Mon Sep 17 00:00:00 2001 From: raysloks Date: Fri, 28 Jun 2024 09:59:05 +0200 Subject: [PATCH 107/138] prints warning to stderr when overwriting old entry --- gens/db/samples.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index 365d7115..0a4451e5 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -21,11 +21,20 @@ def __init__(self, message, sample_id): self.sample_id = sample_id +class NonUniqueIndexError(Exception): + def __init__(self, message, sample_id, case_id, genome_build): + super().__init__(message) + + self.sample_id = sample_id + self.case_id = case_id + self.genome_build = genome_build + + def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, force): """Store a new sample in the database.""" LOG.info(f'Store sample "{sample_id}" in database') if force: - db[COLLECTION].update_one( + result = db[COLLECTION].update_one( filter= { "sample_id": sample_id, @@ -47,6 +56,10 @@ def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, }, upsert=True ) + if result.modified_count == 1: + LOG.error(f'Sample with sample_id="{sample_id}" and case_id="{case_id}" was overwritten.') + if result.modified_count > 1: + raise NonUniqueIndexError(f'More than one entry matched sample_id="{sample_id}", case_id="{case_id}", and genome_build="{genome_build}". This should never happen.', sample_id, case_id, genome_build) else: try: db[COLLECTION].insert_one( From e169c4955c8b362a4fd870868c79970b1644c0f0 Mon Sep 17 00:00:00 2001 From: raysloks Date: Fri, 28 Jun 2024 10:03:00 +0200 Subject: [PATCH 108/138] updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a649933..dba913b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Added - `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. +- `--force` flag prints a warning to stderr when overwriting an existing sample. ### Fixed - Pan able to exit chrosome when using genome build 17 From 73bd596408f4d155b6a609bc745a187afc470bc2 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 28 Jun 2024 13:11:17 +0200 Subject: [PATCH 109/138] Fixes update_one call --- gens/db/samples.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gens/db/samples.py b/gens/db/samples.py index 0a4451e5..ecfb95b8 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -35,13 +35,11 @@ def store_sample(db, sample_id, case_id, genome_build, baf, coverage, overview, LOG.info(f'Store sample "{sample_id}" in database') if force: result = db[COLLECTION].update_one( - filter= { "sample_id": sample_id, "case_id": case_id, "genome_build": genome_build, }, - new_values= { "$set": { From 5c234a8d2f8f73795802a1d9b0e274fa8b4f1870 Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 28 Jun 2024 13:13:41 +0200 Subject: [PATCH 110/138] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dba913b9..2db640b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - `--force` flag prints a warning to stderr when overwriting an existing sample. ### Fixed - Pan able to exit chrosome when using genome build 17 +- `--force` flag `update_one` call not being called properly ## [2.3] ### Added From 804c074057ae2134162b062de8a13e6c4911888a Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 8 Jul 2024 12:54:07 +0200 Subject: [PATCH 111/138] Merge --- gens/db/samples.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gens/db/samples.py b/gens/db/samples.py index ecfb95b8..d3690196 100644 --- a/gens/db/samples.py +++ b/gens/db/samples.py @@ -120,3 +120,15 @@ def query_sample(db, sample_id, case_id, genome_build): overview_file=result["overview_file"], created_at=result["created_at"], ) + + +def delete_sample(db, sample_id, case_id, genome_build): + """Remove a sample from the database.""" + LOG.info(f'Removing sample "{sample_id}" from database') + db[COLLECTION].delete_one( + { + "sample_id": sample_id, + "case_id": case_id, + "genome_build": genome_build, + } + ) From 80e7c3becd75fdbbd8878d4a22444469e1c0845d Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 10 Jul 2024 10:43:42 +0200 Subject: [PATCH 112/138] Updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db640b6..893ac83b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Added - `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. - `--force` flag prints a warning to stderr when overwriting an existing sample. +- `gens delete sample` command ### Fixed - Pan able to exit chrosome when using genome build 17 - `--force` flag `update_one` call not being called properly From 9997b81c2e2bcfef759b6ce98a0a3d42a187a2a7 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 10 Jul 2024 10:52:59 +0200 Subject: [PATCH 113/138] Fixes "# samples loaded into gens" --- gens/blueprints/home/templates/home.html | 2 +- gens/blueprints/home/views.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 4078858d..3b16e5cd 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -20,7 +20,7 @@ {% block content %}

Samples

-

There are {{ samples | length }} samles loaded into Gens. Click on a sample_id to open it

+

There are {{ total_samples }} samples loaded into Gens. Click on a sample_id to open it

{{ samples_table(samples, pagination) }}
diff --git a/gens/blueprints/home/views.py b/gens/blueprints/home/views.py index bb2864b2..84ea4098 100644 --- a/gens/blueprints/home/views.py +++ b/gens/blueprints/home/views.py @@ -39,16 +39,16 @@ def home(): # set pagination page = request.args.get("page", 1, type=int) start = (page - 1) * SAMPLES_PER_PAGE - samples, tot_samples = get_samples(db, start=start, n_samples=SAMPLES_PER_PAGE) + samples, total_samples = get_samples(db, start=start, n_samples=SAMPLES_PER_PAGE) # calculate pagination pagination_info = { "from": start + 1, "to": start + SAMPLES_PER_PAGE, "current_page": page, "last_page": ( - tot_samples // SAMPLES_PER_PAGE - if tot_samples % SAMPLES_PER_PAGE == 0 - else (tot_samples // SAMPLES_PER_PAGE) + 1 + total_samples // SAMPLES_PER_PAGE + if total_samples % SAMPLES_PER_PAGE == 0 + else (total_samples // SAMPLES_PER_PAGE) + 1 ), } # parse samples @@ -68,6 +68,7 @@ def home(): "home.html", pagination=pagination_info, samples=samples, + total_samples=total_samples, scout_base_url=current_app.config.get("SCOUT_BASE_URL"), version=version, ) From 13aa550fc80ce1fcb9ca8e60ee87d60b2962a759 Mon Sep 17 00:00:00 2001 From: Emil Bertilsson Date: Fri, 12 Jul 2024 09:27:29 +0200 Subject: [PATCH 114/138] Updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893ac83b..2a18c31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Fixed - Pan able to exit chrosome when using genome build 17 - `--force` flag `update_one` call not being called properly +- Incorrect total sample count on home page. ## [2.3] ### Added From 1e6693ec1f3af815570880272ffd909b2027ec49 Mon Sep 17 00:00:00 2001 From: Emil Bertilsson Date: Mon, 15 Jul 2024 10:42:50 +0200 Subject: [PATCH 115/138] Merge --- assets/js/track/variant.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 1fe42e87..1dec84a9 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -116,7 +116,12 @@ export class VariantTrack extends BaseAnnotationTrack { const variantType = variant.variant_type const variantLength = variant.length const color = this.colorSchema[variantCategory] || this.colorSchema.default || 'black' - const heightOrder = 1 + + let heightOrder = 1 + while (heightTracker[heightOrder] >= variant.start) + heightOrder += 1 + heightTracker[heightOrder] = variant.end + const canvasYPos = this.tracksYPos(heightOrder) // Only draw visible tracks From da97292bc20acf352b235e55fc83c1687b6c0ca3 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 15 Jul 2024 12:30:46 +0200 Subject: [PATCH 116/138] Merge --- assets/js/track/variant.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 1dec84a9..e406d275 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -109,6 +109,9 @@ export class VariantTrack extends BaseAnnotationTrack { } this.clearTracks() + const heightTracker = Array(200) + let actualMaxHeightOrder = 1 + // Draw track const drawTooltips = this.getResolution < 4 for (const variant of filteredVariants) { @@ -121,6 +124,7 @@ export class VariantTrack extends BaseAnnotationTrack { while (heightTracker[heightOrder] >= variant.start) heightOrder += 1 heightTracker[heightOrder] = variant.end + actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) const canvasYPos = this.tracksYPos(heightOrder) @@ -260,5 +264,6 @@ export class VariantTrack extends BaseAnnotationTrack { }) } } + this.trackData.max_height_order = actualMaxHeightOrder } } From 839d1539aad1cb178401227c4c59c505891f5bd6 Mon Sep 17 00:00:00 2001 From: Emil Date: Mon, 15 Jul 2024 12:33:16 +0200 Subject: [PATCH 117/138] Updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a18c31d..67cd354a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. - `--force` flag prints a warning to stderr when overwriting an existing sample. - `gens delete sample` command +- Height ordering for variants track. ### Fixed - Pan able to exit chrosome when using genome build 17 - `--force` flag `update_one` call not being called properly From 64cc60e014070789b67e9805bfc73fe43810b8eb Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 10:06:42 +0200 Subject: [PATCH 118/138] Changes start to position --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index e406d275..43bacdbe 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -121,7 +121,7 @@ export class VariantTrack extends BaseAnnotationTrack { const color = this.colorSchema[variantCategory] || this.colorSchema.default || 'black' let heightOrder = 1 - while (heightTracker[heightOrder] >= variant.start) + while (heightTracker[heightOrder] >= variant.position) heightOrder += 1 heightTracker[heightOrder] = variant.end actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) From 9b3fc508cd4dbc9a2ebfec1f0817a4fb052699af Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 10:33:03 +0200 Subject: [PATCH 119/138] Sorts filteredVariants to maybe fix faulty height stacking --- assets/js/track/variant.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 43bacdbe..962f119f 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -99,6 +99,8 @@ export class VariantTrack extends BaseAnnotationTrack { { start: variant.position, end: variant.end }, { start: startPos, end: endPos })) } + filteredVariants.sort((a, b) => a.position - b.position) + // dont show tracks with no data in them if (filteredVariants.length > 0 && this.getResolution < this.maxResolution + 1 From 85a9206712f83c49cc5d72abe2a3a83434b9cc0c Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 11:00:02 +0200 Subject: [PATCH 120/138] Does proper calculation of max height order before rendering --- assets/js/track/variant.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 962f119f..6b070bea 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -101,6 +101,18 @@ export class VariantTrack extends BaseAnnotationTrack { } filteredVariants.sort((a, b) => a.position - b.position) + let heightTracker = Array(200) + let actualMaxHeightOrder = 1 + for (const variant of filteredVariants) { + let heightOrder = 1 + while (heightTracker[heightOrder] >= variant.position) + heightOrder += 1 + heightTracker[heightOrder] = variant.end + actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) + } + + this.trackData.max_height_order = actualMaxHeightOrder + // dont show tracks with no data in them if (filteredVariants.length > 0 && this.getResolution < this.maxResolution + 1 @@ -111,8 +123,7 @@ export class VariantTrack extends BaseAnnotationTrack { } this.clearTracks() - const heightTracker = Array(200) - let actualMaxHeightOrder = 1 + heightTracker = Array(200) // Draw track const drawTooltips = this.getResolution < 4 @@ -126,7 +137,6 @@ export class VariantTrack extends BaseAnnotationTrack { while (heightTracker[heightOrder] >= variant.position) heightOrder += 1 heightTracker[heightOrder] = variant.end - actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) const canvasYPos = this.tracksYPos(heightOrder) @@ -266,6 +276,5 @@ export class VariantTrack extends BaseAnnotationTrack { }) } } - this.trackData.max_height_order = actualMaxHeightOrder } } From 3337a029e747d30cf116b76e38c826da676c9a14 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 11:09:28 +0200 Subject: [PATCH 121/138] Limits height stacking to supported variant categories --- assets/js/track/variant.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 6b070bea..b677d42b 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -104,11 +104,14 @@ export class VariantTrack extends BaseAnnotationTrack { let heightTracker = Array(200) let actualMaxHeightOrder = 1 for (const variant of filteredVariants) { - let heightOrder = 1 - while (heightTracker[heightOrder] >= variant.position) - heightOrder += 1 - heightTracker[heightOrder] = variant.end - actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) + const variantCategory = variant.sub_category // del, dup, sv, str + if (['dup', 'del', 'cnv'].includes(variantCategory)) { + let heightOrder = 1 + while (heightTracker[heightOrder] >= variant.position) + heightOrder += 1 + heightTracker[heightOrder] = variant.end + actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) + } } this.trackData.max_height_order = actualMaxHeightOrder @@ -134,9 +137,11 @@ export class VariantTrack extends BaseAnnotationTrack { const color = this.colorSchema[variantCategory] || this.colorSchema.default || 'black' let heightOrder = 1 - while (heightTracker[heightOrder] >= variant.position) - heightOrder += 1 - heightTracker[heightOrder] = variant.end + if (['dup', 'del', 'cnv'].includes(variantCategory)) { + while (heightTracker[heightOrder] >= variant.position) + heightOrder += 1 + heightTracker[heightOrder] = variant.end + } const canvasYPos = this.tracksYPos(heightOrder) From 113b6319d829e0384f1183fffb92663b1f67b2c2 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 12:17:17 +0200 Subject: [PATCH 122/138] Fixes other typos and a docstring --- gens/commands/load.py | 8 ++++---- gens/commands/view.py | 2 +- gens/db/index.py | 2 +- gens/load/annotations.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gens/commands/load.py b/gens/commands/load.py index fc79f796..6cc1bd0c 100644 --- a/gens/commands/load.py +++ b/gens/commands/load.py @@ -67,7 +67,7 @@ def load(): def sample(sample_id, genome_build, baf, coverage, case_id, overview_json, force): """Load a sample into Gens database.""" db = app.config["GENS_DB"] - # if collection is not indexed, crate index + # if collection is not indexed, create index if len(get_indexes(db, SAMPLES_COLLECTION)) == 0: create_index(db, SAMPLES_COLLECTION) # load samples @@ -103,7 +103,7 @@ def sample(sample_id, genome_build, baf, coverage, case_id, overview_json, force def annotations(file, genome_build): """Load annotations from file into the database.""" db = app.config["GENS_DB"] - # if collection is not indexed, crate index + # if collection is not indexed, create index if len(get_indexes(db, ANNOTATIONS_COLLECTION)) == 0: create_index(db, ANNOTATIONS_COLLECTION) # check if path is a directoy of a file @@ -163,7 +163,7 @@ def annotations(file, genome_build): def transcripts(file, mane, genome_build): """Load transcripts into the database.""" db = app.config["GENS_DB"] - # if collection is not indexed, crate index + # if collection is not indexed, create index if len(get_indexes(db, TRANSCRIPTS_COLLECTION)) > 0: create_index(db, TRANSCRIPTS_COLLECTION) LOG.info("Building transcript object") @@ -202,7 +202,7 @@ def transcripts(file, mane, genome_build): def chromosome_info(file, genome_build, timeout): """Load chromosome size information into the database.""" db = app.config["GENS_DB"] - # if collection is not indexed, crate index + # if collection is not indexed, create index if len(get_indexes(db, CHROMSIZES_COLLECTION)) == 0: create_index(db, CHROMSIZES_COLLECTION) # get chromosome info from ensemble diff --git a/gens/commands/view.py b/gens/commands/view.py index e5ea1adb..38e4107f 100644 --- a/gens/commands/view.py +++ b/gens/commands/view.py @@ -15,7 +15,7 @@ @click.group() def view(): - """Load information into Gens database""" + """View information loaded into Gens database""" @view.command() diff --git a/gens/db/index.py b/gens/db/index.py index 12743c2f..01d172c2 100644 --- a/gens/db/index.py +++ b/gens/db/index.py @@ -96,7 +96,7 @@ def get_indexes(db, collection): def create_index(db, collection_name): - """Create indexe for collection in Gens db.""" + """Create indexes for collection in Gens db.""" indexes = INDEXES[collection_name] existing_indexes = get_indexes(db, collection_name) # Drop old indexes diff --git a/gens/load/annotations.py b/gens/load/annotations.py index d9673c84..c5813af2 100644 --- a/gens/load/annotations.py +++ b/gens/load/annotations.py @@ -164,7 +164,7 @@ def update_height_order(db, name): def parse_annotation_file(file, genome_build, file_format): - """Parse a annotation file in bed or aed format.""" + """Parse an annotation file in bed or aed format.""" if file_format == "bed": return parse_bed(file, genome_build) if file_format == "aed": From 63a73657d895a00211919a3ee50e5334002d5dd9 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 17 Jul 2024 12:18:18 +0200 Subject: [PATCH 123/138] Updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cd354a..ceb02a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Pan able to exit chrosome when using genome build 17 - `--force` flag `update_one` call not being called properly - Incorrect total sample count on home page. +- Some typos and documentation. ## [2.3] ### Added From 3d877e823a8d6f747152f0e99d0e4e14bddd5e2a Mon Sep 17 00:00:00 2001 From: Emil Bertilsson Date: Wed, 14 Aug 2024 09:45:45 +0200 Subject: [PATCH 124/138] Merge --- assets/js/track/variant.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index b677d42b..38466638 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -52,6 +52,9 @@ export class VariantTrack extends BaseAnnotationTrack { // Initialize highlighted variant this.highlightedVariantId = highlightedVariantId initTrackTooltips(this) + + // Initialize label tracking + this.labelData = [] } // Draw highlight for a given region @@ -128,6 +131,8 @@ export class VariantTrack extends BaseAnnotationTrack { heightTracker = Array(200) + this.labelData = [] + // Draw track const drawTooltips = this.getResolution < 4 for (const variant of filteredVariants) { @@ -272,14 +277,33 @@ export class VariantTrack extends BaseAnnotationTrack { if (['dup', 'del', 'cnv'].includes(variantCategory)) { const textYPos = this.tracksYPos(heightOrder) // Draw variant type - drawText({ + this.labelData.push({ + text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, + x: (variantObj.start + variantObj.end) / 2, + y: textYPos + this.featureHeight, + fontProp: textSize + }) + /* drawText({ ctx: this.drawCtx, text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, x: scale * ((variantObj.start + variantObj.end) / 2 - this.offscreenPosition.start), y: textYPos + this.featureHeight, fontProp: textSize - }) + }) */ } } } + + drawDynamicOverlay () { + const ctx = this.contentCanvas.getContext('2d') + this.labelData.forEach((label) => { + drawText({ + ctx: ctx, + text: label.text, + x: Math.max(0, label.x - this.onscreenPosition.start), + y: label.y, + fontProp: label.fontProp + }) + }) + } } From 3ea8d61906a290e0af872fc296ff80e3ecc461c8 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 12:51:38 +0200 Subject: [PATCH 125/138] adds proper scaling to overlay --- assets/js/track/variant.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 38466638..07a7f2e3 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -296,11 +296,12 @@ export class VariantTrack extends BaseAnnotationTrack { drawDynamicOverlay () { const ctx = this.contentCanvas.getContext('2d') + const scale = this.contentCanvas.width / (this.onscreenPosition.end - this.onscreenPosition.start) this.labelData.forEach((label) => { drawText({ ctx: ctx, text: label.text, - x: Math.max(0, label.x - this.onscreenPosition.start), + x: Math.max(0, Math.min(this.contentCanvas.width, scale * (label.x - this.onscreenPosition.start))), y: label.y, fontProp: label.fontProp }) From 5834972c38c7addc758a5dc77543c5fed43fe4f0 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 13:00:17 +0200 Subject: [PATCH 126/138] only draw labels for visible variants --- assets/js/track/variant.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 07a7f2e3..7c674a68 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -131,7 +131,7 @@ export class VariantTrack extends BaseAnnotationTrack { heightTracker = Array(200) - this.labelData = [] + const labelData = [] // Draw track const drawTooltips = this.getResolution < 4 @@ -277,8 +277,10 @@ export class VariantTrack extends BaseAnnotationTrack { if (['dup', 'del', 'cnv'].includes(variantCategory)) { const textYPos = this.tracksYPos(heightOrder) // Draw variant type - this.labelData.push({ + labelData.push({ text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, + start: variantObj.start, + end: variantObj.end, x: (variantObj.start + variantObj.end) / 2, y: textYPos + this.featureHeight, fontProp: textSize @@ -292,19 +294,22 @@ export class VariantTrack extends BaseAnnotationTrack { }) */ } } + this.labelData = labelData } drawDynamicOverlay () { const ctx = this.contentCanvas.getContext('2d') const scale = this.contentCanvas.width / (this.onscreenPosition.end - this.onscreenPosition.start) this.labelData.forEach((label) => { - drawText({ - ctx: ctx, - text: label.text, - x: Math.max(0, Math.min(this.contentCanvas.width, scale * (label.x - this.onscreenPosition.start))), - y: label.y, - fontProp: label.fontProp - }) + if (label.start < this.onscreenPosition.end && label.end > this.onscreenPosition.start) { + drawText({ + ctx: ctx, + text: label.text, + x: Math.max(0, Math.min(this.contentCanvas.width, scale * (label.x - this.onscreenPosition.start))), + y: label.y, + fontProp: label.fontProp + }) + } }) } } From a3e6bb1538b58c7826a765850ffcacbbb309d96f Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 13:06:43 +0200 Subject: [PATCH 127/138] adds hardcoded margin --- assets/js/track/variant.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 7c674a68..2e07290f 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -300,12 +300,13 @@ export class VariantTrack extends BaseAnnotationTrack { drawDynamicOverlay () { const ctx = this.contentCanvas.getContext('2d') const scale = this.contentCanvas.width / (this.onscreenPosition.end - this.onscreenPosition.start) + const margin = 200 this.labelData.forEach((label) => { if (label.start < this.onscreenPosition.end && label.end > this.onscreenPosition.start) { drawText({ ctx: ctx, text: label.text, - x: Math.max(0, Math.min(this.contentCanvas.width, scale * (label.x - this.onscreenPosition.start))), + x: Math.max(margin, Math.min(this.contentCanvas.width - margin, scale * (label.x - this.onscreenPosition.start))), y: label.y, fontProp: label.fontProp }) From 3b2a100efa99c789b69b90d372f38fc8984ce8b1 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 13:11:29 +0200 Subject: [PATCH 128/138] adjusts hardcoded margin --- assets/js/track/variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 2e07290f..8d482529 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -300,7 +300,7 @@ export class VariantTrack extends BaseAnnotationTrack { drawDynamicOverlay () { const ctx = this.contentCanvas.getContext('2d') const scale = this.contentCanvas.width / (this.onscreenPosition.end - this.onscreenPosition.start) - const margin = 200 + const margin = 100 this.labelData.forEach((label) => { if (label.start < this.onscreenPosition.end && label.end > this.onscreenPosition.start) { drawText({ From c841b977757d637358caa902a1f7fbd8e6881769 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 13:26:17 +0200 Subject: [PATCH 129/138] updates changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb02a15..ef8992a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - `--force` flag `update_one` call not being called properly - Incorrect total sample count on home page. - Some typos and documentation. +- Labels often not being visible on larger variants. ## [2.3] ### Added From ab03b40ca4800eaf280eba789837ab16a5080c7b Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 14 Aug 2024 13:31:54 +0200 Subject: [PATCH 130/138] fixes overlay not rendering right after expanding variant track --- assets/js/track/base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/track/base.js b/assets/js/track/base.js index c7b04ea0..514bc04e 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -133,6 +133,7 @@ export class BaseAnnotationTrack { data: this.trackData }) this.blitCanvas(this.onscreenPosition.start, this.onscreenPosition.end) + this.drawDynamicOverlay() }, false) } From faf7e5de806a2c1df55ac2456b85ac4d697863e2 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 15 Aug 2024 10:58:24 +0200 Subject: [PATCH 131/138] adds very basic expand button --- assets/css/gens.scss | 8 ++++++++ gens/blueprints/gens/templates/gens.html | 1 + 2 files changed, 9 insertions(+) diff --git a/assets/css/gens.scss b/assets/css/gens.scss index d255a7ef..723e1445 100644 --- a/assets/css/gens.scss +++ b/assets/css/gens.scss @@ -428,6 +428,14 @@ html, body { grid-column: 1; position: relative; } + + .info-expand-button { + top: 0; + right: 0; + z-index: 2; + position: relative; + } + .offscreen { visibility: hidden; } diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index 43e0f2d4..bfefdc9a 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -8,6 +8,7 @@
+
{% endmacro %} From e26c71b989354c5501d739ffa0a5132e32480633 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 16:27:58 +0200 Subject: [PATCH 132/138] Merge --- .eslintrc.yml | 2 + CHANGELOG.md | 1 + assets/__mocks__/fileMock.js | 2 +- assets/__mocks__/styleMock.js | 2 +- assets/css/about.scss | 145 +++--- assets/css/defaults.scss | 14 +- assets/css/error.scss | 90 ++-- assets/css/gens.scss | 819 +++++++++++++++++----------------- assets/css/home.scss | 174 ++++---- assets/css/landing.scss | 127 +++--- assets/css/navbar.scss | 130 +++--- assets/js/draw.js | 16 +- assets/js/draw/graph.js | 137 ++++-- assets/js/draw/shapes.js | 218 +++++---- assets/js/fetch.js | 54 +-- assets/js/fetch.test.js | 36 +- assets/js/gens.js | 147 ++++-- assets/js/gens.test.js | 26 +- assets/js/helper.js | 33 +- assets/js/interactive.js | 646 ++++++++++++++++----------- assets/js/navigation.js | 357 ++++++++------- assets/js/navigation.test.js | 267 +++++------ assets/js/overview.js | 298 ++++++++----- assets/js/track.js | 15 +- assets/js/track/annotation.js | 216 ++++----- assets/js/track/base.js | 342 +++++++------- assets/js/track/base.test.js | 31 +- assets/js/track/constants.js | 31 +- assets/js/track/ideogram.js | 403 ++++++++++------- assets/js/track/tooltip.js | 246 +++++----- assets/js/track/transcript.js | 291 ++++++------ assets/js/track/utils.js | 65 ++- assets/js/track/utils.test.js | 189 ++++---- assets/js/track/variant.js | 349 +++++++++------ 34 files changed, 3350 insertions(+), 2569 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 61121ddd..f719d15c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,6 +3,8 @@ env: es2021: true extends: - standard + - "eslint:recommended" + - "prettier" parserOptions: ecmaVersion: 12 sourceType: module diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8992a6..c99638b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) - Reinstated tooltips to display additional information on genetic elements ### Changed - Use popper for positioning tooltips + - Prettier for code formatting ### Fixed ## [1.2.0] diff --git a/assets/__mocks__/fileMock.js b/assets/__mocks__/fileMock.js index c6c394f7..ea1367c1 100644 --- a/assets/__mocks__/fileMock.js +++ b/assets/__mocks__/fileMock.js @@ -1,3 +1,3 @@ // __mocks__/fileMock.js -module.exports = 'test-file-stub'; \ No newline at end of file +module.exports = "test-file-stub"; diff --git a/assets/__mocks__/styleMock.js b/assets/__mocks__/styleMock.js index eb092ce4..d988e23b 100644 --- a/assets/__mocks__/styleMock.js +++ b/assets/__mocks__/styleMock.js @@ -1,3 +1,3 @@ // __mocks__/styleMock.js -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/assets/css/about.scss b/assets/css/about.scss index 28168050..249b0b8f 100644 --- a/assets/css/about.scss +++ b/assets/css/about.scss @@ -1,4 +1,4 @@ -@import 'defaults', 'navbar'; +@import "defaults", "navbar"; $tablet-width: 768px; $desktop-width: 1024px; @@ -15,96 +15,101 @@ $desktop-width: 1024px; } .content { - width: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - + width: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } #logo { - img { - width: 500px; - } + img { + width: 500px; + } - .version { - font-style: italic; - } + .version { + font-style: italic; + } } .card-panel { - .row { - display: flex; - flex-wrap: wrap; - flex-direction: row; - justify-content: center; - } + .row { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + } + .card { + margin: 10px; + padding: 15px 20px 20px 20px; + border-radius: 3px; + box-shadow: + 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 3px 10px 0 rgba(0, 0, 0, 0.19); + } + @include tablet { .card { - margin: 10px; - padding: 15px 20px 20px 20px; - border-radius: 3px; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); - } - @include tablet { - .card { - max-width: 100%; - flex: 100%; - } + max-width: 100%; + flex: 100%; } + } } #loaded-db-data { + h2, + h3, + h4 { + margin: 0; + padding-top: 0; + padding-bottom: 5px; - h2, h3, h4 { - margin: 0; - padding-top: 0; - padding-bottom: 5px; - - .year { - font-size: 0.85em; - } + .year { + font-size: 0.85em; } + } - h2 { - font-size: 1.4em; - } + h2 { + font-size: 1.4em; + } - ul { - margin: 0; - padding-top: 0; - } - - .year { - font-weight: lighter; - font-style: italic; - } + ul { + margin: 0; + padding-top: 0; + } + + .year { + font-weight: lighter; + font-style: italic; + } } #config { - @extend #loaded-db-data; - - h3 { padding-top: 10px; } - h4 { - padding-top: 5px; - padding-bottom: 0; - } + @extend #loaded-db-data; - .color-group { - width: 150px; - padding-top: 5px; - margin-left: 20px; - display: flex; - justify-content: space-between; - align-items: center; - } - .title { margin: 0; } + h3 { + padding-top: 10px; + } + h4 { + padding-top: 5px; + padding-bottom: 0; + } - .color-box { - width: 30px; - height: 20px; - margin-left: 20px; - } + .color-group { + width: 150px; + padding-top: 5px; + margin-left: 20px; + display: flex; + justify-content: space-between; + align-items: center; + } + .title { + margin: 0; + } + .color-box { + width: 30px; + height: 20px; + margin-left: 20px; + } } diff --git a/assets/css/defaults.scss b/assets/css/defaults.scss index a92fe314..b5649759 100644 --- a/assets/css/defaults.scss +++ b/assets/css/defaults.scss @@ -1,17 +1,17 @@ - // Default colors +// Default colors $default-font-color: #202231; -$default-bg-color: #F7F9F9; +$default-bg-color: #f7f9f9; // font $default-font: Arial, Helvetica, sans-serif; $default-font-print: "Times New Roman", Times, serif; html { - font-family: $default-font; - color: $default-font-color; - background-color: $default-bg-color; + font-family: $default-font; + color: $default-font-color; + background-color: $default-bg-color; } body { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } diff --git a/assets/css/error.scss b/assets/css/error.scss index 3d042fcb..bcfd1292 100644 --- a/assets/css/error.scss +++ b/assets/css/error.scss @@ -1,57 +1,57 @@ #broken-dna { - position: absolute; - transform: rotate(-80deg); - width: 800px; - top: -130px; - left: 30%; - z-index: -1; + position: absolute; + transform: rotate(-80deg); + width: 800px; + top: -130px; + left: 30%; + z-index: -1; } .content { - margin: auto; - max-width: 60%; - margin-top: 50px; - padding: 20px 15px; + margin: auto; + max-width: 60%; + margin-top: 50px; + padding: 20px 15px; - #error-code { - z-index: 1; - position: relative; - left: -150px; - top: 50px; - margin: 0; - padding: 0; - // font - font-size: 150px; - font-weight: bolder; - letter-spacing: 10px; - transform: rotate(-25deg); - color: #4d5663; - } + #error-code { + z-index: 1; + position: relative; + left: -150px; + top: 50px; + margin: 0; + padding: 0; + // font + font-size: 150px; + font-weight: bolder; + letter-spacing: 10px; + transform: rotate(-25deg); + color: #4d5663; + } - .headline { - font-size: 48px; - text-align: center; - font-style: italic; - margin-top: 0; - } + .headline { + font-size: 48px; + text-align: center; + font-style: italic; + margin-top: 0; + } - p { - text-align: center; - padding-top: 15px; - } + p { + text-align: center; + padding-top: 15px; + } - .error-msg-container { - position: absolute; - top: 500px; - width: 40%; - border-radius: 4px; - padding: 20px; - // center in parent - left: 50%; - transform: translateX(-50%); - } + .error-msg-container { + position: absolute; + top: 500px; + width: 40%; + border-radius: 4px; + padding: 20px; + // center in parent + left: 50%; + transform: translateX(-50%); + } } .bold { - font-weight: bold; + font-weight: bold; } diff --git a/assets/css/gens.scss b/assets/css/gens.scss index 723e1445..4bc06373 100644 --- a/assets/css/gens.scss +++ b/assets/css/gens.scss @@ -1,20 +1,21 @@ -@import 'defaults', 'navbar'; +@import "defaults", "navbar"; // region marker $marker-color: rgba(#dcd16f, 0.3); // buttons -$btn-zoom-color: #8FBCBB; -$btn-navigate-color: #6DB2C5; - -$btn-submit-color: #6DB2C5; -html, body { - max-width: 100%; - min-height: 100%; - width: 100%; - overflow-x: hidden; - margin: 0; - display: grid; - grid-template-columns: auto; - grid-template-rows: auto; +$btn-zoom-color: #8fbcbb; +$btn-navigate-color: #6db2c5; + +$btn-submit-color: #6db2c5; +html, +body { + max-width: 100%; + min-height: 100%; + width: 100%; + overflow-x: hidden; + margin: 0; + display: grid; + grid-template-columns: auto; + grid-template-rows: auto; } .tooltip { @@ -37,562 +38,560 @@ html, body { } .tooltip[data-show] { - display: block + display: block; } -.tooltip[data-popper-placement^='top'] > .arrow { +.tooltip[data-popper-placement^="top"] > .arrow { bottom: -4px; } -.tooltip[data-popper-placement^='bottom'] > .arrow { +.tooltip[data-popper-placement^="bottom"] > .arrow { top: -4px; } -.tooltip[data-popper-placement^='left'] > .arrow { +.tooltip[data-popper-placement^="left"] > .arrow { right: -4px; } -.tooltip[data-popper-placement^='right'] > .arrow { +.tooltip[data-popper-placement^="right"] > .arrow { left: -4px; } #loading-div { - display:none; - position:absolute; - font-family:sans-serif; - font-size:9pt; - font-weight:bold; - color:#567; - text-align: center; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - padding-top: 210px; - z-index: 10; - background-color: $default-bg-color; + display: none; + position: absolute; + font-family: sans-serif; + font-size: 9pt; + font-weight: bold; + color: #567; + text-align: center; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding-top: 210px; + z-index: 10; + background-color: $default-bg-color; } .help-popup { - // dimensions and color - display: none; - transition: opacity 2s linear 0.5s; - color: $default-font-color; - background: $default-bg-color; - border: 1px solid $default-font-color; - width: 300px; - padding: 0px 10px 10px 10px; - // positioning - position: absolute; - top: 32px; - right: 25px; - z-index: 2; - // text properties - font-size: small; - text-align: left; - - h3, ul, p { - margin-bottom: 4px; - } + // dimensions and color + display: none; + transition: opacity 2s linear 0.5s; + color: $default-font-color; + background: $default-bg-color; + border: 1px solid $default-font-color; + width: 300px; + padding: 0px 10px 10px 10px; + // positioning + position: absolute; + top: 32px; + right: 25px; + z-index: 2; + // text properties + font-size: small; + text-align: left; + + h3, + ul, + p { + margin-bottom: 4px; + } - ul, p { - margin-top: 4px; - } + ul, + p { + margin-top: 4px; + } } .info:hover + .help-popup { - display: block; + display: block; } .header { - font-size: 22px; - padding: 15px 20px; - display: grid; - grid-template-columns: 30% 40% 30%; - grid-template-rows: auto; - grid-template-areas: 'header header header'; - - @media only screen { - font-family: $default-font; - background: $menubar-bg-color; - color: $menubar-font-color; - } - - @media only print { - font-family: $default-font-print; - background: $menubar-bg-color-print; - color: $menubar-font-color-print; + font-size: 22px; + padding: 15px 20px; + display: grid; + grid-template-columns: 30% 40% 30%; + grid-template-rows: auto; + grid-template-areas: "header header header"; + + @media only screen { + font-family: $default-font; + background: $menubar-bg-color; + color: $menubar-font-color; + } - } + @media only print { + font-family: $default-font-print; + background: $menubar-bg-color-print; + color: $menubar-font-color-print; + } - #left-group { - grid-row: 1; - grid-column: 1; - text-align: left; - } + #left-group { + grid-row: 1; + grid-column: 1; + text-align: left; + } - #center-group { - grid-row: 1; - grid-column: 2; - text-align: center; - } + #center-group { + grid-row: 1; + grid-column: 2; + text-align: center; + } - #right-group { - grid-row: 1; - grid-column: 3; - text-align: right; - } + #right-group { + grid-row: 1; + grid-column: 3; + text-align: right; + } - .header-icon { - background-size: contain; - display: inline-block; - width: 20px; - height: 20px; - filter: invert(88%) sepia(97%) saturate(3909%) hue-rotate(179deg) brightness(111%) contrast(105%); - } + .header-icon { + background-size: contain; + display: inline-block; + width: 20px; + height: 20px; + filter: invert(88%) sepia(97%) saturate(3909%) hue-rotate(179deg) + brightness(111%) contrast(105%); + } - .print-icon { - background-size: contain; - position: fixed; - top: 40%; - left: 50%; - width: 100px; - height: 100px; - filter: invert(70%); - visibility: hidden; - } + .print-icon { + background-size: contain; + position: fixed; + top: 40%; + left: 50%; + width: 100px; + height: 100px; + filter: invert(70%); + visibility: hidden; + } - .info-icon { - position: absolute; - } + .info-icon { + position: absolute; + } - .bold { - font-weight: bold; - } + .bold { + font-weight: bold; + } - #home-link { - text-decoration: none; - } + #home-link { + text-decoration: none; + } - #logo-container { - width: 50px; - height: 50px; - border-radius: 50%; - display: inline-block; - background: $menubar-font-color; - } + #logo-container { + width: 50px; + height: 50px; + border-radius: 50%; + display: inline-block; + background: $menubar-font-color; + } - .logo { - background: url(/gens/static/svg/gens-logo-only.svg) no-repeat top left; - background-size: contain; - width: 35px; - height: 35px; - display: inherit; - // center in parent - position: relative; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } + .logo { + background: url(/gens/static/svg/gens-logo-only.svg) no-repeat top left; + background-size: contain; + width: 35px; + height: 35px; + display: inherit; + // center in parent + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } - .version { - font-size: 12px; - } + .version { + font-size: 12px; + } - .date { - font-size: 18px; - margin-left: 15px; - } + .date { + font-size: 18px; + margin-left: 15px; + } } #main-container { - grid-row: 1; - grid-column: 1; + grid-row: 1; + grid-column: 1; } #visualization-container { - height: min-content; - display: none; - grid-row: 1; - grid-column: 1; - + height: min-content; + display: none; + grid-row: 1; + grid-column: 1; } #button-container { - text-align: center; - grid-row: 7; - grid-column: 1; - margin-top: 5px; + text-align: center; + grid-row: 7; + grid-column: 1; + margin-top: 5px; - @media only print { - page-break-after: always; - } + @media only print { + page-break-after: always; + } - select { - margin-right: 15px; - } + select { + margin-right: 15px; + } - form { - margin-left: 15px; - display: inline; - } + form { + margin-left: 15px; + display: inline; + } + + form.error:disabled { + outline: 5px dotted red; + background-color: red; + } - form.error:disabled { - outline: 5px dotted red; - background-color: red; + .button { + border: 0px; + box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.3); + padding: 5px 15px; + + &--zoom-in { + @extend .button; + background: $btn-zoom-color; + margin-left: 5px; + margin-right: 2px; } - .button { - border: 0px; - box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.3); - padding: 5px 15px; - - &--zoom-in { - @extend .button; - background: $btn-zoom-color; - margin-left: 5px; - margin-right: 2px; - } - - &--zoom-out { - @extend .button; - background: $btn-zoom-color; - margin-right: 5px; - } - - &--pan { - @extend .button; - background: $btn-navigate-color; - - } - &--submit { - @extend .button; - background: $btn-submit-color; - } + &--zoom-out { + @extend .button; + background: $btn-zoom-color; + margin-right: 5px; } - .icon { - background-size: contain; - display: inline-block; - width: 15px; - height: 15px; + &--pan { + @extend .button; + background: $btn-navigate-color; + } + &--submit { + @extend .button; + background: $btn-submit-color; } + } + + .icon { + background-size: contain; + display: inline-block; + width: 15px; + height: 15px; + } } #chromosome-title { - display: grid; - grid-row: 1; - grid-column: 1; + display: grid; + grid-row: 1; + grid-column: 1; } #cytogenetic-ideogram { - display: grid; - grid-row: 2; - grid-column: 1; - - .marker { - border-style: solid; - border-color: red; - border-left-width: 2px; - border-right-width: 2px; - } + display: grid; + grid-row: 2; + grid-column: 1; + + .marker { + border-style: solid; + border-color: red; + border-left-width: 2px; + border-right-width: 2px; + } } #interactive-container { - display: grid; - grid-row: 3; - grid-column: 1; + display: grid; + grid-row: 3; + grid-column: 1; - #interactive-static { - z-index: 1; - pointer-events: none; - grid-row: 4; - grid-column: 1; - } + #interactive-static { + z-index: 1; + pointer-events: none; + grid-row: 4; + grid-column: 1; + } - #interactive-content { - z-index: 1; - grid-row: 4; - grid-column: 1; - } + #interactive-content { + z-index: 1; + grid-row: 4; + grid-column: 1; + } - #interactive-marker { - position: absolute; - z-index: 1; - } + #interactive-marker { + position: absolute; + z-index: 1; + } } #interactive-options { - text-align: center; - width: 100%; - height: 18px; + text-align: center; + width: 100%; + height: 18px; } - #source-list { - visibility: hidden; - height: 100%; - height: 21px; - width: 200px; + visibility: hidden; + height: 100%; + height: 21px; + width: 200px; } #times { - background: url(./svg/times-solid.svg) no-repeat top left; + background: url(./svg/times-solid.svg) no-repeat top left; } #trash-alt { - background: url(./svg/trash-alt-regular.svg) no-repeat top left; + background: url(./svg/trash-alt-regular.svg) no-repeat top left; } .marker { - background-color: $marker-color; - border: dashed 1px #7c7c7c; - border-top-width: 0; - border-bottom-width: 0; - pointer-events:none; - position: relative; + background-color: $marker-color; + border: dashed 1px #7c7c7c; + border-top-width: 0; + border-bottom-width: 0; + pointer-events: none; + position: relative; } -.info-container[data-state=nodata] { - height: 0; - border: none; +.info-container[data-state="nodata"] { + height: 0; + border: none; } -.info-container[data-state=collapsed] { - padding-right: 0; - border: 1px solid gray; - overflow-y: hidden; +.info-container[data-state="collapsed"] { + padding-right: 0; + border: 1px solid gray; + overflow-y: hidden; } -.info-container[data-state=expanded] { - padding-right: 6px; - border: 1px solid gray; +.info-container[data-state="expanded"] { + padding-right: 6px; + border: 1px solid gray; } -.track-container[data-state=data] { - visibility: visible; +.track-container[data-state="data"] { + visibility: visible; } -.track-container[data-state=nodata] { - visibility: collapse; - height: 0; +.track-container[data-state="nodata"] { + visibility: collapse; + height: 0; } #variant-container { - grid-row: 4; + grid-row: 4; } #transcript-container { - grid-row: 5; + grid-row: 5; } #annotation-container { - padding-top: 10px; - grid-row: 6; + padding-top: 10px; + grid-row: 6; } .track-xlabel { - // position - position: relative; - top: 25px; - width: 80px; - z-index: 10; - margin: 0; - padding: 0; - // text properties - transform: rotate(270deg); - text-align: center; - font-size: x-small; - font-weight: bold; + // position + position: relative; + top: 25px; + width: 80px; + z-index: 10; + margin: 0; + padding: 0; + // text properties + transform: rotate(270deg); + text-align: center; + font-size: x-small; + font-weight: bold; } .info-container { - display: grid; + display: grid; + grid-column: 1; + overflow-y: auto; + overflow-x: hidden; + height: 100px; + + .info-canvas { + top: 0; + left: 0; + z-index: 0; + grid-row: 1; grid-column: 1; - overflow-y: auto; - overflow-x: hidden; - height: 100px; - - .info-canvas { - top: 0; - left: 0; - z-index: 0; - grid-row: 1; - grid-column: 1; - } - - .info-titles { - top: 0; - left: 0; - z-index: 1; - grid-row: 1; - grid-column: 1; - position: relative; - } - - .info-expand-button { - top: 0; - right: 0; - z-index: 2; - position: relative; - } + } - .offscreen { - visibility: hidden; - } + .info-titles { + top: 0; + left: 0; + z-index: 1; + grid-row: 1; + grid-column: 1; + position: relative; + } + .offscreen { + visibility: hidden; + } } .info-container::-webkit-scrollbar { - width: 6px; - border-left: 1px solid black; + width: 6px; + border-left: 1px solid black; } .info-container::-webkit-scrollbar-track { - border-radius: 10px; + border-radius: 10px; } .info-container::-webkit-scrollbar-thumb { - background: rgba(50, 50, 50, 0.5); + background: rgba(50, 50, 50, 0.5); } - .info { - background: url(./svg/info-circle-fill.svg) no-repeat top left; + background: url(./svg/info-circle-fill.svg) no-repeat top left; } .print { - background: url(./svg/printer-fill.svg) no-repeat top left; + background: url(./svg/printer-fill.svg) no-repeat top left; } .permalink { - background: url(./svg/link-45deg.svg) no-repeat top left; + background: url(./svg/link-45deg.svg) no-repeat top left; } .arrow-left { - background: url(./svg/arrow-left.svg) no-repeat top left; + background: url(./svg/arrow-left.svg) no-repeat top left; } .arrow-right { - background: url(./svg/arrow-right.svg) no-repeat top left; + background: url(./svg/arrow-right.svg) no-repeat top left; } .search-plus { - background: url(./svg/zoom-in.svg) no-repeat top left; + background: url(./svg/zoom-in.svg) no-repeat top left; } .search-minus { - background: url(./svg/zoom-out.svg) no-repeat top left; + background: url(./svg/zoom-out.svg) no-repeat top left; } .loading-view { - // pos - position: absolute; - z-index: 99; - width: 100%; - height: 100%; - margin: 0; - // style - $black: #000; - background-color: $default-bg-color; - - .loading-container { - position: relative; - top: 30%; - margin: auto; - display: flex; - width: 190px; - padding: 10px; - justify-content: center; - align-items: center; - } - .message { - font: italic 1.2em sans-serif; - padding-right: 12px; - color: $default-font-color; - flex: 1; - } - .spinner { - border-top: 3px solid rgba($menubar-bg-color, 0.5); - border-right: 3px solid transparent; - border-radius: 50%; - width: 30px; - height: 30px; - animation: spin .8s linear infinite; - } + // pos + position: absolute; + z-index: 99; + width: 100%; + height: 100%; + margin: 0; + // style + $black: #000; + background-color: $default-bg-color; + + .loading-container { + position: relative; + top: 30%; + margin: auto; + display: flex; + width: 190px; + padding: 10px; + justify-content: center; + align-items: center; + } + .message { + font: italic 1.2em sans-serif; + padding-right: 12px; + color: $default-font-color; + flex: 1; + } + .spinner { + border-top: 3px solid rgba($menubar-bg-color, 0.5); + border-right: 3px solid transparent; + border-radius: 50%; + width: 30px; + height: 30px; + animation: spin 0.8s linear infinite; + } } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .content { - font-family: sans-serif; - padding-left: 10px; - h1 { - margin: 2px; - font-size: 1.5em; - } + font-family: sans-serif; + padding-left: 10px; + h1 { + margin: 2px; + font-size: 1.5em; + } - p { margin: 2px 2px 10px 0;} + p { + margin: 2px 2px 10px 0; + } - .version { font-weight: bold;} + .version { + font-weight: bold; + } } - #overview-container { - display: grid; - grid-row: 8; + display: grid; + grid-row: 8; + grid-column: 1; + + #overview-static { + top: 0; + left: 0; + z-index: 0; + grid-row: 1; grid-column: 1; + } - #overview-static { - top: 0; - left: 0; - z-index: 0; - grid-row: 1; - grid-column: 1; - } - - #staticCanvas { - top: 0; - left: 0; - z-index: 0; - pointer-events: none; - grid-row: 1; - grid-column: 1; - } + #staticCanvas { + top: 0; + left: 0; + z-index: 0; + pointer-events: none; + grid-row: 1; + grid-column: 1; + } - #dataCanvas { - top: 0; - left: 0; - z-index: -1; - grid-row: 1; - grid-column: 1; - } + #dataCanvas { + top: 0; + left: 0; + z-index: -1; + grid-row: 1; + grid-column: 1; + } - #drawCanvas { - top: 0; - left: 0; - visibility: hidden; - } + #drawCanvas { + top: 0; + left: 0; + visibility: hidden; + } - #staticOverviewCanvas { - top: 0; - left: 0; - z-index: 0; - visibility: hidden; - } + #staticOverviewCanvas { + top: 0; + left: 0; + z-index: 0; + visibility: hidden; + } - #drawOverviewCanvas { - top: 0; - left: 0; - visibility: hidden; - } + #drawOverviewCanvas { + top: 0; + left: 0; + visibility: hidden; + } - #overview-marker { - position:relative; - } + #overview-marker { + position: relative; + } - .marker { - border-color: darkred; - } + .marker { + border-color: darkred; + } } diff --git a/assets/css/home.scss b/assets/css/home.scss index a50b83e6..277cd066 100644 --- a/assets/css/home.scss +++ b/assets/css/home.scss @@ -3,115 +3,123 @@ $table-border-color: #ddd; $selected-color: adjust-color($menubar-bg-color, $lightness: +30%); - -.align-right { text-align: right; } +.align-right { + text-align: right; +} .content { - width: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - .display-info { - margin-bottom: 4px; + width: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .display-info { + margin-bottom: 4px; + } + + table { + border-collapse: collapse; + tr { + border: 1px solid $table-border-color; + } + td { + padding: 8px 12px; + border: 1px solid $table-border-color; } - table { - border-collapse: collapse; - tr { - border: 1px solid $table-border-color; - } - td { - padding: 8px 12px; - border: 1px solid $table-border-color; - } + thead { + background-color: $menubar-bg-color; + color: $menubar-font-color; + font-weight: bold; + } - thead { - background-color: $menubar-bg-color; - color: $menubar-font-color; - font-weight: bold; + tbody { + // hover and coloring of table rows + tr:nth-child(even) { + background-color: #f2f2f2; } - - tbody { - // hover and coloring of table rows - tr:nth-child(even) { background-color: #f2f2f2;} - tr:hover { background-color: $table-border-color;} - // other styling + tr:hover { + background-color: $table-border-color; } + // other styling } + } } .pagination { - float: right; - margin-top: 10px; - padding-bottom: 50px; - - - ul { - margin: 0; - padding: 0; - list-style: none; - } - - li:hover { background-color: $table-border-color;} - - li { - float: left; - border-style: solid solid solid hidden; - border-width: 1px; - border-color: grey; - } - - li:first-child { - border-style: solid; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - } - - li:last-child { - border-style: solid solid solid hidden; - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - } - - a { - display: block; - padding: 8px; - text-decoration: none; - color: $default-font-color; - } + float: right; + margin-top: 10px; + padding-bottom: 50px; + + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li:hover { + background-color: $table-border-color; + } + + li { + float: left; + border-style: solid solid solid hidden; + border-width: 1px; + border-color: grey; + } + + li:first-child { + border-style: solid; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + + li:last-child { + border-style: solid solid solid hidden; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + } + + a { + display: block; + padding: 8px; + text-decoration: none; + color: $default-font-color; + } } .disabled { - pointer-events: none; - a { - color: dimgray; - } + pointer-events: none; + a { + color: dimgray; + } } .selected { - background-color: $selected-color; + background-color: $selected-color; - a { - color: $menubar-font-color; - } + a { + color: $menubar-font-color; + } } // icons .icon { - display: inline-block; - width: 15px; - height: 15px; - transform: scale(1.4); + display: inline-block; + width: 15px; + height: 15px; + transform: scale(1.4); } -.icon-color-red {filter: invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);} +.icon-color-red { + filter: invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) + brightness(104%) contrast(97%); +} .icon-check { - background: url(./svg/check.svg) no-repeat top left; + background: url(./svg/check.svg) no-repeat top left; } .icon-x { - background: url(./svg/x.svg) no-repeat top left; + background: url(./svg/x.svg) no-repeat top left; } - diff --git a/assets/css/landing.scss b/assets/css/landing.scss index 3afd1b86..e6be13b8 100644 --- a/assets/css/landing.scss +++ b/assets/css/landing.scss @@ -3,82 +3,81 @@ $table-border-color: #ddd; $selected-color: adjust-color($menubar-bg-color, $lightness: +30%); - -.align-right { text-align: right; } -.content { - width: auto; - display: flex; - flex-direction: column; - justify-content: left; - align-items: center; - - .display-info { - margin-bottom: 4px; - } - - form { - float: right; - margin-top: 10px; - padding-bottom: 50px; - } - ul { - margin: 0; - padding: 0; - list-style: none; - } - - li { - float: left; - border-style: solid; - border-radius: 5px; - border-width: 1px; - border-color: grey; - } - - a { - padding: 8px; - } - - p { - width: 600px; - } - - input { - font-family: $default-font; - font-size: 1.5em; - } +.align-right { + text-align: right; } - - - -.btn { +.content { + width: auto; + display: flex; + flex-direction: column; + justify-content: left; + align-items: center; + + .display-info { + margin-bottom: 4px; + } + + form { + float: right; + margin-top: 10px; + padding-bottom: 50px; + } + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li { + float: left; + border-style: solid; + border-radius: 5px; + border-width: 1px; + border-color: grey; + } + + a { + padding: 8px; + } + + p { + width: 600px; + } + + input { font-family: $default-font; font-size: 1.5em; - border-radius: 3px; - color: $menubar-font-color; - background-color: $menubar-bg-color; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); + } } +.btn { + font-family: $default-font; + font-size: 1.5em; + border-radius: 3px; + color: $menubar-font-color; + background-color: $menubar-bg-color; + box-shadow: + 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 3px 10px 0 rgba(0, 0, 0, 0.19); +} .mainlogo { - img { - width: 500px; - } + img { + width: 500px; + } } - .disabled { - pointer-events: none; - a { - color: dimgray; - } + pointer-events: none; + a { + color: dimgray; + } } .selected { - background-color: $selected-color; + background-color: $selected-color; - a { - color: $menubar-font-color; - } + a { + color: $menubar-font-color; + } } diff --git a/assets/css/navbar.scss b/assets/css/navbar.scss index d80e8622..c56d257b 100644 --- a/assets/css/navbar.scss +++ b/assets/css/navbar.scss @@ -1,83 +1,85 @@ @use "sass:color"; - // Define menuebar colors -$menubar-font-color: #F4FAFF; -$menubar-bg-color: #4C6D94; +// Define menuebar colors +$menubar-font-color: #f4faff; +$menubar-bg-color: #4c6d94; $menubar-bg-color-dark: adjust-color($menubar-bg-color, $lightness: -10%); $menubar-bg-color-print: darkgrey; $menubar-font-color-print: white; // size .navbar { - font-family: $default-font; - font-size: 1.5em; - background: $menubar-bg-color; - color: $menubar-font-color; - // alignment of items in navbar - display: flex; - align-items: center; - padding-left: 10px; - // drop shadow - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 3px 10px 0 rgba(0, 0, 0, 0.19); + font-family: $default-font; + font-size: 1.5em; + background: $menubar-bg-color; + color: $menubar-font-color; + // alignment of items in navbar + display: flex; + align-items: center; + padding-left: 10px; + // drop shadow + box-shadow: + 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 3px 10px 0 rgba(0, 0, 0, 0.19); - #home-link { - text-decoration: none; - } + #home-link { + text-decoration: none; + } - .logo-container { - width: 50px; - height: 50px; - border-radius: 50%; - display: inline-block; - margin: 5px 0; - background: $menubar-font-color; - } + .logo-container { + width: 50px; + height: 50px; + border-radius: 50%; + display: inline-block; + margin: 5px 0; + background: $menubar-font-color; + } - .active { background-color: $menubar-bg-color-dark} + .active { + background-color: $menubar-bg-color-dark; + } - .version { - font-size: 0.6em; - padding: 0 25px 0 8px; - } + .version { + font-size: 0.6em; + padding: 0 25px 0 8px; + } - .logo { - background: url(/gens/static/svg/gens-logo-only.svg) no-repeat top left; - background-size: contain; - width: 35px; - height: 35px; - display: inherit; - // center in parent - position: relative; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } + .logo { + background: url(/gens/static/svg/gens-logo-only.svg) no-repeat top left; + background-size: contain; + width: 35px; + height: 35px; + display: inherit; + // center in parent + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } - ul { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; - } - - li { - float: left; - - a { - display: block; - color: inherit; - text-align: center; - padding: 16px 18px; - text-decoration: none; - } + ul { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; + } - .logout { - float: right; - } + li { + float: left; + a { + display: block; + color: inherit; + text-align: center; + padding: 16px 18px; + text-decoration: none; } - - a:hover { - background-color: $menubar-bg-color-dark + .logout { + float: right; } + } + + a:hover { + background-color: $menubar-bg-color-dark; + } } diff --git a/assets/js/draw.js b/assets/js/draw.js index 4a16ee07..c871ef38 100644 --- a/assets/js/draw.js +++ b/assets/js/draw.js @@ -1,4 +1,16 @@ // entrypoint for drawing functions -export { drawVerticalTicks, createGraph, drawGraphLines } from './draw/graph.js' -export { drawRotatedText, drawPoints, drawText, drawLine, drawRect, drawArrow, drawWaveLine } from './draw/shapes.js' +export { + drawVerticalTicks, + createGraph, + drawGraphLines, +} from "./draw/graph.js"; +export { + drawRotatedText, + drawPoints, + drawText, + drawLine, + drawRect, + drawArrow, + drawWaveLine, +} from "./draw/shapes.js"; diff --git a/assets/js/draw/graph.js b/assets/js/draw/graph.js index aac694f9..07d8871e 100644 --- a/assets/js/draw/graph.js +++ b/assets/js/draw/graph.js @@ -1,39 +1,53 @@ // graph related objects -import { drawRect, drawLine, drawRotatedText, drawText } from './shapes.js' +import { drawRect, drawLine, drawRotatedText, drawText } from "./shapes.js"; // Draws vertical tick marks for selected values between // xStart and xEnd with step length. // The amplitude scales the values to drawing size -export function drawVerticalTicks ({ - ctx, renderX, y, xStart, xEnd, - xoStart, xoEnd, - width, yMargin, titleColor +export function drawVerticalTicks({ + ctx, + renderX, + y, + xStart, + xEnd, + xoStart, + xoEnd, + width, + yMargin, + titleColor, }) { - const lineThickness = 1 - const lineWidth = 5 - const regionSize = xEnd - xStart - const scale = width / regionSize - const maxNumTicks = 30 + const lineThickness = 1; + const lineWidth = 5; + const regionSize = xEnd - xStart; + const scale = width / regionSize; + const maxNumTicks = 30; // Create a step size which is an even power of ten (10, 100, 1000 etc) - let stepLength = 10 ** Math.floor(Math.log10(regionSize) - 1) + let stepLength = 10 ** Math.floor(Math.log10(regionSize) - 1); // Change to "half-steps" (50, 500, 5000) if too many ticks if (regionSize / stepLength > maxNumTicks) { - stepLength *= 5 + stepLength *= 5; } // Get starting position for the first tick - const xFirstTick = Math.ceil(xoStart / stepLength) * stepLength + const xFirstTick = Math.ceil(xoStart / stepLength) * stepLength; // Draw the ticks for (let step = xFirstTick; step <= xoEnd; step += stepLength) { - const xStep = Math.round(scale * (step - xoStart)) - const value = numberWithCommas(step.toFixed(0)) + const xStep = Math.round(scale * (step - xoStart)); + const value = numberWithCommas(step.toFixed(0)); // Draw text and ticks only for the leftmost box - drawRotatedText(ctx, value, 10, renderX + xStep + 8, - y - value.length - 3 * yMargin, -Math.PI / 4, titleColor) + drawRotatedText( + ctx, + value, + 10, + renderX + xStep + 8, + y - value.length - 3 * yMargin, + -Math.PI / 4, + titleColor, + ); // Draw tick line drawLine({ @@ -43,52 +57,95 @@ export function drawVerticalTicks ({ x2: renderX + xStep, y2: y, lineWidth: lineThickness, - color: '#777' - }) + color: "#777", + }); } } // Draws horizontal lines for selected values between // yStart and yEnd with step length. // The amplitude scales the values to drawing size -export function drawGraphLines ({ ctx, x, y, yStart, yEnd, stepLength, yMargin, width, height }) { - const ampl = (height - 2 * yMargin) / (yStart - yEnd) // Amplitude for scaling y-axis to fill whole height - const lineThickness = 1 +export function drawGraphLines({ + ctx, + x, + y, + yStart, + yEnd, + stepLength, + yMargin, + width, + height, +}) { + const ampl = (height - 2 * yMargin) / (yStart - yEnd); // Amplitude for scaling y-axis to fill whole height + const lineThickness = 1; for (let step = yStart; step >= yEnd; step -= stepLength) { // Draw horizontal line - const yPos = y + yMargin + (yStart - step) * ampl + const yPos = y + yMargin + (yStart - step) * ampl; drawLine({ ctx, x, y: yPos, x2: x + width - 2 * lineThickness, y2: yPos, - color: '#e5e5e5' - }) + color: "#e5e5e5", + }); } } // Creates a graph for one chromosome data type -export function createGraph (ctx, x, y, width, height, yMargin, yStart, - yEnd, step, addTicks, color = 'black', open) { +export function createGraph( + ctx, + x, + y, + width, + height, + yMargin, + yStart, + yEnd, + step, + addTicks, + color = "black", + open, +) { // Draw tick marks if (addTicks) { - drawTicks(ctx, x, y + yMargin, yStart, yEnd, step, yMargin, width, - height, color) + drawTicks( + ctx, + x, + y + yMargin, + yStart, + yEnd, + step, + yMargin, + width, + height, + color, + ); } // Draw surrounding coordinate box - drawRect({ ctx, x, y, width, height, lineWidth: 1, color, open }) + drawRect({ ctx, x, y, width, height, lineWidth: 1, color, open }); } // Draws tick marks for selected values between // yStart and yEnd with step length. // The amplitude scales the values to drawing size -function drawTicks (ctx, x, y, yStart, yEnd, stepLength, yMargin, width, height, color = 'black') { - const ampl = (height - 2 * yMargin) / (yStart - yEnd) // Amplitude for scaling y-axis to fill whole height - const lineThickness = 2 - const lineWidth = 4 +function drawTicks( + ctx, + x, + y, + yStart, + yEnd, + stepLength, + yMargin, + width, + height, + color = "black", +) { + const ampl = (height - 2 * yMargin) / (yStart - yEnd); // Amplitude for scaling y-axis to fill whole height + const lineThickness = 2; + const lineWidth = 4; for (let step = yStart; step >= yEnd; step -= stepLength) { drawText({ @@ -97,8 +154,8 @@ function drawTicks (ctx, x, y, yStart, yEnd, stepLength, yMargin, width, height, y: y + (yStart - step) * ampl + 2.2, text: step.toFixed(1), textSize: 10, - align: 'right' - }) + align: "right", + }); // Draw tick line drawLine({ @@ -108,12 +165,12 @@ function drawTicks (ctx, x, y, yStart, yEnd, stepLength, yMargin, width, height, x2: x, y2: y + (yStart - step) * ampl, lineWidth: lineThickness, - color - }) + color, + }); } } // Makes large numbers more readable with commas -function numberWithCommas (x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } diff --git a/assets/js/draw/shapes.js b/assets/js/draw/shapes.js index 2448d84e..59c26643 100644 --- a/assets/js/draw/shapes.js +++ b/assets/js/draw/shapes.js @@ -1,145 +1,187 @@ // draw basic objects and shapes // Draw data points -export function drawPoints ({ ctx, data, color = 'black' }) { - ctx.fillStyle = '#000' +export function drawPoints({ ctx, data, color = "black" }) { + ctx.fillStyle = "#000"; for (let i = 0; i < data.length; i += 2) { - if (data[i + 1] > 0) { // FIXME: Why are some values < 0? + if (data[i + 1] > 0) { + // FIXME: Why are some values < 0? ctx.fillRect( data[i], // x data[i + 1], // y 2, // width - 2 // height - ) + 2, // height + ); } } } // Draw 90 degree rotated text -export function drawRotatedText (ctx, text, textSize = '', posx, posy, rot, color = 'black') { - ctx.save() - ctx.fillStyle = color - ctx.font = ''.concat(textSize, 'px Arial') - ctx.translate(posx, posy) // Position for text - ctx.rotate(rot) // Rotate rot degrees - ctx.textAlign = 'center' - ctx.fillText(text, 0, 9) - ctx.restore() +export function drawRotatedText( + ctx, + text, + textSize = "", + posx, + posy, + rot, + color = "black", +) { + ctx.save(); + ctx.fillStyle = color; + ctx.font = "".concat(textSize, "px Arial"); + ctx.translate(posx, posy); // Position for text + ctx.rotate(rot); // Rotate rot degrees + ctx.textAlign = "center"; + ctx.fillText(text, 0, 9); + ctx.restore(); } // Draw aligned text at (x, y) -export function drawText ({ ctx, x, y, text, fontProp = '', align = 'center' }) { - ctx.save() - ctx.font = ''.concat(fontProp, 'px Arial') - ctx.textAlign = align - ctx.textBaseline = 'middle' - ctx.fillStyle = 'black' - ctx.fillText(text, x, y) - const textBbox = ctx.measureText(text) - ctx.restore() +export function drawText({ ctx, x, y, text, fontProp = "", align = "center" }) { + ctx.save(); + ctx.font = "".concat(fontProp, "px Arial"); + ctx.textAlign = align; + ctx.textBaseline = "middle"; + ctx.fillStyle = "black"; + ctx.fillText(text, x, y); + const textBbox = ctx.measureText(text); + ctx.restore(); return { x: x - textBbox.width / 2, y: y - textBbox.actualBoundingBoxAscent, width: textBbox.width, - height: textBbox.actualBoundingBoxAscent + textBbox.actualBoundingBoxDescent - } + height: + textBbox.actualBoundingBoxAscent + textBbox.actualBoundingBoxDescent, + }; } // Draws a line between point (x, y) and (x2, y2) -export function drawLine ({ ctx, x, y, x2, y2, lineWidth = 1, color = 'black' }) { +export function drawLine({ + ctx, + x, + y, + x2, + y2, + lineWidth = 1, + color = "black", +}) { // transpose coordinates .5 px to become sharper - x = Math.floor(x) + 0.5 - x2 = Math.floor(x2) + 0.5 - y = Math.floor(y) + 0.5 - y2 = Math.floor(y2) + 0.5 + x = Math.floor(x) + 0.5; + x2 = Math.floor(x2) + 0.5; + y = Math.floor(y) + 0.5; + y2 = Math.floor(y2) + 0.5; // draw path - ctx.strokeStyle = color - ctx.lineWidth = lineWidth - ctx.beginPath() - ctx.moveTo(x, y) - ctx.lineTo(x2, y2) - ctx.stroke() - ctx.restore() + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x2, y2); + ctx.stroke(); + ctx.restore(); } // Draws a box from top left corner with a top and bottom margin -export function drawRect ({ - ctx, x, y, width, height, lineWidth, color = null, - fillColor = null, open = false, debug = false +export function drawRect({ + ctx, + x, + y, + width, + height, + lineWidth, + color = null, + fillColor = null, + open = false, + debug = false, }) { - x = Math.floor(x) + 0.5 - y = Math.floor(y) + 0.5 - width = Math.floor(width) + x = Math.floor(x) + 0.5; + y = Math.floor(y) + 0.5; + width = Math.floor(width); - if (color !== null) ctx.strokeStyle = color - ctx.lineWidth = lineWidth + if (color !== null) ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; // define path to draw - const path = new Path2D() + const path = new Path2D(); // Draw box without left part, to allow stacking boxes // horizontally without getting double lines between them. if (open === true) { - path.moveTo(x, y) - path.lineTo(x + width, y) - path.lineTo(x + width, y + height) - path.lineTo(x, y + height) - // Draw normal 4-sided box + path.moveTo(x, y); + path.lineTo(x + width, y); + path.lineTo(x + width, y + height); + path.lineTo(x, y + height); + // Draw normal 4-sided box } else { - path.rect(x, y, width, height) + path.rect(x, y, width, height); } - ctx.stroke(path) - if (fillColor !== null) { - ctx.fillStyle = fillColor - ctx.fill(path) + ctx.stroke(path); + if (fillColor !== null) { + ctx.fillStyle = fillColor; + ctx.fill(path); } - return path + return path; } // Draw an arrow in desired direction // Forward arrow: direction = 1 // Reverse arrow: direction = -1 -export async function drawArrow ({ ctx, x, y, dir, height, lineWidth = 2, color = 'black' }) { - const width = dir * lineWidth - ctx.save() - ctx.strokeStyle = color - ctx.lineWidth = lineWidth - ctx.beginPath() - ctx.moveTo(x - width / 2, y - height / 2) - ctx.lineTo(x + width / 2, y) - ctx.moveTo(x + width / 2, y) - ctx.lineTo(x - width / 2, y + height / 2) - ctx.stroke() - ctx.restore() +export async function drawArrow({ + ctx, + x, + y, + dir, + height, + lineWidth = 2, + color = "black", +}) { + const width = dir * lineWidth; + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.beginPath(); + ctx.moveTo(x - width / 2, y - height / 2); + ctx.lineTo(x + width / 2, y); + ctx.moveTo(x + width / 2, y); + ctx.lineTo(x - width / 2, y + height / 2); + ctx.stroke(); + ctx.restore(); } // Draw a wave line from xStart to xStop at yPos where yPos is top left of the line. // Pattern is drawn by incrementing pointer by a half wave length and plot either // upward (/) or downward (\) line. // if the end is trunctated a partial wave is plotted. -export function drawWaveLine ({ ctx, x, y, x2, height, color = 'black', lineWidth = 2 }) { - ctx.save() - ctx.strokeStyle = color - ctx.lineWidth = lineWidth - ctx.beginPath() - ctx.moveTo(x, y) // begin at bottom left - const waveLength = 2 * (height / Math.tan(45)) - const lineLength = x2 - x + 1 +export function drawWaveLine({ + ctx, + x, + y, + x2, + height, + color = "black", + lineWidth = 2, +}) { + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.beginPath(); + ctx.moveTo(x, y); // begin at bottom left + const waveLength = 2 * (height / Math.tan(45)); + const lineLength = x2 - x + 1; // plot whole wave pattern - const midline = y - height / 2 // middle of line - let lastXpos = x + const midline = y - height / 2; // middle of line + let lastXpos = x; for (let i = 0; i < Math.floor(lineLength / (waveLength / 2)); i++) { - lastXpos += waveLength / 2 - height *= -1 // reverse sign - ctx.lineTo(lastXpos, midline + height / 2) // move up + lastXpos += waveLength / 2; + height *= -1; // reverse sign + ctx.lineTo(lastXpos, midline + height / 2); // move up } // plot partial wave patterns - const partialWaveLength = lineLength % (waveLength / 2) + const partialWaveLength = lineLength % (waveLength / 2); if (partialWaveLength !== 0) { - height *= -1 // reverse sign - const partialWaveHeight = partialWaveLength * Math.tan(45) - ctx.lineTo(x2, y - Math.sign(height) * partialWaveHeight) + height *= -1; // reverse sign + const partialWaveHeight = partialWaveLength * Math.tan(45); + ctx.lineTo(x2, y - Math.sign(height) * partialWaveHeight); } - ctx.stroke() - ctx.restore() + ctx.stroke(); + ctx.restore(); } diff --git a/assets/js/fetch.js b/assets/js/fetch.js index e0f87fcb..52e9c277 100644 --- a/assets/js/fetch.js +++ b/assets/js/fetch.js @@ -1,61 +1,65 @@ // Fetch.js // functions for making api requests to Gens -async function request (url, params, method = 'GET') { +async function request(url, params, method = "GET") { // options passed to hte fetch request const options = { method, headers: { - 'Content-Type': 'application/json' - } - } + "Content-Type": "application/json", + }, + }; // handle params if (params) { - if (method === 'GET') { - url += '?' + objectToQueryString(params) + if (method === "GET") { + url += "?" + objectToQueryString(params); } else { - options.body = JSON.stringify(params) + options.body = JSON.stringify(params); } } // fetch returns a promise - const response = await fetch(_apiHost + url, options) + const response = await fetch(_apiHost + url, options); if (response.status !== 200) { - return generateErrorResponse('The server responded with an unexpected status.') + return generateErrorResponse( + "The server responded with an unexpected status.", + ); } - const result = await response.json() + const result = await response.json(); // returns a single Promise object - return result + return result; } // converts an object into a query string // ex {region: 8:12-55} --> ®ion=8:12-55 -export function objectToQueryString (obj) { - return Object.keys(obj).map(key => key + '=' + obj[key]).join('&') +export function objectToQueryString(obj) { + return Object.keys(obj) + .map((key) => key + "=" + obj[key]) + .join("&"); } // A generic error handler that just returns an object with status=error and message -function generateErrorResponse (message) { +function generateErrorResponse(message) { return new Error({ - status: 'error', - message - }) + status: "error", + message, + }); } -export function get (url, params) { - return request(url, params) +export function get(url, params) { + return request(url, params); } -export function create (url, params) { - return request(url, params, 'POST') +export function create(url, params) { + return request(url, params, "POST"); } -export function update (url, params) { - return request(url, params, 'PUT') +export function update(url, params) { + return request(url, params, "PUT"); } -export function remove (url, params) { - return request(url, params, 'DELETE') +export function remove(url, params) { + return request(url, params, "DELETE"); } diff --git a/assets/js/fetch.test.js b/assets/js/fetch.test.js index d17038f0..a08eb018 100644 --- a/assets/js/fetch.test.js +++ b/assets/js/fetch.test.js @@ -1,20 +1,22 @@ -import { objectToQueryString } from './fetch.js' +import { objectToQueryString } from "./fetch.js"; -describe('Test objectToQueryString', () => { - test('test objectToQueryString single args', () => { - const paramString = objectToQueryString({region: '1:1-10'}) - expect(paramString).toBe('region=1:1-10') - }) +describe("Test objectToQueryString", () => { + test("test objectToQueryString single args", () => { + const paramString = objectToQueryString({ region: "1:1-10" }); + expect(paramString).toBe("region=1:1-10"); + }); - test('test objectToQueryString multiple args', () => { - const paramString = objectToQueryString({region: '1:1-10', page: 1}) - expect(paramString).toBe('region=1:1-10&page=1') - }) + test("test objectToQueryString multiple args", () => { + const paramString = objectToQueryString({ region: "1:1-10", page: 1 }); + expect(paramString).toBe("region=1:1-10&page=1"); + }); - test('test objectToQueryString multiple args multiple types', () => { - const paramString = objectToQueryString( - {region: '1:1-10', page: 1, print: true} - ) - expect(paramString).toBe('region=1:1-10&page=1&print=true') - }) -}) + test("test objectToQueryString multiple args multiple types", () => { + const paramString = objectToQueryString({ + region: "1:1-10", + page: 1, + print: true, + }); + expect(paramString).toBe("region=1:1-10&page=1&print=true"); + }); +}); diff --git a/assets/js/gens.js b/assets/js/gens.js index d3dee942..f27ee2e6 100644 --- a/assets/js/gens.js +++ b/assets/js/gens.js @@ -1,38 +1,102 @@ // GENS module -import { InteractiveCanvas } from './interactive.js' -import { OverviewCanvas } from './overview.js' -import { VariantTrack, AnnotationTrack, TranscriptTrack, CytogeneticIdeogram } from './track.js' +import { InteractiveCanvas } from "./interactive.js"; +import { OverviewCanvas } from "./overview.js"; +import { + VariantTrack, + AnnotationTrack, + TranscriptTrack, + CytogeneticIdeogram, +} from "./track.js"; export { - setupDrawEventManager, drawTrack, previousChromosome, nextChromosome, - panTracks, zoomIn, zoomOut, parseRegionDesignation, queryRegionOrGene -} from './navigation.js' + setupDrawEventManager, + drawTrack, + previousChromosome, + nextChromosome, + panTracks, + zoomIn, + zoomOut, + parseRegionDesignation, + queryRegionOrGene, +} from "./navigation.js"; -export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiColors, scoutBaseURL, selectedVariant, annotationFile }) { +export function initCanvases({ + sampleName, + caseId, + genomeBuild, + hgFileDir, + uiColors, + scoutBaseURL, + selectedVariant, + annotationFile, +}) { // initialize and return the different canvases // WEBGL values - const near = 0.1 - const far = 100 - const lineMargin = 2 // Margin for line thickness + const near = 0.1; + const far = 100; + const lineMargin = 2; // Margin for line thickness // Listener values - const inputField = document.getElementById('region-field') + const inputField = document.getElementById("region-field"); // Initiate interactive canvas - const ic = new InteractiveCanvas(inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) + const ic = new InteractiveCanvas( + inputField, + lineMargin, + near, + far, + caseId, + sampleName, + genomeBuild, + hgFileDir, + ); // Initiate variant, annotation and transcript canvases - const vc = new VariantTrack(ic.x, ic.plotWidth, near, far, caseId, genomeBuild, uiColors.variants, scoutBaseURL, selectedVariant) - const tc = new TranscriptTrack(ic.x, ic.plotWidth, near, far, genomeBuild, uiColors.transcripts) - const ac = new AnnotationTrack(ic.x, ic.plotWidth, near, far, genomeBuild, annotationFile) + const vc = new VariantTrack( + ic.x, + ic.plotWidth, + near, + far, + caseId, + genomeBuild, + uiColors.variants, + scoutBaseURL, + selectedVariant, + ); + const tc = new TranscriptTrack( + ic.x, + ic.plotWidth, + near, + far, + genomeBuild, + uiColors.transcripts, + ); + const ac = new AnnotationTrack( + ic.x, + ic.plotWidth, + near, + far, + genomeBuild, + annotationFile, + ); // Initiate and draw overview canvas - const oc = new OverviewCanvas(ic.x, ic.plotWidth, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) + const oc = new OverviewCanvas( + ic.x, + ic.plotWidth, + lineMargin, + near, + far, + caseId, + sampleName, + genomeBuild, + hgFileDir, + ); // Draw cytogenetic ideogram figure const cg = new CytogeneticIdeogram({ - targetId: 'cytogenetic-ideogram', + targetId: "cytogenetic-ideogram", genomeBuild, x: ic.x, y: ic.y, width: ic.plotWidth, - height: 40 - }) + height: 40, + }); return { ic: ic, vc: vc, @@ -40,36 +104,47 @@ export function initCanvases({ sampleName, caseId, genomeBuild, hgFileDir, uiCol ac: ac, oc: oc, cg: cg, - } + }; } // Make hard link and copy link to clipboard export function copyPermalink(genomeBuild, region) { // create element and add url to it - const tempElement = document.createElement('input') - const loc = window.location - tempElement.value = `${loc.host}${loc.pathname}?genome_build=${genomeBuild}®ion=${region}` + const tempElement = document.createElement("input"); + const loc = window.location; + tempElement.value = `${loc.host}${loc.pathname}?genome_build=${genomeBuild}®ion=${region}`; // add element to DOM - document.body.append(tempElement) - tempElement.select() - document.execCommand('copy') - tempElement.remove() // remove temp node + document.body.append(tempElement); + tempElement.select(); + document.execCommand("copy"); + tempElement.remove(); // remove temp node } // Reloads page to printable size export function loadPrintPage(region) { - let location = window.location.href.replace(/region=.*&/, `region=${region}&`) - location = location.includes('?') ? `${location}&print_page=true` : `${location}?print_page=true` - window.location.replace(location) + let location = window.location.href.replace( + /region=.*&/, + `region=${region}&`, + ); + location = location.includes("?") + ? `${location}&print_page=true` + : `${location}?print_page=true`; + window.location.replace(location); } // Show print prompt and reloads page after print export function printPage() { - document.querySelector('.no-print').toggleAttribute('hidden') - window.addEventListener('afterprint', () => { - window.location.replace(window.location.href.replace('&print_page=true', '')) - }, { once: true }) - print() + document.querySelector(".no-print").toggleAttribute("hidden"); + window.addEventListener( + "afterprint", + () => { + window.location.replace( + window.location.href.replace("&print_page=true", ""), + ); + }, + { once: true }, + ); + print(); } -export { CHROMOSOMES, setupGenericEventManager } from './track.js' \ No newline at end of file +export { CHROMOSOMES, setupGenericEventManager } from "./track.js"; diff --git a/assets/js/gens.test.js b/assets/js/gens.test.js index 048338d0..55d2a2fc 100644 --- a/assets/js/gens.test.js +++ b/assets/js/gens.test.js @@ -1,18 +1,20 @@ // Test functions for drawing the genecanvas -import { copyPermalink } from './gens.js' +import { copyPermalink } from "./gens.js"; -test('Test copyPermalink', () => { +test("Test copyPermalink", () => { // setup mocks - document.execCommand = jest.fn() - delete window.location - const inputElem = document.createElement('input') - document.createElement = jest.fn().mockReturnValueOnce(inputElem) - delete window.createElement - window.location = new URL('https://www.example.com/sampleId?foo=bar&doo=moo') + document.execCommand = jest.fn(); + delete window.location; + const inputElem = document.createElement("input"); + document.createElement = jest.fn().mockReturnValueOnce(inputElem); + delete window.createElement; + window.location = new URL("https://www.example.com/sampleId?foo=bar&doo=moo"); // run function - copyPermalink('38', '1:10-100') + copyPermalink("38", "1:10-100"); // expect copy to clipboard has been called - expect(document.execCommand).toHaveBeenCalledWith('copy') + expect(document.execCommand).toHaveBeenCalledWith("copy"); // assert the content copied - expect(inputElem.value).toEqual('www.example.com/sampleId?genome_build=38®ion=1:10-100') -}) + expect(inputElem.value).toEqual( + "www.example.com/sampleId?genome_build=38®ion=1:10-100", + ); +}); diff --git a/assets/js/helper.js b/assets/js/helper.js index 9f20c4a3..541ac20e 100644 --- a/assets/js/helper.js +++ b/assets/js/helper.js @@ -1,25 +1,24 @@ // Various helper functions -import { get } from './fetch.js' +import { get } from "./fetch.js"; -function cacheChromSizes (genomeBuild = '38') { - const cache = {} - return async genomeBuild => { +function cacheChromSizes(genomeBuild = "38") { + const cache = {}; + return async (genomeBuild) => { if (!cache[genomeBuild]) { - const result = await get('get-overview-chrom-dim', - { - genome_build: genomeBuild, - x_pos: 1, - y_pos: 1, - plot_width: 1 - }) - const sizes = {} + const result = await get("get-overview-chrom-dim", { + genome_build: genomeBuild, + x_pos: 1, + y_pos: 1, + plot_width: 1, + }); + const sizes = {}; for (const chrom in result.chrom_dims) { - sizes[chrom] = result.chrom_dims[chrom].size + sizes[chrom] = result.chrom_dims[chrom].size; } - cache[genomeBuild] = sizes + cache[genomeBuild] = sizes; } - return cache[genomeBuild] - } + return cache[genomeBuild]; + }; } -export const chromSizes = cacheChromSizes() +export const chromSizes = cacheChromSizes(); diff --git a/assets/js/interactive.js b/assets/js/interactive.js index 5678520a..3b283600 100644 --- a/assets/js/interactive.js +++ b/assets/js/interactive.js @@ -1,234 +1,340 @@ // Functions for rendering the interactive canvas -import { drawRotatedText, drawPoints, drawText, createGraph, drawVerticalTicks, drawGraphLines } from './draw.js' -import { drawTrack, zoomIn, zoomOut, keyLogger, limitRegionToChromosome, readInputField } from './navigation.js' -import { get } from './fetch.js' -import { BaseScatterTrack } from './track.js' +import { + drawRotatedText, + drawPoints, + drawText, + createGraph, + drawVerticalTicks, + drawGraphLines, +} from "./draw.js"; +import { + drawTrack, + zoomIn, + zoomOut, + keyLogger, + limitRegionToChromosome, + readInputField, +} from "./navigation.js"; +import { get } from "./fetch.js"; +import { BaseScatterTrack } from "./track.js"; export class InteractiveCanvas extends BaseScatterTrack { - constructor (inputField, lineMargin, near, far, caseId, sampleName, genomeBuild, hgFileDir) { - super({ caseId, sampleName, genomeBuild, hgFileDir }) + constructor( + inputField, + lineMargin, + near, + far, + caseId, + sampleName, + genomeBuild, + hgFileDir, + ) { + super({ caseId, sampleName, genomeBuild, hgFileDir }); // The canvas input field to display and fetch chromosome range from - this.inputField = inputField + this.inputField = inputField; // Plot variable - this.titleMargin = 80 // Margin between plot and title - this.legendMargin = 45 // Margin between legend and plot - this.leftRightPadding = 2 // Padding for left and right in graph - this.topBottomPadding = 8 // margin for top and bottom in graph - this.plotWidth = Math.min(1500, 0.9 * document.body.clientWidth - this.legendMargin) // Width of one plot - this.extraWidth = this.plotWidth / 1.5 // Width for loading in extra edge data - this.plotHeight = 180 // Height of one plot - this.x = document.body.clientWidth / 2 - this.plotWidth / 2 // X-position for first plot - this.y = 10 + 2 * lineMargin + this.titleMargin // Y-position for first plot - this.titleYPos = null - this.titleBbox = null - this.canvasHeight = 2 + this.y + 2 * (this.leftRightPadding + this.plotHeight) // Height for whole canvas + this.titleMargin = 80; // Margin between plot and title + this.legendMargin = 45; // Margin between legend and plot + this.leftRightPadding = 2; // Padding for left and right in graph + this.topBottomPadding = 8; // margin for top and bottom in graph + this.plotWidth = Math.min( + 1500, + 0.9 * document.body.clientWidth - this.legendMargin, + ); // Width of one plot + this.extraWidth = this.plotWidth / 1.5; // Width for loading in extra edge data + this.plotHeight = 180; // Height of one plot + this.x = document.body.clientWidth / 2 - this.plotWidth / 2; // X-position for first plot + this.y = 10 + 2 * lineMargin + this.titleMargin; // Y-position for first plot + this.titleYPos = null; + this.titleBbox = null; + this.canvasHeight = + 2 + this.y + 2 * (this.leftRightPadding + this.plotHeight); // Height for whole canvas // setup objects for tracking the positions of draw and content canvases - this.offscreenPosition = { start: null, end: null, scale: null } - this.onscreenPosition = { start: null, end: null } + this.offscreenPosition = { start: null, end: null, scale: null }; + this.onscreenPosition = { start: null, end: null }; // BAF values this.baf = { yStart: 1.0, // Start value for y axis yEnd: 0.0, // End value for y axis step: 0.2, // Step value for drawing ticks along y-axis - color: '#000000' // Viz color - } + color: "#000000", // Viz color + }; // Log2 ratio values this.log2 = { yStart: 4.0, // Start value for y axis yEnd: -4.0, // End value for y axis step: 1.0, // Step value for drawing ticks along y-axis - color: '#000000' // Viz color - } + color: "#000000", // Viz color + }; // Setup draw canvas - this.drawWidth = Math.max(this.plotWidth + 2 * this.extraWidth, document.body.clientWidth) // Draw-canvas width - this.drawCanvas.width = parseInt(this.drawWidth) - this.drawCanvas.height = parseInt(this.canvasHeight) + this.drawWidth = Math.max( + this.plotWidth + 2 * this.extraWidth, + document.body.clientWidth, + ); // Draw-canvas width + this.drawCanvas.width = parseInt(this.drawWidth); + this.drawCanvas.height = parseInt(this.canvasHeight); // Setup visible canvases - this.contentCanvas = document.getElementById('interactive-content') - this.staticCanvas = document.getElementById('interactive-static') - this.staticCanvas.width = this.contentCanvas.width = document.body.clientWidth - this.staticCanvas.height = this.contentCanvas.height = this.canvasHeight + this.contentCanvas = document.getElementById("interactive-content"); + this.staticCanvas = document.getElementById("interactive-static"); + this.staticCanvas.width = this.contentCanvas.width = + document.body.clientWidth; + this.staticCanvas.height = this.contentCanvas.height = this.canvasHeight; // this.drawCanvas = this.contentCanvas // Setup loading div dimensions - this.loadingDiv = document.getElementById('loading-div') - this.loadingDiv.style.width = `${this.plotWidth - 2.5}px` - this.loadingDiv.style.left = `${this.x + 2}px` - this.loadingDiv.style.top = `${this.y + 82}px` - this.loadingDiv.style.height = `${this.plotHeight * 2 - 2.5}px` + this.loadingDiv = document.getElementById("loading-div"); + this.loadingDiv.style.width = `${this.plotWidth - 2.5}px`; + this.loadingDiv.style.left = `${this.x + 2}px`; + this.loadingDiv.style.top = `${this.y + 82}px`; + this.loadingDiv.style.height = `${this.plotHeight * 2 - 2.5}px`; // Initialize marker div - this.markerElem = document.getElementById('interactive-marker') - this.markerElem.style.height = `${this.plotHeight * 2}px` - this.markerElem.style.top = `${this.y + 131}px` + this.markerElem = document.getElementById("interactive-marker"); + this.markerElem.style.height = `${this.plotHeight * 2}px`; + this.markerElem.style.top = `${this.y + 131}px`; // State values - this.allowDraw = true + this.allowDraw = true; // Listener values - this.markingRegion = false - this.drag = false - this.dragStart = {} - this.dragEnd = {} + this.markingRegion = false; + this.drag = false; + this.dragStart = {}; + this.dragEnd = {}; - this.scale = this.calcScale() + this.scale = this.calcScale(); // Setup listeners // update chromosome title event - this.contentCanvas.addEventListener('update-title', event => { - console.log(`interactive got an ${event.type} event`) - const len = event.detail.bands.length + this.contentCanvas.addEventListener("update-title", (event) => { + console.log(`interactive got an ${event.type} event`); + const len = event.detail.bands.length; if (len > 0) { - const bandIds = len === 1 ? event.detail.bands[0].id : `${event.detail.bands[0].id}-${event.detail.bands[len - 1].id}` - this.drawCanvas.getContext('2d').clearRect(this.titleBbox.x, this.titleBbox.y, this.titleBbox.width, this.titleBbox.height) - this.titleBbox = this.drawTitle(`Chromosome ${event.detail.chrom}; ${bandIds}`) - this.blitChromName({textPosition: this.titleBbox}) + const bandIds = + len === 1 + ? event.detail.bands[0].id + : `${event.detail.bands[0].id}-${event.detail.bands[len - 1].id}`; + this.drawCanvas + .getContext("2d") + .clearRect( + this.titleBbox.x, + this.titleBbox.y, + this.titleBbox.width, + this.titleBbox.height, + ); + this.titleBbox = this.drawTitle( + `Chromosome ${event.detail.chrom}; ${bandIds}`, + ); + this.blitChromName({ textPosition: this.titleBbox }); } - }) + }); // redraw events - this.contentCanvas.parentElement.addEventListener('draw', event => { - console.log('interactive got draw event') - this.drawInteractiveContent({ ...event.detail.region, ...event.detail }) - }) + this.contentCanvas.parentElement.addEventListener("draw", (event) => { + console.log("interactive got draw event"); + this.drawInteractiveContent({ ...event.detail.region, ...event.detail }); + }); // navigation events - this.contentCanvas.addEventListener('mousedown', (event) => { - event.stopPropagation() + this.contentCanvas.addEventListener("mousedown", (event) => { + event.stopPropagation(); if (this.allowDraw && !this.drag) { if (keyLogger.heldKeys.Control) { - zoomOut() - } else { // Related to dragging + zoomOut(); + } else { + // Related to dragging // If region should be marked if (keyLogger.heldKeys.Shift) { - this.markingRegion = true + this.markingRegion = true; } // Make sure scale factor is updated - this.scale = this.calcScale() + this.scale = this.calcScale(); // store coordinates this.dragStart = { x: event.x, - y: event.y - } + y: event.y, + }; this.dragEnd = { x: event.x, - y: event.y - } - this.drag = true + y: event.y, + }; + this.drag = true; } } - }) + }); // When in active dragging of the canvas - this.contentCanvas.addEventListener('mousemove', (event) => { - event.preventDefault() - event.stopPropagation() + this.contentCanvas.addEventListener("mousemove", (event) => { + event.preventDefault(); + event.stopPropagation(); // If region should be marked if (keyLogger.heldKeys.Shift && this.allowDraw) { - this.markingRegion = true + this.markingRegion = true; } if (this.drag) { this.dragEnd = { x: event.x, - y: event.y - } + y: event.y, + }; if (this.markingRegion) { - this.markRegion({ start: this.dragStart.x, end: this.dragEnd.x }) + this.markRegion({ start: this.dragStart.x, end: this.dragEnd.x }); } else { // pan content canvas - this.panContent(this.dragEnd.x - this.dragStart.x) + this.panContent(this.dragEnd.x - this.dragStart.x); } } - }) + }); // When stop dragging - this.contentCanvas.addEventListener('mouseup', (event) => { - event.preventDefault() - event.stopPropagation() + this.contentCanvas.addEventListener("mouseup", (event) => { + event.preventDefault(); + event.stopPropagation(); // reset marking region if (this.markingRegion) { - this.markingRegion = false - this.resetRegionMarker() - const scale = this.calcScale() - const rawStart = this.onscreenPosition.start + Math.round((this.dragStart.x - this.x) / scale) - const rawEnd = rawStart + Math.round((this.dragEnd.x - this.dragStart.x) / scale) + this.markingRegion = false; + this.resetRegionMarker(); + const scale = this.calcScale(); + const rawStart = + this.onscreenPosition.start + + Math.round((this.dragStart.x - this.x) / scale); + const rawEnd = + rawStart + Math.round((this.dragEnd.x - this.dragStart.x) / scale); // sort positions so lowest number is allways start - const [start, end] = [rawStart, rawEnd].sort((a, b) => a - b) + const [start, end] = [rawStart, rawEnd].sort((a, b) => a - b); // if shift - click, zoom in a region 10 - if ((end - start) < 10) { - zoomIn() + if (end - start < 10) { + zoomIn(); } else { drawTrack({ chrom: this.chromosome, start: start, end: end, - exclude: ['cytogenetic-ideogram'], + exclude: ["cytogenetic-ideogram"], force: true, - drawTitle: false - }) + drawTitle: false, + }); } } else if (this.drag) { // reload window when stop draging - drawTrack({ ...readInputField(), force: true, displayLoading: false, drawTitle: false }) + drawTrack({ + ...readInputField(), + force: true, + displayLoading: false, + drawTitle: false, + }); } // reset dragging behaviour - this.markingRegion = false - this.drag = false - }) + this.markingRegion = false; + this.drag = false; + }); } // Draw static content for interactive canvas - async drawStaticContent () { - const linePadding = 2 - const staticContext = this.staticCanvas.getContext('2d') + async drawStaticContent() { + const linePadding = 2; + const staticContext = this.staticCanvas.getContext("2d"); // Fill background colour - staticContext.fillStyle = '#F7F9F9' - staticContext.fillRect(0, 0, this.staticCanvas.width, this.staticCanvas.height) + staticContext.fillStyle = "#F7F9F9"; + staticContext.fillRect( + 0, + 0, + this.staticCanvas.width, + this.staticCanvas.height, + ); // Make content area visible // content window - staticContext.clearRect(this.x + linePadding, this.y + linePadding, - this.plotWidth, this.staticCanvas.height) + staticContext.clearRect( + this.x + linePadding, + this.y + linePadding, + this.plotWidth, + this.staticCanvas.height, + ); // area for ticks above content area - staticContext.clearRect(0, 0, this.staticCanvas.width, this.y + linePadding) + staticContext.clearRect( + 0, + 0, + this.staticCanvas.width, + this.y + linePadding, + ); // Draw rotated y-axis legends - drawRotatedText(staticContext, 'B Allele Freq', 18, this.x - this.legendMargin, - this.y + this.plotHeight / 2, -Math.PI / 2, this.titleColor) - drawRotatedText(staticContext, 'Log2 Ratio', 18, this.x - this.legendMargin, - this.y + 1.5 * this.plotHeight, -Math.PI / 2, this.titleColor) + drawRotatedText( + staticContext, + "B Allele Freq", + 18, + this.x - this.legendMargin, + this.y + this.plotHeight / 2, + -Math.PI / 2, + this.titleColor, + ); + drawRotatedText( + staticContext, + "Log2 Ratio", + 18, + this.x - this.legendMargin, + this.y + 1.5 * this.plotHeight, + -Math.PI / 2, + this.titleColor, + ); // Draw BAF - createGraph(staticContext, this.x, this.y, this.plotWidth, - this.plotHeight, this.topBottomPadding, this.baf.yStart, this.baf.yEnd, - this.baf.step, true, this.borderColor) + createGraph( + staticContext, + this.x, + this.y, + this.plotWidth, + this.plotHeight, + this.topBottomPadding, + this.baf.yStart, + this.baf.yEnd, + this.baf.step, + true, + this.borderColor, + ); // Draw Log 2 ratio - createGraph(staticContext, this.x, this.y + this.plotHeight, - this.plotWidth, this.plotHeight, this.topBottomPadding, this.log2.yStart, - this.log2.yEnd, this.log2.step, true, this.borderColor) + createGraph( + staticContext, + this.x, + this.y + this.plotHeight, + this.plotWidth, + this.plotHeight, + this.topBottomPadding, + this.log2.yStart, + this.log2.yEnd, + this.log2.step, + true, + this.borderColor, + ); // Transfer image to visible canvas - staticContext.drawImage(this.drawCanvas, 0, 0) + staticContext.drawImage(this.drawCanvas, 0, 0); } // Draw values for interactive canvas - async drawInteractiveContent ({ chrom, start, end, displayLoading = true, drawTitle = true }) { - console.log('drawing interactive canvas', chrom, start, end) + async drawInteractiveContent({ + chrom, + start, + end, + displayLoading = true, + drawTitle = true, + }) { + console.log("drawing interactive canvas", chrom, start, end); if (displayLoading) { - this.loadingDiv.style.display = 'block' + this.loadingDiv.style.display = "block"; } else { - document.getElementsByTagName('body')[0].style.cursor = 'wait' + document.getElementsByTagName("body")[0].style.cursor = "wait"; } - console.time('getcoverage') - get('get-coverage', { + console.time("getcoverage"); + get("get-coverage", { region: `${chrom}:${start}-${end}`, case_id: this.caseId, sample_id: this.sampleName, @@ -244,173 +350,186 @@ export class InteractiveCanvas extends BaseScatterTrack { baf_y_end: this.baf.yEnd, log2_y_start: this.log2.yStart, log2_y_end: this.log2.yEnd, - reduce_data: 1 - }).then(result => { - console.timeEnd('getcoverage') - if (result.status === 'error') { - throw new Error(result) - } - // store new start and end values - this.offscreenPosition = { - start: parseInt(result.padded_start), - end: parseInt(result.padded_end) - } - this.offscreenPosition.scale = this.drawWidth / (this.offscreenPosition.end - this.offscreenPosition.start) - this.chromosome = chrom - // clear draw and content canvas - const ctx = this.drawCanvas.getContext('2d') - ctx.clearRect( - 0, 0, this.drawCanvas.width, this.drawCanvas.height - ) - - drawVerticalTicks({ - ctx, - renderX: 0, - y: this.y, - xStart: start, - xEnd: end, - xoStart: this.offscreenPosition.start, - xoEnd: this.offscreenPosition.end, - width: this.plotWidth, - yMargin: this.topBottomPadding, - titleColor: this.titleColor - }) - - // Draw horizontal lines for BAF and Log 2 ratio - drawGraphLines({ - ctx, - x: 0, - y: result.y_pos, - yStart: this.baf.yStart, - yEnd: this.baf.yEnd, - stepLength: this.baf.step, - yMargin: this.topBottomPadding, - width: this.drawWidth, - height: this.plotHeight - }) - drawGraphLines({ - ctx, - x: 0, - y: result.y_pos + this.plotHeight, - yStart: this.log2.yStart, - yEnd: this.log2.yEnd, - stepLength: this.log2.step, - yMargin: this.topBottomPadding, - width: this.drawWidth, - height: this.plotHeight - }) + reduce_data: 1, + }) + .then((result) => { + console.timeEnd("getcoverage"); + if (result.status === "error") { + throw new Error(result); + } + // store new start and end values + this.offscreenPosition = { + start: parseInt(result.padded_start), + end: parseInt(result.padded_end), + }; + this.offscreenPosition.scale = + this.drawWidth / + (this.offscreenPosition.end - this.offscreenPosition.start); + this.chromosome = chrom; + // clear draw and content canvas + const ctx = this.drawCanvas.getContext("2d"); + ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); + + drawVerticalTicks({ + ctx, + renderX: 0, + y: this.y, + xStart: start, + xEnd: end, + xoStart: this.offscreenPosition.start, + xoEnd: this.offscreenPosition.end, + width: this.plotWidth, + yMargin: this.topBottomPadding, + titleColor: this.titleColor, + }); + + // Draw horizontal lines for BAF and Log 2 ratio + drawGraphLines({ + ctx, + x: 0, + y: result.y_pos, + yStart: this.baf.yStart, + yEnd: this.baf.yEnd, + stepLength: this.baf.step, + yMargin: this.topBottomPadding, + width: this.drawWidth, + height: this.plotHeight, + }); + drawGraphLines({ + ctx, + x: 0, + y: result.y_pos + this.plotHeight, + yStart: this.log2.yStart, + yEnd: this.log2.yEnd, + stepLength: this.log2.step, + yMargin: this.topBottomPadding, + width: this.drawWidth, + height: this.plotHeight, + }); + + // Plot scatter data + drawPoints({ + ctx, + data: result.baf, + color: this.baf.color, + }); + drawPoints({ + ctx, + data: result.data, + color: this.log2.color, + }); + + // Transfer image to visible canvas + this.blitInteractiveCanvas({ start, end }); + // Draw chromosome title on the content canvas as a blitting + // work around + this.titleYPos = result.y_pos - this.titleMargin; + if (drawTitle) { + this.titleBbox !== null && + this.blitChromName({ + textPosition: this.titleBbox, + clearOnly: true, + }); + this.titleBbox = this.drawTitle(`Chromosome ${result.chrom}`); + this.blitChromName({ textPosition: this.titleBbox }); + } - // Plot scatter data - drawPoints({ - ctx, data: result.baf, color: this.baf.color + return result; }) - drawPoints({ - ctx, data: result.data, color: this.log2.color + .then((result) => { + if (displayLoading) { + this.loadingDiv.style.display = "none"; + } else { + document.getElementsByTagName("body")[0].style.cursor = "auto"; + } + this.allowDraw = true; }) + .catch((error) => { + this.allowDraw = true; - // Transfer image to visible canvas - this.blitInteractiveCanvas({ start, end }) - // Draw chromosome title on the content canvas as a blitting - // work around - this.titleYPos = result.y_pos - this.titleMargin - if ( drawTitle ) { - this.titleBbox !== null && this.blitChromName( - {textPosition: this.titleBbox, clearOnly: true - }) - this.titleBbox = this.drawTitle(`Chromosome ${result.chrom}`) - this.blitChromName({textPosition: this.titleBbox}) - } - - return result - }).then((result) => { - if (displayLoading) { - this.loadingDiv.style.display = 'none' - } else { - document.getElementsByTagName('body')[0].style.cursor = 'auto' - } - this.allowDraw = true - }).catch(error => { - this.allowDraw = true - - this.inputField.dispatchEvent( - new CustomEvent('error', { detail: { error: error } }) - ) - }) + this.inputField.dispatchEvent( + new CustomEvent("error", { detail: { error: error } }), + ); + }); } drawTitle(title) { - const ctx = this.drawCanvas.getContext('2d') + const ctx = this.drawCanvas.getContext("2d"); return drawText({ ctx, x: Math.round(document.body.clientWidth / 2), y: this.titleYPos, text: title, - fontProp: 'bold 15', - align: 'center' - }) + fontProp: "bold 15", + align: "center", + }); } - calcScale () { - return this.plotWidth / (this.onscreenPosition.end - this.onscreenPosition.start) + calcScale() { + return ( + this.plotWidth / (this.onscreenPosition.end - this.onscreenPosition.start) + ); } // Function for highlighting region - markRegion ({ start, end }) { + markRegion({ start, end }) { // Update the dom element - this.markerElem.style.left = start < end ? `${start}px` : `${end}px` - this.markerElem.style.width = `${Math.abs(end - start) + 1}px` - this.markerElem.hidden = false + this.markerElem.style.left = start < end ? `${start}px` : `${end}px`; + this.markerElem.style.width = `${Math.abs(end - start) + 1}px`; + this.markerElem.hidden = false; } - resetRegionMarker () { - this.markerElem.style.left = '0px' - this.markerElem.style.width = '0px' - this.markerElem.hidden = true + resetRegionMarker() { + this.markerElem.style.left = "0px"; + this.markerElem.style.width = "0px"; + this.markerElem.hidden = true; } - blitChromName ({textPosition, clearOnly=false}) { - const ctx = this.contentCanvas.getContext('2d') - const padding = 20 + blitChromName({ textPosition, clearOnly = false }) { + const ctx = this.contentCanvas.getContext("2d"); + const padding = 20; // clear area on contentCanvas ctx.clearRect( textPosition.x - padding / 2, textPosition.y, textPosition.width + padding, - textPosition.height - ) + textPosition.height, + ); // transfer from draw canvas - !clearOnly && ctx.drawImage( - this.drawCanvas, // source - textPosition.x - padding / 2, // sX - textPosition.y, // sY - textPosition.width + padding, // sWidth - textPosition.height, // sHeight - textPosition.x - padding / 2, // dX - textPosition.y, // dY - textPosition.width + padding, // dWidth - textPosition.height // dHeight - ) + !clearOnly && + ctx.drawImage( + this.drawCanvas, // source + textPosition.x - padding / 2, // sX + textPosition.y, // sY + textPosition.width + padding, // sWidth + textPosition.height, // sHeight + textPosition.x - padding / 2, // dX + textPosition.y, // dY + textPosition.width + padding, // dWidth + textPosition.height, // dHeight + ); } - blitInteractiveCanvas ({ start, end, updateCoord = true }) { + blitInteractiveCanvas({ start, end, updateCoord = true }) { // blit areas from the drawCanvas to content canvas. // start, end are onscreen position - const width = end - start + const width = end - start; // store onscreen coords - if (updateCoord) this.onscreenPosition = { start: start, end: end } + if (updateCoord) this.onscreenPosition = { start: start, end: end }; const offscreenOffset = Math.round( - (start - this.offscreenPosition.start) * this.offscreenPosition.scale) - const offscSegmentWidth = Math.round(width * this.offscreenPosition.scale) - const onscSegmentWidth = width * this.calcScale() + (start - this.offscreenPosition.start) * this.offscreenPosition.scale, + ); + const offscSegmentWidth = Math.round(width * this.offscreenPosition.scale); + const onscSegmentWidth = width * this.calcScale(); // clear current canvas - const ctx = this.contentCanvas.getContext('2d') + const ctx = this.contentCanvas.getContext("2d"); ctx.clearRect( 0, this.titleMargin / 2 - 2, this.contentCanvas.width, - this.contentCanvas.height - ) + this.contentCanvas.height, + ); // normalize the genomic coordinates to screen coordinates ctx.drawImage( this.drawCanvas, // source image @@ -421,27 +540,34 @@ export class InteractiveCanvas extends BaseScatterTrack { this.x, // dX this.titleMargin / 2, // dY onscSegmentWidth, // dWidth - this.contentCanvas.height // dHeight - ) + this.contentCanvas.height, // dHeight + ); } // Move track x distance - async panContent (distance) { + async panContent(distance) { // calculate the chromosome positions - const scale = this.calcScale() - const dist = distance / scale + const scale = this.calcScale(); + const dist = distance / scale; const region = await limitRegionToChromosome({ chrom: this.chromosome, start: this.onscreenPosition.start - dist, - end: this.onscreenPosition.end - dist - }) + end: this.onscreenPosition.end - dist, + }); // Copy draw image to content Canvas - this.blitInteractiveCanvas({ start: region.start, end: region.end, updateCoord: false }) + this.blitInteractiveCanvas({ + start: region.start, + end: region.end, + updateCoord: false, + }); drawTrack({ ...region, - exclude: [`${this.contentCanvas.parentElement.id}`, 'cytogenetic-ideogram'], - displayLoading: false - }) + exclude: [ + `${this.contentCanvas.parentElement.id}`, + "cytogenetic-ideogram", + ], + displayLoading: false, + }); } } diff --git a/assets/js/navigation.js b/assets/js/navigation.js index 0b8164c0..d502b8bf 100644 --- a/assets/js/navigation.js +++ b/assets/js/navigation.js @@ -1,52 +1,52 @@ -import { get } from './fetch.js' -import { CHROMOSOMES } from './track.js' -import { chromSizes } from './helper.js' +import { get } from "./fetch.js"; +import { CHROMOSOMES } from "./track.js"; +import { chromSizes } from "./helper.js"; function redrawEvent({ region, exclude = [], ...kwargs }) { - return new CustomEvent( - 'draw', { detail: { region: region, exclude: exclude, ...kwargs } } - ) + return new CustomEvent("draw", { + detail: { region: region, exclude: exclude, ...kwargs }, + }); } function drawEventManager({ target, throttleTime }) { const tracks = [ - ...target.querySelectorAll('.track-container'), - target.querySelector('#cytogenetic-ideogram') - ] - let lastEventTime = 0 + ...target.querySelectorAll(".track-container"), + target.querySelector("#cytogenetic-ideogram"), + ]; + let lastEventTime = 0; return (event) => { - const now = Date.now() - console.log(`Test event times ${lastEventTime} ? ${now}, diff: ${now - lastEventTime}`) - if (throttleTime < Date.now() - lastEventTime || - event.detail.force - ) { - lastEventTime = Date.now() + const now = Date.now(); + console.log( + `Test event times ${lastEventTime} ? ${now}, diff: ${now - lastEventTime}`, + ); + if (throttleTime < Date.now() - lastEventTime || event.detail.force) { + lastEventTime = Date.now(); for (const track of tracks) { if (!event.detail.exclude.includes(track.id)) { - track.dispatchEvent(redrawEvent({ ...event.detail })) + track.dispatchEvent(redrawEvent({ ...event.detail })); } } } - } + }; } export function setupDrawEventManager({ target, throttleTime = 20 }) { - const manager = drawEventManager({ target, throttleTime }) - target.addEventListener('draw', (event) => { - manager(event) - }) + const manager = drawEventManager({ target, throttleTime }); + target.addEventListener("draw", (event) => { + manager(event); + }); } export function readInputField() { - const field = document.getElementById('region-field') - return parseRegionDesignation(field.value) + const field = document.getElementById("region-field"); + return parseRegionDesignation(field.value); } function updateInputField({ chrom, start, end }) { - const field = document.getElementById('region-field') - field.value = `${chrom}:${start}-${end}` - field.placeholder = field.value - field.blur() + const field = document.getElementById("region-field"); + field.value = `${chrom}:${start}-${end}`; + field.placeholder = field.value; + field.blur(); } // parse chromosomal region designation string @@ -55,132 +55,170 @@ function updateInputField({ chrom, start, end }) { // 1: --> 1, null, null // 1 --> 1, null, null export function parseRegionDesignation(regionString) { - if (regionString.includes(':')) { - const [chromosome, position] = regionString.split(':') + if (regionString.includes(":")) { + const [chromosome, position] = regionString.split(":"); // verify chromosome if (!CHROMOSOMES.includes(chromosome)) { - throw new Error(`${chromosome} is not a valid chromosome`) + throw new Error(`${chromosome} is not a valid chromosome`); } - let [start, end] = position.split('-') - start = parseInt(start) - end = parseInt(end) - return { chrom: chromosome, start: start, end: end } + let [start, end] = position.split("-"); + start = parseInt(start); + end = parseInt(end); + return { chrom: chromosome, start: start, end: end }; } } -export async function limitRegionToChromosome({ chrom, start, end, genomeBuild = '38' }) { +export async function limitRegionToChromosome({ + chrom, + start, + end, + genomeBuild = "38", +}) { // assert that start/stop are within start and end of chromosome - const sizes = await chromSizes(genomeBuild) - const chromSize = sizes[chrom] - start = start === null ? 1 : start - end = end === null ? chromSize : end + const sizes = await chromSizes(genomeBuild); + const chromSize = sizes[chrom]; + start = start === null ? 1 : start; + end = end === null ? chromSize : end; // ensure the window size stay the same - const windowSize = end - start + 1 >= chromSize ? chromSize : end - start - let updStart, updEnd + const windowSize = end - start + 1 >= chromSize ? chromSize : end - start; + let updStart, updEnd; if (windowSize >= chromSize) { - updStart = 1 - updEnd = chromSize + updStart = 1; + updEnd = chromSize; } else if (start < 1) { - updStart = 1 - updEnd = windowSize + updStart = 1; + updEnd = windowSize; } else if (end > chromSize) { - updStart = chromSize - windowSize - updEnd = chromSize + updStart = chromSize - windowSize; + updEnd = chromSize; } else { - updStart = start - updEnd = end + updStart = start; + updEnd = end; } - return { chrom: chrom, start: Math.round(updStart), end: Math.round(updEnd) } + return { chrom: chrom, start: Math.round(updStart), end: Math.round(updEnd) }; } export async function drawTrack({ - chrom, start, end, genomeBuild = '38', - exclude = [], force = false, ...kwargs + chrom, + start, + end, + genomeBuild = "38", + exclude = [], + force = false, + ...kwargs }) { // update input field - const region = await limitRegionToChromosome({ chrom, start, end, genomeBuild }) - updateInputField({ ...region }) - const trackContainer = document.getElementById('visualization-container') + const region = await limitRegionToChromosome({ + chrom, + start, + end, + genomeBuild, + }); + updateInputField({ ...region }); + const trackContainer = document.getElementById("visualization-container"); trackContainer.dispatchEvent( - redrawEvent({ region, exclude, force, ...kwargs }) - ) + redrawEvent({ region, exclude, force, ...kwargs }), + ); // make overview update its region marking - const markRegionEvent = new CustomEvent('mark-region', { detail: { region: region } }) - document.getElementById('overview-container').dispatchEvent(markRegionEvent) - document.getElementById('cytogenetic-ideogram').dispatchEvent(markRegionEvent) + const markRegionEvent = new CustomEvent("mark-region", { + detail: { region: region }, + }); + document.getElementById("overview-container").dispatchEvent(markRegionEvent); + document + .getElementById("cytogenetic-ideogram") + .dispatchEvent(markRegionEvent); } // If query is a regionString draw the relevant region // If input is a chromosome display entire chromosome // Else query api for genes with that name and draw that region export function queryRegionOrGene(query, genomeBuild = 38) { - if (query.includes(':')) { - drawTrack(parseRegionDesignation(query)) + if (query.includes(":")) { + drawTrack(parseRegionDesignation(query)); } else if (CHROMOSOMES.includes(query)) { - drawTrack({ chrom: query, start: 1, end: null }) + drawTrack({ chrom: query, start: 1, end: null }); } else { - get('search-annotation', { query: query, genome_build: genomeBuild }) - .then(result => { + get("search-annotation", { query: query, genome_build: genomeBuild }).then( + (result) => { if (result.status === 200) { drawTrack({ chrom: result.chromosome, start: result.start_pos, - end: result.end_pos - }) + end: result.end_pos, + }); } - }) + }, + ); } } // goto the next chromosome export function nextChromosome() { - const position = readInputField() - const chrom = CHROMOSOMES[CHROMOSOMES.indexOf(position.chrom) + 1] - drawTrack({ chrom: chrom, start: 1, end: null }) + const position = readInputField(); + const chrom = CHROMOSOMES[CHROMOSOMES.indexOf(position.chrom) + 1]; + drawTrack({ chrom: chrom, start: 1, end: null }); } // goto the previous chromosome export function previousChromosome() { - const position = readInputField() - const chrom = CHROMOSOMES[CHROMOSOMES.indexOf(position.chrom) - 1] - drawTrack({ chrom: chrom, start: 1, end: null }) + const position = readInputField(); + const chrom = CHROMOSOMES[CHROMOSOMES.indexOf(position.chrom) - 1]; + drawTrack({ chrom: chrom, start: 1, end: null }); } // Pan whole canvas and tracks to the left -export function panTracks(direction = 'left', speed = 0.1) { - const pos = readInputField() - const distance = Math.abs(Math.floor(speed * (pos.end - pos.start))) - if (direction === 'left') { - pos.start -= distance - pos.end -= distance +export function panTracks(direction = "left", speed = 0.1) { + const pos = readInputField(); + const distance = Math.abs(Math.floor(speed * (pos.end - pos.start))); + if (direction === "left") { + pos.start -= distance; + pos.end -= distance; } else { - pos.start += distance - pos.end += distance + pos.start += distance; + pos.end += distance; } // drawTrack will correct the window eventually, but let us not go negative at least if (pos.start < 0) { - pos.end = pos.end - pos.start - pos.start = 1 + pos.end = pos.end - pos.start; + pos.start = 1; } - drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, drawTitle: false, exclude: ['cytogenetic-ideogram'] }) + drawTrack({ + chrom: pos.chrom, + start: pos.start, + end: pos.end, + drawTitle: false, + exclude: ["cytogenetic-ideogram"], + }); } // Handle zoom in button click export function zoomIn() { - const pos = readInputField() - const factor = Math.floor((pos.end - pos.start) * 0.2) - pos.start += factor - pos.end -= factor - drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, exclude: ['cytogenetic-ideogram'], drawTitle: false }) + const pos = readInputField(); + const factor = Math.floor((pos.end - pos.start) * 0.2); + pos.start += factor; + pos.end -= factor; + drawTrack({ + chrom: pos.chrom, + start: pos.start, + end: pos.end, + exclude: ["cytogenetic-ideogram"], + drawTitle: false, + }); } // Handle zoom out button click export function zoomOut() { - const pos = readInputField() - const factor = Math.floor((pos.end - pos.start) / 3) - pos.start = (pos.start - factor) < 1 ? 1 : pos.start - factor - pos.end += factor - drawTrack({ chrom: pos.chrom, start: pos.start, end: pos.end, exclude: ['cytogenetic-ideogram'], drawTitle: false }) + const pos = readInputField(); + const factor = Math.floor((pos.end - pos.start) / 3); + pos.start = pos.start - factor < 1 ? 1 : pos.start - factor; + pos.end += factor; + drawTrack({ + chrom: pos.chrom, + start: pos.start, + end: pos.end, + exclude: ["cytogenetic-ideogram"], + drawTitle: false, + }); } // Dispatch dispatch an event to draw a given region @@ -189,95 +227,100 @@ class KeyLogger { // Records keypress combinations constructor(bufferSize = 10) { // Setup variables - this.bufferSize = bufferSize - this.lastKeyTime = Date.now() - this.heldKeys = {} // store held keys - this.keyBuffer = [] // store recent keys + this.bufferSize = bufferSize; + this.lastKeyTime = Date.now(); + this.heldKeys = {}; // store held keys + this.keyBuffer = []; // store recent keys // Setup event listending functions - document.addEventListener('keydown', event => { + document.addEventListener("keydown", (event) => { // store event const eventData = { key: event.key, target: window.event.target.nodeName, - time: Date.now() - } - const keyEvent = new CustomEvent('keyevent', { detail: eventData }) - this.heldKeys[event.key] = true // recored pressed keys - this.keyBuffer.push(eventData) + time: Date.now(), + }; + const keyEvent = new CustomEvent("keyevent", { detail: eventData }); + this.heldKeys[event.key] = true; // recored pressed keys + this.keyBuffer.push(eventData); // empty buffer - while (this.keyBuffer.length > this.bufferSize) { this.keyBuffer.shift() } - document.dispatchEvent(keyEvent) // event information - }) - document.addEventListener('keyup', event => { - delete this.heldKeys[event.key] - }) + while (this.keyBuffer.length > this.bufferSize) { + this.keyBuffer.shift(); + } + document.dispatchEvent(keyEvent); // event information + }); + document.addEventListener("keyup", (event) => { + delete this.heldKeys[event.key]; + }); } recentKeys(timeWindow) { // get keys pressed within a window of time. - const currentTime = Date.now() - return this.keyBuffer.filter(keyEvent => - timeWindow > currentTime - keyEvent.time) + const currentTime = Date.now(); + return this.keyBuffer.filter( + (keyEvent) => timeWindow > currentTime - keyEvent.time, + ); } lastKeypressTime() { - return this.keyBuffer[this.keyBuffer.length - 1] - Date.now() + return this.keyBuffer[this.keyBuffer.length - 1] - Date.now(); } } -export const keyLogger = new KeyLogger() +export const keyLogger = new KeyLogger(); // Setup handling of keydown events -const keystrokeDelay = 2000 -document.addEventListener('keyevent', event => { - const key = event.detail.key +const keystrokeDelay = 2000; +document.addEventListener("keyevent", (event) => { + const key = event.detail.key; // dont act on key presses in input fields - const excludeFileds = ['input', 'select', 'textarea'] + const excludeFileds = ["input", "select", "textarea"]; if (!excludeFileds.includes(event.detail.target.toLowerCase())) { - if (key === 'Enter') { + if (key === "Enter") { // Enter was pressed, process previous key presses. - const recentKeys = keyLogger.recentKeys(keystrokeDelay) - recentKeys.pop() // skip Enter key - const lastKey = recentKeys[recentKeys.length - 1] - const numKeys = parseInt((recentKeys - .slice(lastKey.length - 2) - .filter(val => parseInt(val.key)) - .map(val => val.key) - .join(''))) + const recentKeys = keyLogger.recentKeys(keystrokeDelay); + recentKeys.pop(); // skip Enter key + const lastKey = recentKeys[recentKeys.length - 1]; + const numKeys = parseInt( + recentKeys + .slice(lastKey.length - 2) + .filter((val) => parseInt(val.key)) + .map((val) => val.key) + .join(""), + ); // process keys - if (lastKey.key === 'x' || lastKey.key === 'y') { - drawTrack({ region: lastKey.key }) + if (lastKey.key === "x" || lastKey.key === "y") { + drawTrack({ region: lastKey.key }); } else if (numKeys && numKeys > 0 < 23) { - drawTrack({ region: numKeys }) + drawTrack({ region: numKeys }); } else { - return + return; } } switch (key) { - case 'ArrowLeft': - previousChromosome() - break - case 'ArrowRight': - nextChromosome() - break - case 'a': - panTracks('left', 0.7) - break - case 'd': - panTracks('right', 0.7) - break - case 'ArrowUp': - case 'w': - case '+': - zoomIn() - break - case 'ArrowDown': - case 's': - case '-': - zoomOut() - break + case "ArrowLeft": + previousChromosome(); + break; + case "ArrowRight": + nextChromosome(); + break; + case "a": + panTracks("left", 0.7); + break; + case "d": + panTracks("right", 0.7); + break; + case "ArrowUp": + case "w": + case "+": + zoomIn(); + break; + case "ArrowDown": + case "s": + case "-": + zoomOut(); + break; default: } } -}) +}); diff --git a/assets/js/navigation.test.js b/assets/js/navigation.test.js index 1b66ffaa..7ab1b16a 100644 --- a/assets/js/navigation.test.js +++ b/assets/js/navigation.test.js @@ -1,129 +1,146 @@ -import { parseRegionDesignation, limitRegionToChromosome, readInputField } from './navigation.js' -import * as helper from './helper.js' +import { + parseRegionDesignation, + limitRegionToChromosome, + readInputField, +} from "./navigation.js"; +import * as helper from "./helper.js"; // Test Track class -describe('Test parseRegionDesignation', () => { - test('Parse :-', () => { - let region = parseRegionDesignation('12:11-100') - expect(region).toEqual({chrom: '12', start: 11, end: 100}) - - region = parseRegionDesignation('X:11-100') - expect(region).toEqual({chrom: 'X', start: 11, end: 100}) - }) - - test('Parse :-None', () => { - const region = parseRegionDesignation('12:11-None') - expect(region).toEqual({chrom: '12', start: 11, end: NaN}) - }) - - test('Parse :', () => { - const region = parseRegionDesignation('12:11') - expect(region).toEqual({chrom: '12', start: 11, end: NaN}) - }) - - test('Parse :', () => { - const region = parseRegionDesignation('12:') - expect(region).toEqual({chrom: '12', start: NaN, end: NaN}) - }) - - test('Input only the chromosome returns null', () => { - const region = parseRegionDesignation('12') - expect(region).toBeFalsy() - }) - - test('Invalid chromosome throws an error', () => { - expect(() => parseRegionDesignation('30:')).toThrow() - expect(() => parseRegionDesignation('Z:')).toThrow() - }) -}) - -describe('test limitRegionToChromosome ', () => { +describe("Test parseRegionDesignation", () => { + test("Parse :-", () => { + let region = parseRegionDesignation("12:11-100"); + expect(region).toEqual({ chrom: "12", start: 11, end: 100 }); + + region = parseRegionDesignation("X:11-100"); + expect(region).toEqual({ chrom: "X", start: 11, end: 100 }); + }); + + test("Parse :-None", () => { + const region = parseRegionDesignation("12:11-None"); + expect(region).toEqual({ chrom: "12", start: 11, end: NaN }); + }); + + test("Parse :", () => { + const region = parseRegionDesignation("12:11"); + expect(region).toEqual({ chrom: "12", start: 11, end: NaN }); + }); + + test("Parse :", () => { + const region = parseRegionDesignation("12:"); + expect(region).toEqual({ chrom: "12", start: NaN, end: NaN }); + }); + + test("Input only the chromosome returns null", () => { + const region = parseRegionDesignation("12"); + expect(region).toBeFalsy(); + }); + + test("Invalid chromosome throws an error", () => { + expect(() => parseRegionDesignation("30:")).toThrow(); + expect(() => parseRegionDesignation("Z:")).toThrow(); + }); +}); + +describe("test limitRegionToChromosome ", () => { // setup mocks - const mockRes = {1: 5000, 2: 10000} - - test('test position within chromosome', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: 1000, end: 2000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1000, end: 2000}) - }) - expect(chromSizeMock.mock.calls.length).toBe(1) // assert mock works - }) - - test('test end pos outside chromosome', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: 4000, end: 6000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 3000, end: 5000}) - }) - }) - - test('test start pos outside chromosome', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: -1000, end: 2000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1, end: 3000}) - }) - }) - - - test('test start pos is null', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: null, end: 2000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1, end: 2000}) - }) - }) - - test('test end pos is null', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: 1000, end: null}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1000, end: 5000}) - }) - }) - - test('test start and end pos is outsize chrom', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: -2000, end: 10000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1, end: 5000}) - }) - }) - - test('test if start and end are retained', () => { - const chromSizeMock = jest.spyOn(helper, 'chromSizes') - .mockReturnValueOnce(mockRes) - .mockName('chromSizes') - - limitRegionToChromosome({chrom: 1, start: 1, end: 5000}) - .then( region => { - expect(region).toEqual({chrom: 1, start: 1, end: 5000}) - }) - }) -}) - -test('test readInputField', () => { + const mockRes = { 1: 5000, 2: 10000 }; + + test("test position within chromosome", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: 1000, end: 2000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1000, end: 2000 }); + }, + ); + expect(chromSizeMock.mock.calls.length).toBe(1); // assert mock works + }); + + test("test end pos outside chromosome", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: 4000, end: 6000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 3000, end: 5000 }); + }, + ); + }); + + test("test start pos outside chromosome", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: -1000, end: 2000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1, end: 3000 }); + }, + ); + }); + + test("test start pos is null", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: null, end: 2000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1, end: 2000 }); + }, + ); + }); + + test("test end pos is null", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: 1000, end: null }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1000, end: 5000 }); + }, + ); + }); + + test("test start and end pos is outsize chrom", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: -2000, end: 10000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1, end: 5000 }); + }, + ); + }); + + test("test if start and end are retained", () => { + const chromSizeMock = jest + .spyOn(helper, "chromSizes") + .mockReturnValueOnce(mockRes) + .mockName("chromSizes"); + + limitRegionToChromosome({ chrom: 1, start: 1, end: 5000 }).then( + (region) => { + expect(region).toEqual({ chrom: 1, start: 1, end: 5000 }); + }, + ); + }); +}); + +test("test readInputField", () => { document.body.innerHTML = ` -` - const region = readInputField() - expect(region).toEqual({chrom: '1', start: 100, end: 1000}) -}) +`; + const region = readInputField(); + expect(region).toEqual({ chrom: "1", start: 100, end: 1000 }); +}); diff --git a/assets/js/overview.js b/assets/js/overview.js index 8006f52e..88757e36 100644 --- a/assets/js/overview.js +++ b/assets/js/overview.js @@ -1,175 +1,217 @@ // Overview canvas definition -import { BaseScatterTrack, CHROMOSOMES } from './track.js' -import { create, get } from './fetch.js' -import { createGraph, drawPoints, drawGraphLines, drawText, drawRotatedText } from './draw.js' +import { BaseScatterTrack, CHROMOSOMES } from "./track.js"; +import { create, get } from "./fetch.js"; +import { + createGraph, + drawPoints, + drawGraphLines, + drawText, + drawRotatedText, +} from "./draw.js"; -import { drawTrack } from './navigation.js' +import { drawTrack } from "./navigation.js"; export class OverviewCanvas extends BaseScatterTrack { - constructor (xPos, fullPlotWidth, lineMargin, near, far, caseId, sampleName, - genomeBuild, hgFileDir) { - super({ caseId, sampleName, genomeBuild, hgFileDir }) + constructor( + xPos, + fullPlotWidth, + lineMargin, + near, + far, + caseId, + sampleName, + genomeBuild, + hgFileDir, + ) { + super({ caseId, sampleName, genomeBuild, hgFileDir }); // Plot variables - this.fullPlotWidth = fullPlotWidth // Width for all chromosomes to fit in - this.plotHeight = 180 // Height of one plot - this.titleMargin = 10 // Margin between plot and title - this.legendMargin = 45 // Margin between legend and plot - this.x = xPos // Starting x-position for plot - this.y = 20 + this.titleMargin + 2 * lineMargin // Starting y-position for plot - this.leftRightPadding = 2 // Padding for left and right in graph - this.topBottomPadding = 8 // Padding for top and bottom in graph - this.leftmostPoint = this.x + 10 // Draw y-values for graph left of this point + this.fullPlotWidth = fullPlotWidth; // Width for all chromosomes to fit in + this.plotHeight = 180; // Height of one plot + this.titleMargin = 10; // Margin between plot and title + this.legendMargin = 45; // Margin between legend and plot + this.x = xPos; // Starting x-position for plot + this.y = 20 + this.titleMargin + 2 * lineMargin; // Starting y-position for plot + this.leftRightPadding = 2; // Padding for left and right in graph + this.topBottomPadding = 8; // Padding for top and bottom in graph + this.leftmostPoint = this.x + 10; // Draw y-values for graph left of this point // Setup canvas for repeated patterns - this.patternCanvas = document.createElement('canvas') - const size = 20 - this.patternCanvas.width = size - this.patternCanvas.height = size - const patternCtx = this.patternCanvas.getContext('2d') - patternCtx.fillStyle = "#E6E9ED" - patternCtx.strokeStyle = "#4C6D94" - patternCtx.lineWidth = Math.round(size / 10) - patternCtx.lineCap = 'square' - patternCtx.fillRect(0, 0, size, size) - patternCtx.moveTo(size / 2, 0) - patternCtx.lineTo(size, size / 2) - patternCtx.moveTo(0, size / 2) - patternCtx.lineTo(size / 2, size) - patternCtx.stroke() + this.patternCanvas = document.createElement("canvas"); + const size = 20; + this.patternCanvas.width = size; + this.patternCanvas.height = size; + const patternCtx = this.patternCanvas.getContext("2d"); + patternCtx.fillStyle = "#E6E9ED"; + patternCtx.strokeStyle = "#4C6D94"; + patternCtx.lineWidth = Math.round(size / 10); + patternCtx.lineCap = "square"; + patternCtx.fillRect(0, 0, size, size); + patternCtx.moveTo(size / 2, 0); + patternCtx.lineTo(size, size / 2); + patternCtx.moveTo(0, size / 2); + patternCtx.lineTo(size / 2, size); + patternCtx.stroke(); // BAF values this.baf = { yStart: 1.0, // Start value for y axis yEnd: 0.0, // End value for y axis step: 0.2, // Step value for drawing ticks along y-axis - color: '#000000' // Viz color - } + color: "#000000", // Viz color + }; // Log2 ratio values this.log2 = { yStart: 3.0, // Start value for y axis yEnd: -3.0, // End value for y axis step: 1.0, // Step value for drawing ticks along y-axis - color: '#000000' // Viz color - } + color: "#000000", // Viz color + }; // Canvas variables - this.disabledChroms = [] - this.width = document.body.clientWidth // Canvas width - this.height = this.y + 2 * this.plotHeight + 2 * this.topBottomPadding // Canvas height - this.drawCanvas.width = parseInt(this.width) - this.drawCanvas.height = parseInt(this.height) - this.staticCanvas = document.getElementById('overview-static') + this.disabledChroms = []; + this.width = document.body.clientWidth; // Canvas width + this.height = this.y + 2 * this.plotHeight + 2 * this.topBottomPadding; // Canvas height + this.drawCanvas.width = parseInt(this.width); + this.drawCanvas.height = parseInt(this.height); + this.staticCanvas = document.getElementById("overview-static"); // Initialize marker div element - this.markerElem = document.getElementById('overview-marker') - this.markerElem.style.height = (this.plotHeight * 2) + 'px' - this.markerElem.style.marginTop = 0 - (this.plotHeight + this.topBottomPadding) * 2 + 'px' + this.markerElem = document.getElementById("overview-marker"); + this.markerElem.style.height = this.plotHeight * 2 + "px"; + this.markerElem.style.marginTop = + 0 - (this.plotHeight + this.topBottomPadding) * 2 + "px"; // Set dimensions of overview canvases - this.staticCanvas.width = this.width - this.staticCanvas.height = this.height + this.staticCanvas.width = this.width; + this.staticCanvas.height = this.height; this.getOverviewChromDim().then(() => { // Select a chromosome in overview track - this.staticCanvas.addEventListener('mousedown', event => { - event.stopPropagation() - const selectedChrom = this.pixelPosToGenomicLoc(event.x) + this.staticCanvas.addEventListener("mousedown", (event) => { + event.stopPropagation(); + const selectedChrom = this.pixelPosToGenomicLoc(event.x); if (!this.disabledChroms.includes(selectedChrom.chrom)) { // Dont update if chrom previously selected // Move interactive view to selected region - const chrom = selectedChrom.chrom - const start = 1 - const end = this.dims[chrom].size - 1 + const chrom = selectedChrom.chrom; + const start = 1; + const end = this.dims[chrom].size - 1; // Mark region - this.markRegion({ chrom, start, end }) - drawTrack({ chrom, start, end }) // redraw canvas + this.markRegion({ chrom, start, end }); + drawTrack({ chrom, start, end }); // redraw canvas } - }) - this.staticCanvas.parentElement.addEventListener('mark-region', event => { - this.markRegion({ ...event.detail.region }) - }) - }) + }); + this.staticCanvas.parentElement.addEventListener( + "mark-region", + (event) => { + this.markRegion({ ...event.detail.region }); + }, + ); + }); } - pixelPosToGenomicLoc (pixelpos) { - const match = {} + pixelPosToGenomicLoc(pixelpos) { + const match = {}; for (const i of CHROMOSOMES) { - const chr = this.dims[i] + const chr = this.dims[i]; if (pixelpos > chr.x_pos && pixelpos < chr.x_pos + chr.width) { - match.chrom = i - match.pos = Math.floor(chr.size * (pixelpos - chr.x_pos) / chr.width) + match.chrom = i; + match.pos = Math.floor((chr.size * (pixelpos - chr.x_pos)) / chr.width); } } - return match + return match; } - async getOverviewChromDim () { - await get('get-overview-chrom-dim', { + async getOverviewChromDim() { + await get("get-overview-chrom-dim", { x_pos: this.x, y_pos: this.y, plot_width: this.fullPlotWidth, - genome_build: this.genomeBuild - }).then(result => { - this.dims = result.chrom_dims - this.chromPos = CHROMOSOMES.map(chrom => { + genome_build: this.genomeBuild, + }).then((result) => { + this.dims = result.chrom_dims; + this.chromPos = CHROMOSOMES.map((chrom) => { return { region: `${chrom}:0-None`, x_pos: this.dims[chrom].x_pos + this.leftRightPadding, y_pos: this.dims[chrom].y_pos, - x_ampl: this.dims[chrom].width - 2 * this.leftRightPadding - } - }) - }) + x_ampl: this.dims[chrom].width - 2 * this.leftRightPadding, + }; + }); + }); } - markRegion ({ chrom, start, end }) { + markRegion({ chrom, start, end }) { if (this.dims !== undefined) { - const scale = this.dims[chrom].width / this.dims[chrom].size - const overviewMarker = document.getElementById('overview-marker') + const scale = this.dims[chrom].width / this.dims[chrom].size; + const overviewMarker = document.getElementById("overview-marker"); - let markerStartPos, markerWidth + let markerStartPos, markerWidth; // Calculate position and size of marker if ((end - start) * scale < 2) { - markerStartPos = 1 + (this.dims[chrom].x_pos + start * scale) - markerWidth = 2 + markerStartPos = 1 + (this.dims[chrom].x_pos + start * scale); + markerWidth = 2; } else { - markerStartPos = 1.5 + (this.dims[chrom].x_pos + start * scale) - markerWidth = Math.max(2, Math.ceil((end - start) * scale) - 1) + markerStartPos = 1.5 + (this.dims[chrom].x_pos + start * scale); + markerWidth = Math.max(2, Math.ceil((end - start) * scale) - 1); } // Update the dom element - overviewMarker.style.left = markerStartPos + 'px' - overviewMarker.style.width = (markerWidth) + 'px' + overviewMarker.style.left = markerStartPos + "px"; + overviewMarker.style.width = markerWidth + "px"; } } - async drawOverviewPlotSegment ({ canvas, chrom, width, chromCovData }) { + async drawOverviewPlotSegment({ canvas, chrom, width, chromCovData }) { // Draw chromosome title - const ctx = canvas.getContext('2d') + const ctx = canvas.getContext("2d"); drawText({ ctx, x: chromCovData.x_pos - this.leftRightPadding + width / 2, y: chromCovData.y_pos - this.titleMargin, text: chromCovData.chrom, fontProp: 10, - align: 'center' - }) + align: "center", + }); // Draw rotated y-axis legends if (chromCovData.x_pos < this.leftmostPoint) { - drawRotatedText(ctx, 'B Allele Freq', 18, chromCovData.x_pos - this.legendMargin, - chromCovData.y_pos + this.plotHeight / 2, -Math.PI / 2, this.titleColor) - drawRotatedText(ctx, 'Log2 Ratio', 18, chromCovData.x_pos - this.legendMargin, - chromCovData.y_pos + 1.5 * this.plotHeight, -Math.PI / 2, this.titleColor) + drawRotatedText( + ctx, + "B Allele Freq", + 18, + chromCovData.x_pos - this.legendMargin, + chromCovData.y_pos + this.plotHeight / 2, + -Math.PI / 2, + this.titleColor, + ); + drawRotatedText( + ctx, + "Log2 Ratio", + 18, + chromCovData.x_pos - this.legendMargin, + chromCovData.y_pos + 1.5 * this.plotHeight, + -Math.PI / 2, + this.titleColor, + ); } // Draw BAF - createGraph(ctx, + createGraph( + ctx, chromCovData.x_pos - this.leftRightPadding, - chromCovData.y_pos, width, this.plotHeight, this.topBottomPadding, - this.baf.yStart, this.baf.yEnd, this.baf.step, - chromCovData.x_pos < this.leftmostPoint, this.borderColor, chrom !== CHROMOSOMES[0]) + chromCovData.y_pos, + width, + this.plotHeight, + this.topBottomPadding, + this.baf.yStart, + this.baf.yEnd, + this.baf.step, + chromCovData.x_pos < this.leftmostPoint, + this.borderColor, + chrom !== CHROMOSOMES[0], + ); drawGraphLines({ ctx, x: chromCovData.x_pos, @@ -179,17 +221,24 @@ export class OverviewCanvas extends BaseScatterTrack { stepLength: this.baf.step, yMargin: this.topBottomPadding, width: width, - height: this.plotHeight - }) + height: this.plotHeight, + }); // Draw Log 2 ratio createGraph( ctx, chromCovData.x_pos - this.leftRightPadding, - chromCovData.y_pos + this.plotHeight, width, - this.plotHeight, this.topBottomPadding, this.log2.yStart, - this.log2.yEnd, this.log2.step, - chromCovData.x_pos < this.leftmostPoint, this.borderColor, chrom !== CHROMOSOMES[0]) + chromCovData.y_pos + this.plotHeight, + width, + this.plotHeight, + this.topBottomPadding, + this.log2.yStart, + this.log2.yEnd, + this.log2.step, + chromCovData.x_pos < this.leftmostPoint, + this.borderColor, + chrom !== CHROMOSOMES[0], + ); drawGraphLines({ ctx, x: chromCovData.x_pos, @@ -199,32 +248,37 @@ export class OverviewCanvas extends BaseScatterTrack { stepLength: this.log2.step, yMargin: this.topBottomPadding, width: width, - height: this.plotHeight - }) + height: this.plotHeight, + }); // Plot scatter data - if ( chromCovData.baf.length > 0 || chromCovData.data.length > 0 ) { + if (chromCovData.baf.length > 0 || chromCovData.data.length > 0) { drawPoints({ ctx, data: chromCovData.baf, - color: this.baf.color - }) + color: this.baf.color, + }); drawPoints({ ctx, data: chromCovData.data, - color: this.log2.color - }) + color: this.log2.color, + }); } else { - const pattern = ctx.createPattern(this.patternCanvas, 'repeat') - ctx.fillStyle = pattern - ctx.fillRect(chromCovData.x_pos, chromCovData.y_pos + 1, width - 2, (this.plotHeight * 2) - 2) - this.disabledChroms.push(chrom) + const pattern = ctx.createPattern(this.patternCanvas, "repeat"); + ctx.fillStyle = pattern; + ctx.fillRect( + chromCovData.x_pos, + chromCovData.y_pos + 1, + width - 2, + this.plotHeight * 2 - 2, + ); + this.disabledChroms.push(chrom); } } - async drawOverviewContent (printing) { - await this.getOverviewChromDim() + async drawOverviewContent(printing) { + await this.getOverviewChromDim(); // query gens for coverage values - const covData = await create('get-multiple-coverages', { + const covData = await create("get-multiple-coverages", { case_id: this.caseId, sample_id: this.sampleName, genome_build: this.genomeBuild, @@ -235,16 +289,16 @@ export class OverviewCanvas extends BaseScatterTrack { baf_y_end: this.baf.yEnd, log2_y_start: this.log2.yStart, log2_y_end: this.log2.yEnd, - overview: 'True', - reduce_data: 1 - }) + overview: "True", + reduce_data: 1, + }); for (const [chrom, res] of Object.entries(covData.results)) { this.drawOverviewPlotSegment({ canvas: this.staticCanvas, chrom: chrom, width: this.dims[chrom].width, - chromCovData: res - }) + chromCovData: res, + }); } } } diff --git a/assets/js/track.js b/assets/js/track.js index 939ff6f7..4ecbf679 100644 --- a/assets/js/track.js +++ b/assets/js/track.js @@ -1,8 +1,11 @@ // Entrypoint for track module -export { CHROMOSOMES } from './track/constants.js' -export { VariantTrack } from './track/variant.js' -export { TranscriptTrack } from './track/transcript.js' -export { AnnotationTrack } from './track/annotation.js' -export { BaseScatterTrack } from './track/base.js' -export { CytogeneticIdeogram, setupGenericEventManager } from './track/ideogram.js' +export { CHROMOSOMES } from "./track/constants.js"; +export { VariantTrack } from "./track/variant.js"; +export { TranscriptTrack } from "./track/transcript.js"; +export { AnnotationTrack } from "./track/annotation.js"; +export { BaseScatterTrack } from "./track/base.js"; +export { + CytogeneticIdeogram, + setupGenericEventManager, +} from "./track/ideogram.js"; diff --git a/assets/js/track/annotation.js b/assets/js/track/annotation.js index 47523dbd..abb5ce3a 100644 --- a/assets/js/track/annotation.js +++ b/assets/js/track/annotation.js @@ -1,144 +1,151 @@ // Annotation track definition -import { BaseAnnotationTrack } from './base.js' -import { isElementOverlapping } from './utils.js' -import { get } from '../fetch.js' -import { parseRegionDesignation } from '../navigation.js' -import { drawRect, drawText } from '../draw.js' -import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' -import { createPopper } from '@popperjs/core' +import { BaseAnnotationTrack } from "./base.js"; +import { isElementOverlapping } from "./utils.js"; +import { get } from "../fetch.js"; +import { parseRegionDesignation } from "../navigation.js"; +import { drawRect, drawText } from "../draw.js"; +import { + initTrackTooltips, + createTooltipElement, + makeVirtualDOMElement, + updateVisibleElementCoordinates, +} from "./tooltip.js"; +import { createPopper } from "@popperjs/core"; // Convert to 32bit integer -function stringToHash (string) { - let hash = 0 - if (string.length === 0) return hash +function stringToHash(string) { + let hash = 0; + if (string.length === 0) return hash; for (let i = 0; i < string.length; i++) { - const char = string.charCodeAt(i) - hash = ((hash << 5) - hash) + char - hash = hash & hash + const char = string.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; } - return hash + return hash; } export class AnnotationTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, genomeBuild, defaultAnnotation) { + constructor(x, width, near, far, genomeBuild, defaultAnnotation) { // Dimensions of track canvas - const visibleHeight = 300 // Visible height for expanded canvas, overflows for scroll - const minHeight = 35 // Minimized height + const visibleHeight = 300; // Visible height for expanded canvas, overflows for scroll + const minHeight = 35; // Minimized height - super(width, near, far, visibleHeight, minHeight) + super(width, near, far, visibleHeight, minHeight); // Set inherited variables // TODO use the names contentCanvas and drawCanvas - this.drawCanvas = document.getElementById('annotation-draw') - this.contentCanvas = document.getElementById('annotation-content') - this.trackTitle = document.getElementById('annotation-titles') - this.trackContainer = document.getElementById('annotation-track-container') - this.featureHeight = 18 - this.arrowThickness = 2 + this.drawCanvas = document.getElementById("annotation-draw"); + this.contentCanvas = document.getElementById("annotation-content"); + this.trackTitle = document.getElementById("annotation-titles"); + this.trackContainer = document.getElementById("annotation-track-container"); + this.featureHeight = 18; + this.arrowThickness = 2; // Setup html objects now that we have gotten the canvas and div elements - this.setupHTML(x + 1) + this.setupHTML(x + 1); - this.trackContainer.style.marginTop = '-1px' - this.genomeBuild = genomeBuild + this.trackContainer.style.marginTop = "-1px"; + this.genomeBuild = genomeBuild; // Setup annotation list - this.sourceList = document.getElementById('source-list') - this.sourceList.addEventListener('change', () => { - this.expanded = false - this.additionalQueryParams = { source: this.sourceList.value } - const region = parseRegionDesignation(document.getElementById('region-field').value) - this.drawTrack({ forceRedraw: true, ...region }) - }) - this.annotSourceList(defaultAnnotation) + this.sourceList = document.getElementById("source-list"); + this.sourceList.addEventListener("change", () => { + this.expanded = false; + this.additionalQueryParams = { source: this.sourceList.value }; + const region = parseRegionDesignation( + document.getElementById("region-field").value, + ); + this.drawTrack({ forceRedraw: true, ...region }); + }); + this.annotSourceList(defaultAnnotation); // GENS api parameters - this.apiEntrypoint = 'get-annotation-data' - this.additionalQueryParams = { source: defaultAnnotation } + this.apiEntrypoint = "get-annotation-data"; + this.additionalQueryParams = { source: defaultAnnotation }; - this.maxResolution = 6 // define other max resolution - this.numRenderedElements = 0 - initTrackTooltips(this) + this.maxResolution = 6; // define other max resolution + this.numRenderedElements = 0; + initTrackTooltips(this); } // Fills the list with source files - annotSourceList (defaultAnnotation) { - get('get-annotation-sources', { genome_build: this.genomeBuild }) - .then(result => { + annotSourceList(defaultAnnotation) { + get("get-annotation-sources", { genome_build: this.genomeBuild }) + .then((result) => { if (result.sources.length > 0) { - this.sourceList.style.visibility = 'visible' + this.sourceList.style.visibility = "visible"; } for (const fileName of result.sources) { // Add annotation file name to list - const opt = document.createElement('option') - opt.value = fileName - opt.innerHTML = fileName + const opt = document.createElement("option"); + opt.value = fileName; + opt.innerHTML = fileName; // Set mimisbrunnr as default file if (fileName.match(defaultAnnotation)) { - opt.setAttribute('selected', true) + opt.setAttribute("selected", true); } - this.sourceList.appendChild(opt) + this.sourceList.appendChild(opt); } }) .then(() => { - const region = parseRegionDesignation(document.getElementById('region-field').value) - this.drawTrack({ ...region }) - }) + const region = parseRegionDesignation( + document.getElementById("region-field").value, + ); + this.drawTrack({ ...region }); + }); } // Draws annotations in given range - async drawOffScreenTrack ({ startPos, endPos, maxHeightOrder, data }) { - const textSize = 10 + async drawOffScreenTrack({ startPos, endPos, maxHeightOrder, data }) { + const textSize = 10; // store positions used when rendering the canvas this.offscreenPosition = { start: startPos, end: endPos, - scale: (this.drawCanvas.width / - (endPos - startPos)) - } - const scale = this.offscreenPosition.scale + scale: this.drawCanvas.width / (endPos - startPos), + }; + const scale = this.offscreenPosition.scale; this.heightOrderRecord = { latestHeight: 0, // Latest height order for annotation latestNameEnd: 0, // Latest annotations end position - latestTrackEnd: 0 // Latest annotations title's end position - } + latestTrackEnd: 0, // Latest annotations title's end position + }; // limit drawing of transcript to pre-defined resolutions - let filteredAnnotations = [] + let filteredAnnotations = []; if (this.getResolution < this.maxResolution + 1) { - filteredAnnotations = data - .annotations - .filter(annot => isElementOverlapping(annot, - { - start: startPos, - end: endPos - })) + filteredAnnotations = data.annotations.filter((annot) => + isElementOverlapping(annot, { + start: startPos, + end: endPos, + }), + ); } // dont show tracks with no data in them if (filteredAnnotations.length > 0) { // Set needed height of visible canvas and transcript tooltips - this.setContainerHeight(this.trackData.max_height_order) + this.setContainerHeight(this.trackData.max_height_order); } else { // Set needed height of visible canvas and transcript tooltips - this.setContainerHeight(0) + this.setContainerHeight(0); } - this.clearTracks() + this.clearTracks(); // Go through results and draw appropriate symbols for (const track of filteredAnnotations) { - const annotationName = track.name - const heightOrder = track.height_order - const start = track.start - const end = track.end - const color = track.color + const annotationName = track.name; + const heightOrder = track.height_order; + const start = track.start; + const end = track.end; + const color = track.color; // Only draw visible tracks if (!this.expanded && heightOrder !== 1) { - continue + continue; } // Keep track of latest annotations @@ -146,12 +153,12 @@ export class AnnotationTrack extends BaseAnnotationTrack { this.heightOrderRecord = { latestHeight: heightOrder, latestNameEnd: 0, - latestTrackEnd: 0 - } + latestTrackEnd: 0, + }; } // make an annotation object that tracks screen coordinates of object - const x1 = scale * (start - this.offscreenPosition.start) - const canvasYPos = this.tracksYPos(heightOrder) + const x1 = scale * (start - this.offscreenPosition.start); + const canvasYPos = this.tracksYPos(heightOrder); const annotationObj = { id: stringToHash(track.name), name: track.name, @@ -160,10 +167,10 @@ export class AnnotationTrack extends BaseAnnotationTrack { x1: x1, x2: x1 + scale * (end - start + 1), y1: canvasYPos, - y2: canvasYPos + (this.featureHeight / 2), + y2: canvasYPos + this.featureHeight / 2, features: [], - isDisplayed: false - } + isDisplayed: false, + }; // Draw box for annotation drawRect({ ctx: this.drawCtx, @@ -173,46 +180,51 @@ export class AnnotationTrack extends BaseAnnotationTrack { height: this.featureHeight / 2, lineWidth: 1, fillColor: color, - open: false - }) + open: false, + }); // get onscreen positions for offscreen xy coordinates updateVisibleElementCoordinates({ element: annotationObj, screenPosition: this.onscreenPosition, - scale: this.offscreenPosition.scale - }) + scale: this.offscreenPosition.scale, + }); // create a tooltip html element and append to DOM const tooltip = createTooltipElement({ id: `popover-${annotationObj.id}`, title: annotationObj.name, information: [ { title: track.chrom, value: `${track.start}-${track.end}` }, - { title: 'Score', value: `${track.score}` } - ] - }) - this.trackContainer.appendChild(tooltip) + { title: "Score", value: `${track.score}` }, + ], + }); + this.trackContainer.appendChild(tooltip); // make a virtual element as tooltip hitbox const virtualElement = makeVirtualDOMElement({ x1: annotationObj.visibleX1, x2: annotationObj.visibleX2, y1: annotationObj.visibleY1, y2: annotationObj.visibleY2, - canvas: this.contentCanvas - }) + canvas: this.contentCanvas, + }); // add tooltip to annotationObj annotationObj.tooltip = { instance: createPopper(virtualElement, tooltip, { modifiers: [ - { name: 'offset', options: { offset: [0, virtualElement.getBoundingClientRect().height] } } - ] + { + name: "offset", + options: { + offset: [0, virtualElement.getBoundingClientRect().height], + }, + }, + ], }), virtualElement: virtualElement, tooltip: tooltip, - isDisplayed: false - } - this.geneticElements.push(annotationObj) + isDisplayed: false, + }; + this.geneticElements.push(annotationObj); - const textYPos = this.tracksYPos(heightOrder) + const textYPos = this.tracksYPos(heightOrder); // limit drawing of titles to certain resolution if (this.getResolution < 6) { // Draw annotation name @@ -221,8 +233,8 @@ export class AnnotationTrack extends BaseAnnotationTrack { text: annotationName, x: scale * ((start + end) / 2 - this.offscreenPosition.start), y: textYPos + this.featureHeight, - fontProp: textSize - }) + fontProp: textSize, + }); } } } diff --git a/assets/js/track/base.js b/assets/js/track/base.js index 514bc04e..dfaa7032 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -1,178 +1,192 @@ // Generic functions related to drawing annotation tracks -import { get } from '../fetch.js' -import { hideTooltip } from './tooltip.js' +import { get } from "../fetch.js"; +import { hideTooltip } from "./tooltip.js"; // Calculate offscreen position -export function calculateOffscreenWindowPos ({ start, end, multiplier }) { - const width = end - start - const padding = ((width * multiplier) - width) / 2 +export function calculateOffscreenWindowPos({ start, end, multiplier }) { + const width = end - start; + const padding = (width * multiplier - width) / 2; // const paddedStart = (start - padding) > 0 ? (start - padding) : 1; return { - start: Math.round(start - padding), end: Math.round(end + padding) - } + start: Math.round(start - padding), + end: Math.round(end + padding), + }; } // function for shading and blending colors on the fly -export function lightenColor (color, percent) { - const num = parseInt(color.replace('#', ''), 16) - const amt = Math.round(2.55 * percent) - const red = (num >> 16) + amt - const blue = (num >> 8 & 0x00FF) + amt - const green = (num & 0x0000FF) + amt - return '#' + (0x1000000 + (red < 255 ? red < 1 ? 0 : red : 255) * 0x10000 + (blue < 255 ? blue < 1 ? 0 : blue : 255) * 0x100 + (green < 255 ? green < 1 ? 0 : green : 255)).toString(16).slice(1) -}; +export function lightenColor(color, percent) { + const num = parseInt(color.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const red = (num >> 16) + amt; + const blue = ((num >> 8) & 0x00ff) + amt; + const green = (num & 0x0000ff) + amt; + return ( + "#" + + ( + 0x1000000 + + (red < 255 ? (red < 1 ? 0 : red) : 255) * 0x10000 + + (blue < 255 ? (blue < 1 ? 0 : blue) : 255) * 0x100 + + (green < 255 ? (green < 1 ? 0 : green) : 255) + ) + .toString(16) + .slice(1) + ); +} export class BaseScatterTrack { - constructor ({ caseId, sampleName, genomeBuild, hgFileDir }) { + constructor({ caseId, sampleName, genomeBuild, hgFileDir }) { // setup IO - this.caseId = caseId // Case id to use for querying data - this.sampleName = sampleName // File name to load data from - this.genomeBuild = genomeBuild // Whether to load HG37 or HG38, default is HG38 - this.hgFileDir = hgFileDir // File directory + this.caseId = caseId; // Case id to use for querying data + this.sampleName = sampleName; // File name to load data from + this.genomeBuild = genomeBuild; // Whether to load HG37 or HG38, default is HG38 + this.hgFileDir = hgFileDir; // File directory // Border - this.borderColor = '#666' // Color of border - this.titleColor = 'black' // Color of titles/legends + this.borderColor = "#666"; // Color of border + this.titleColor = "black"; // Color of titles/legends // Setup canvas - this.drawCanvas = document.createElement('canvas') - this.context = this.drawCanvas.getContext('2d') + this.drawCanvas = document.createElement("canvas"); + this.context = this.drawCanvas.getContext("2d"); } } export class BaseAnnotationTrack { - constructor (width, near, far, visibleHeight, minHeight, colorSchema) { + constructor(width, near, far, visibleHeight, minHeight, colorSchema) { // Track variables - this.featureHeight = 20 // Max height for feature - this.featureMargin = 14 // Margin for fitting gene name under track - this.yPos = this.featureHeight / 2 // First y-position - this.arrowColor = 'white' - this.arrowWidth = 4 - this.arrowDistance = 200 - this.arrowThickness = 1 - this.expanded = false - this.colorSchema = colorSchema + this.featureHeight = 20; // Max height for feature + this.featureMargin = 14; // Margin for fitting gene name under track + this.yPos = this.featureHeight / 2; // First y-position + this.arrowColor = "white"; + this.arrowWidth = 4; + this.arrowDistance = 200; + this.arrowThickness = 1; + this.expanded = false; + this.colorSchema = colorSchema; // errors preventing fetching of data - this.preventDrawingTrack = false + this.preventDrawingTrack = false; // Dimensions of track canvas - this.width = Math.round(width) // Width of displayed canvas - this.drawCanvasMultiplier = 4 - this.maxHeight = 16000 // Max height of canvas - this.visibleHeight = visibleHeight // Visible height for expanded canvas, overflows for scroll - this.minHeight = minHeight // Minimized height + this.width = Math.round(width); // Width of displayed canvas + this.drawCanvasMultiplier = 4; + this.maxHeight = 16000; // Max height of canvas + this.visibleHeight = visibleHeight; // Visible height for expanded canvas, overflows for scroll + this.minHeight = minHeight; // Minimized height // Canvases // the drawCanvas is used to draw objects offscreen // the region to be displayed is blitted to the onscreen contentCanvas - this.trackContainer = null // Set in parent class - this.drawCanvas = null // Set in parent class + this.trackContainer = null; // Set in parent class + this.drawCanvas = null; // Set in parent class // Canvases for static content - this.contentCanvas = null - this.trackTitle = null // Set in parent class + this.contentCanvas = null; + this.trackTitle = null; // Set in parent class // data cache - this.trackData = null + this.trackData = null; // Store coordinates of offscreen canvas - this.offscreenPosition = { start: null, end: null, scale: null } - this.onscreenPosition = { start: null, end: null } + this.offscreenPosition = { start: null, end: null, scale: null }; + this.onscreenPosition = { start: null, end: null }; // Max resolution - this.maxResolution = 4 - this.geneticElements = [] // for tooltips + this.maxResolution = 4; + this.geneticElements = []; // for tooltips } - tracksYPos (heightOrder) { - return this.yPos + (heightOrder - 1) * (this.featureHeight + this.featureMargin) - }; + tracksYPos(heightOrder) { + return ( + this.yPos + (heightOrder - 1) * (this.featureHeight + this.featureMargin) + ); + } - setupHTML (xPos) { - this.contentCanvas.style.width = this.width + 'px' + setupHTML(xPos) { + this.contentCanvas.style.width = this.width + "px"; // Setup variant canvas - this.trackContainer.style.marginLeft = xPos + 'px' - this.trackContainer.style.width = this.width + 'px' + this.trackContainer.style.marginLeft = xPos + "px"; + this.trackContainer.style.width = this.width + "px"; // set xlabel - this.trackContainer - .parentElement - .querySelector('.track-xlabel') - .style - .left = `${xPos - 60}px` + this.trackContainer.parentElement.querySelector( + ".track-xlabel", + ).style.left = `${xPos - 60}px`; // Setup initial track Canvas - this.drawCtx = this.drawCanvas.getContext('2d') - this.drawCanvas.width = this.width * this.drawCanvasMultiplier - this.drawCanvas.height = this.maxHeight - this.contentCanvas.width = this.width - this.contentCanvas.height = this.minHeight + this.drawCtx = this.drawCanvas.getContext("2d"); + this.drawCanvas.width = this.width * this.drawCanvasMultiplier; + this.drawCanvas.height = this.maxHeight; + this.contentCanvas.width = this.width; + this.contentCanvas.height = this.minHeight; // Setup track div - this.trackTitle.style.width = this.width + 'px' - this.trackTitle.style.height = this.minHeight + 'px' + this.trackTitle.style.width = this.width + "px"; + this.trackTitle.style.height = this.minHeight + "px"; - this.trackContainer.parentElement.addEventListener('draw', (event) => { - this.drawTrack({...event.detail.region}) - }) + this.trackContainer.parentElement.addEventListener("draw", (event) => { + this.drawTrack({ ...event.detail.region }); + }); // Setup context menu - this.trackContainer.addEventListener('contextmenu', + this.trackContainer.addEventListener( + "contextmenu", async (event) => { - event.preventDefault() + event.preventDefault(); // hide all tooltips for (const element of this.geneticElements) { - if (element.tooltip) hideTooltip(element.tooltip) + if (element.tooltip) hideTooltip(element.tooltip); } // Toggle between expanded/collapsed view - this.expanded = !this.expanded + this.expanded = !this.expanded; // set datastate for css if (this.expanded) { - this.trackContainer.setAttribute('data-state', 'expanded') + this.trackContainer.setAttribute("data-state", "expanded"); } else { - this.trackContainer.setAttribute('data-state', 'collapsed') + this.trackContainer.setAttribute("data-state", "collapsed"); } await this.drawOffScreenTrack({ startPos: this.offscreenPosition.start, endPos: this.offscreenPosition.end, maxHeightOrder: this.expanded ? this.trackData.max_height_order : 1, - data: this.trackData - }) - this.blitCanvas(this.onscreenPosition.start, this.onscreenPosition.end) - this.drawDynamicOverlay() - }, false) + data: this.trackData, + }); + this.blitCanvas(this.onscreenPosition.start, this.onscreenPosition.end); + this.drawDynamicOverlay(); + }, + false, + ); } // Clears previous tracks - clearTracks () { + clearTracks() { // Clear canvas - this.drawCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height) + this.drawCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); // Clear tooltip titles - this.trackTitle.innerHTML = '' + this.trackTitle.innerHTML = ""; } // Sets the container height depending on maximum height of tracks - setContainerHeight (maxHeightOrder) { + setContainerHeight(maxHeightOrder) { if (maxHeightOrder === 0) { // No results, do not show tracks - this.contentCanvas.height = 0 - this.trackTitle.style.height = '0px' - this.trackContainer.style.height = '0px' + this.contentCanvas.height = 0; + this.trackTitle.style.height = "0px"; + this.trackContainer.style.height = "0px"; // hide parent element - this.trackContainer.parentElement.setAttribute('data-state', 'nodata') + this.trackContainer.parentElement.setAttribute("data-state", "nodata"); } else { - this.trackContainer.parentElement.setAttribute('data-state', 'data') + this.trackContainer.parentElement.setAttribute("data-state", "data"); // controll track content if (this.expanded) { // Set variables for an expanded view - const maxYPos = this.tracksYPos(maxHeightOrder + 1) - this.contentCanvas.height = maxYPos - this.drawCanvas.height = maxYPos - this.trackTitle.style.height = `${maxYPos}px` - this.trackContainer.style.height = `${this.visibleHeight}px` - this.trackContainer.setAttribute('data-state', 'expanded') + const maxYPos = this.tracksYPos(maxHeightOrder + 1); + this.contentCanvas.height = maxYPos; + this.drawCanvas.height = maxYPos; + this.trackTitle.style.height = `${maxYPos}px`; + this.trackContainer.style.height = `${this.visibleHeight}px`; + this.trackContainer.setAttribute("data-state", "expanded"); } else { // Set variables for a collapsed view - this.contentCanvas.height = this.minHeight - this.drawCanvas.height = this.minHeight - this.trackTitle.style.height = `${this.minHeight}px` - this.trackContainer.style.height = `${this.minHeight}px` - this.trackContainer.setAttribute('data-state', 'collapsed') + this.contentCanvas.height = this.minHeight; + this.drawCanvas.height = this.minHeight; + this.trackTitle.style.height = `${this.minHeight}px`; + this.trackContainer.style.height = `${this.minHeight}px`; + this.trackContainer.setAttribute("data-state", "collapsed"); } } } @@ -183,81 +197,97 @@ export class BaseAnnotationTrack { // if new chromosome selected --> cache all annotations for chrom // if new region in offscreen canvas --> blit image // if new region outside offscreen canvas --> redraw offscreen using cache - async drawTrack ({ chrom, start, end, forceRedraw = false, hideWhileLoading = false }) { - if (this.preventDrawingTrack) return // disable drawing track + async drawTrack({ + chrom, + start, + end, + forceRedraw = false, + hideWhileLoading = false, + }) { + if (this.preventDrawingTrack) return; // disable drawing track // store genomic position of the region to draw - this.onscreenPosition.start = start - this.onscreenPosition.end = end - const width = end - start + 1 - let updatedData = false + this.onscreenPosition.start = start; + this.onscreenPosition.end = end; + const width = end - start + 1; + let updatedData = false; // verify that // 1. data is loaded // 2. right chromosome is loaded // 3. right expansion - if (!this.trackData || - this.trackData.chromosome !== chrom || - forceRedraw) { + if (!this.trackData || this.trackData.chromosome !== chrom || forceRedraw) { // hide track while loading - if (hideWhileLoading) this.trackContainer.parentElement.setAttribute('data-state', 'nodata') + if (hideWhileLoading) + this.trackContainer.parentElement.setAttribute("data-state", "nodata"); // request new data this.trackData = await get( this.apiEntrypoint, - Object.assign({ // build query parameters - sample_id: oc.sampleName, - region: `${chrom}:1-None`, - genome_build: this.genomeBuild, - collapsed: false // allways get all height orders - }, this.additionalQueryParams) // parameters specific to track type - ) + Object.assign( + { + // build query parameters + sample_id: oc.sampleName, + region: `${chrom}:1-None`, + genome_build: this.genomeBuild, + collapsed: false, // allways get all height orders + }, + this.additionalQueryParams, + ), // parameters specific to track type + ); // disable track if data loading encountered an error - if (this.trackData.status === 'error') { - this.trackContainer.parentElement.setAttribute('data-state', 'nodata') - this.preventDrawingTrack = true - return + if (this.trackData.status === "error") { + this.trackContainer.parentElement.setAttribute("data-state", "nodata"); + this.preventDrawingTrack = true; + return; } // the track data is used to determine the new start/ end positions - end = end > this.trackData.end_pos ? this.trackData.end_pos : end - updatedData = true + end = end > this.trackData.end_pos ? this.trackData.end_pos : end; + updatedData = true; } // redraw offscreen canvas if, // 1. not drawn before; // 2. if onscreen canvas close of offscreen canvas edge // 3. size of region has been changed, zoom in or out - if (!this.offscreenPosition.start || - start < this.offscreenPosition.start + width || - this.offscreenPosition.end - width < end || - this.offscreenPosition.scale !== this.contentCanvas.width / (end - start) || - updatedData + if ( + !this.offscreenPosition.start || + start < this.offscreenPosition.start + width || + this.offscreenPosition.end - width < end || + this.offscreenPosition.scale !== + this.contentCanvas.width / (end - start) || + updatedData ) { const offscreenPos = calculateOffscreenWindowPos({ - start: start, end: end, multiplier: this.drawCanvasMultiplier - }) + start: start, + end: end, + multiplier: this.drawCanvasMultiplier, + }); // draw offscreen position for the first time await this.drawOffScreenTrack({ startPos: offscreenPos.start, endPos: offscreenPos.end, maxHeightOrder: this.trackData.max_height_order, - data: this.trackData - }) + data: this.trackData, + }); } // blit image from offscreen canvas to onscreen canvas - this.blitCanvas(start, end) + this.blitCanvas(start, end); + this.drawDynamicOverlay(); } // blit drawCanvas to content canvas. - blitCanvas (chromStart, chromEnd) { + blitCanvas(chromStart, chromEnd) { // blit drawCanvas to content canvas. // clear current canvas - const ctx = this.contentCanvas.getContext('2d') - ctx.clearRect(0, 0, this.contentCanvas.width, - this.contentCanvas.height) - const width = chromEnd - chromStart - this.onscreenPosition = { start: chromStart, end: chromEnd } // store onscreen coords + const ctx = this.contentCanvas.getContext("2d"); + ctx.clearRect(0, 0, this.contentCanvas.width, this.contentCanvas.height); + const width = chromEnd - chromStart; + this.onscreenPosition = { start: chromStart, end: chromEnd }; // store onscreen coords // Debugging - const offscreenOffset = Math.round((chromStart - this.offscreenPosition.start) * this.offscreenPosition.scale) - const elementWidth = Math.round(width * this.offscreenPosition.scale) + const offscreenOffset = Math.round( + (chromStart - this.offscreenPosition.start) * + this.offscreenPosition.scale, + ); + const elementWidth = Math.round(width * this.offscreenPosition.scale); // normalize the genomic coordinates to screen coordinates ctx.drawImage( @@ -269,27 +299,29 @@ export class BaseAnnotationTrack { 0, // dX 0, // dY this.contentCanvas.width, // dWidth - this.drawCanvas.height // dHeight - ) + this.drawCanvas.height, // dHeight + ); } + drawDynamicOverlay() {} + // Classify the resolution wich can be used chose when to display variants - get getResolution () { - const width = this.onscreenPosition.end - this.onscreenPosition.start + 1 - let resolution + get getResolution() { + const width = this.onscreenPosition.end - this.onscreenPosition.start + 1; + let resolution; if (width > 5 * Math.pow(10, 7)) { - resolution = 6 + resolution = 6; } else if (width > 1.5 * Math.pow(10, 7)) { - resolution = 5 + resolution = 5; } else if (width > 4 * Math.pow(10, 6)) { - resolution = 4 + resolution = 4; } else if (width > 1 * Math.pow(10, 6)) { - resolution = 3 + resolution = 3; } else if (width > 2 * Math.pow(10, 5)) { - resolution = 2 + resolution = 2; } else { - resolution = 1 + resolution = 1; } - return resolution + return resolution; } } diff --git a/assets/js/track/base.test.js b/assets/js/track/base.test.js index b6ded476..79ff9436 100644 --- a/assets/js/track/base.test.js +++ b/assets/js/track/base.test.js @@ -1,16 +1,23 @@ // Test tracks -import { calculateOffscreenWindowPos } from './base.js' +import { calculateOffscreenWindowPos } from "./base.js"; import "regenerator-runtime/runtime"; - // test that offscreen window position -describe('Test calculateOffscreenWindowPos', () => { - test('test no padding', () => { - const region = calculateOffscreenWindowPos({start: 100, end: 200, multiplier: 1}) - expect(region).toEqual({start: 100, end: 200}) - }) - test('test padding to region', () => { - const region = calculateOffscreenWindowPos({start: 100, end: 200, multiplier: 2}) - expect(region).toEqual({start: 50, end: 250}) - }) -}) +describe("Test calculateOffscreenWindowPos", () => { + test("test no padding", () => { + const region = calculateOffscreenWindowPos({ + start: 100, + end: 200, + multiplier: 1, + }); + expect(region).toEqual({ start: 100, end: 200 }); + }); + test("test padding to region", () => { + const region = calculateOffscreenWindowPos({ + start: 100, + end: 200, + multiplier: 2, + }); + expect(region).toEqual({ start: 50, end: 250 }); + }); +}); diff --git a/assets/js/track/constants.js b/assets/js/track/constants.js index ddd54f5e..af1da0f6 100644 --- a/assets/js/track/constants.js +++ b/assets/js/track/constants.js @@ -1,6 +1,29 @@ // constants used throughout gens -const CHROMOSOMES = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', - '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', - '22', 'X', 'Y'] -export { CHROMOSOMES } +const CHROMOSOMES = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "X", + "Y", +]; +export { CHROMOSOMES }; diff --git a/assets/js/track/ideogram.js b/assets/js/track/ideogram.js index d3956d05..d57b8f67 100644 --- a/assets/js/track/ideogram.js +++ b/assets/js/track/ideogram.js @@ -1,252 +1,335 @@ // Cytogenetic ideogram -import { get } from '../fetch.js' -import { drawRect } from '../draw.js' -import { lightenColor } from './base.js' -import tippy, { followCursor } from 'tippy.js' -import 'tippy.js/dist/tippy.css'; -import { isElementOverlapping } from './utils.js'; -import { thisExpression } from '@babel/types'; +import { get } from "../fetch.js"; +import { drawRect } from "../draw.js"; +import { lightenColor } from "./base.js"; +import tippy, { followCursor } from "tippy.js"; +import "tippy.js/dist/tippy.css"; +import { isElementOverlapping } from "./utils.js"; +import { thisExpression } from "@babel/types"; export class CytogeneticIdeogram { - constructor({targetId, genomeBuild, x, y, width, height}) { + constructor({ targetId, genomeBuild, x, y, width, height }) { // define core varialbes - this.genomeBuild = genomeBuild - this.x = x - this.y = y - this.plotWidth = width - this.plotHeight = height + this.genomeBuild = genomeBuild; + this.x = x; + this.y = y; + this.plotWidth = width; + this.plotHeight = height; // Setup cytogenetic ideogram image - this.targetElement = document.getElementById(targetId) - this.targetElement.style.height = `${height + 10}px` + this.targetElement = document.getElementById(targetId); + this.targetElement.style.height = `${height + 10}px`; // create canvas and append to target section of dom - const canvas = document.createElement('canvas') - canvas.style.marginLeft = `${x}px` - canvas.width = width - canvas.height = height - this.targetElement.appendChild(canvas) - this.canvas = canvas - this.ctx = canvas.getContext('2d') + const canvas = document.createElement("canvas"); + canvas.style.marginLeft = `${x}px`; + canvas.width = width; + canvas.height = height; + this.targetElement.appendChild(canvas); + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); // create region marker - const markerElement = document.createElement('div') - markerElement.id = "ideogram-marker" - markerElement.classList = ["marker"] - markerElement.style.height = `${height - 4}px` - markerElement.style.width = 0 - markerElement.style.top = `-${height -4}px` - markerElement.style.marginLeft = `${x}px` - this.targetElement.appendChild(markerElement) + const markerElement = document.createElement("div"); + markerElement.id = "ideogram-marker"; + markerElement.classList = ["marker"]; + markerElement.style.height = `${height - 4}px`; + markerElement.style.width = 0; + markerElement.style.top = `-${height - 4}px`; + markerElement.style.marginLeft = `${x}px`; + this.targetElement.appendChild(markerElement); // chromosomeImage - this.drawPaths = null + this.drawPaths = null; // define tooltip element - const tooltip = createChromosomeTooltip({}) + const tooltip = createChromosomeTooltip({}); tippy(canvas, { arrow: true, - followCursor: 'horizontal', + followCursor: "horizontal", content: tooltip, - plugins: [followCursor] - }) + plugins: [followCursor], + }); // register event handeler for updating popups - canvas.addEventListener('mousemove', (event) => { - this.drawPaths !== null && this.drawPaths.bands.map((bandPath) => { - const ctx = canvas.getContext('2d') - if (ctx.isPointInPath(bandPath.path, event.offsetX, event.offsetY)) { - tooltip.querySelector('.ideogram-tooltip-value').innerHTML = bandPath.id - } - }) - }) + canvas.addEventListener("mousemove", (event) => { + this.drawPaths !== null && + this.drawPaths.bands.map((bandPath) => { + const ctx = canvas.getContext("2d"); + if (ctx.isPointInPath(bandPath.path, event.offsetX, event.offsetY)) { + tooltip.querySelector(".ideogram-tooltip-value").innerHTML = + bandPath.id; + } + }); + }); // register event for moving and zooming region marker - this.targetElement.addEventListener('mark-region', (event) => { + this.targetElement.addEventListener("mark-region", (event) => { // if marking a subset of chromosome - const { chrom, start, end } = event.detail.region + const { chrom, start, end } = event.detail.region; // get marker element - const markerElement = document.getElementById('ideogram-marker') - if (this.drawPaths !== null && chrom === this.drawPaths.chromosome.chromInfo.chrom) { + const markerElement = document.getElementById("ideogram-marker"); + if ( + this.drawPaths !== null && + chrom === this.drawPaths.chromosome.chromInfo.chrom + ) { // if segment of chromosome is drawn - const { scale, x } = this.drawPaths.chromosome.chromInfo - markerElement.hidden = false // display marker - markerElement.style.marginLeft = `${Math.round(x + (start * scale))}px` - markerElement.style.width = `${Math.round((end - start + 1) * scale)}px` + const { scale, x } = this.drawPaths.chromosome.chromInfo; + markerElement.hidden = false; // display marker + markerElement.style.marginLeft = `${Math.round(x + start * scale)}px`; + markerElement.style.width = `${Math.round((end - start + 1) * scale)}px`; // dispatch event to update title - const scaledStart = Math.round(start * scale) - const scaledEnd = Math.round(end * scale) - const bandsWithinMarkedRegion = this.drawPaths.bands.filter((band) => isElementOverlapping({start: scaledStart, end: scaledEnd}, band)) - document.getElementById('visualization-container').dispatchEvent( - new CustomEvent('update-title', { - detail: { bands: bandsWithinMarkedRegion, chrom: chrom } }) - ) + const scaledStart = Math.round(start * scale); + const scaledEnd = Math.round(end * scale); + const bandsWithinMarkedRegion = this.drawPaths.bands.filter((band) => + isElementOverlapping({ start: scaledStart, end: scaledEnd }, band), + ); + document.getElementById("visualization-container").dispatchEvent( + new CustomEvent("update-title", { + detail: { bands: bandsWithinMarkedRegion, chrom: chrom }, + }), + ); } else { // if entire chromosome is drawn - markerElement.hidden = true // hide marker + markerElement.hidden = true; // hide marker } - }) + }); // register event for moving and zooming region marker - this.targetElement.addEventListener('draw', (event) => { + this.targetElement.addEventListener("draw", (event) => { // check if this is supposed to be excluded - if ( !event.detail.exclude.includes(this.targetElement.id) ) { + if (!event.detail.exclude.includes(this.targetElement.id)) { // remove old figures - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) - this.ctx.save() - markerElement.style.width = 0 + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.save(); + markerElement.style.width = 0; // draw new figure cytogeneticIdeogram({ ctx: this.ctx, chromosomeName: event.detail.region.chrom, - x: this.x, - width: this.plotWidth, + x: this.x, + width: this.plotWidth, height: this.plotHeight, genomeBuild: this.genomeBuild, - }).then((paths) => { - this.drawPaths = paths - }) + }).then((paths) => { + this.drawPaths = paths; + }); } - }) + }); } } -export function setupGenericEventManager({eventName, ownerElement, targetElementIds}) { +export function setupGenericEventManager({ + eventName, + ownerElement, + targetElementIds, +}) { // pass directed from owner element to taget elements ownerElement.addEventListener(eventName, (event) => { - targetElementIds.map((id) => { - document.getElementById(id).dispatchEvent( - new CustomEvent(eventName, {detail: event.detail}) - ) - }) - }) + targetElementIds.map((id) => { + document + .getElementById(id) + .dispatchEvent(new CustomEvent(eventName, { detail: event.detail })); + }); + }); } function createChromosomeTooltip({ bandId }) { - const element = document.createElement('div') - element.id = 'ideogram-tooltip' - const name = document.createElement('span') - name.innerHTML = 'ID:' - name.classList = ["ideogram-tooltip-key"] - element.appendChild(name) - const value = document.createElement('span') - value.classList = ["ideogram-tooltip-value"] - element.appendChild(value) - return element + const element = document.createElement("div"); + element.id = "ideogram-tooltip"; + const name = document.createElement("span"); + name.innerHTML = "ID:"; + name.classList = ["ideogram-tooltip-key"]; + element.appendChild(name); + const value = document.createElement("span"); + value.classList = ["ideogram-tooltip-value"]; + element.appendChild(value); + return element; } -export async function cytogeneticIdeogram({ctx, chromosomeName, genomeBuild, x, width, height}) { - const chromInfo = await getChromosomeInfo(chromosomeName, genomeBuild) +export async function cytogeneticIdeogram({ + ctx, + chromosomeName, + genomeBuild, + x, + width, + height, +}) { + const chromInfo = await getChromosomeInfo(chromosomeName, genomeBuild); // recalculate genomic coordinates to screen coordinates - const scale = width / chromInfo.size - const centromere = chromInfo.centromere !== null ? - { start: Math.round(chromInfo.centromere.start * scale), end: Math.round(chromInfo.centromere.end * scale) } : null + const scale = width / chromInfo.size; + const centromere = + chromInfo.centromere !== null + ? { + start: Math.round(chromInfo.centromere.start * scale), + end: Math.round(chromInfo.centromere.end * scale), + } + : null; const drawPaths = drawChromosome({ - ctx: ctx, x: 3, y: 5, + ctx: ctx, + x: 3, + y: 5, width: width - 5, height: height - 6, centromere, - color: 'white', + color: "white", bands: chromInfo.bands.map((band) => { - band.start = Math.round(band.start * scale) - band.end = Math.round(band.end * scale) - return band - }) - }) + band.start = Math.round(band.start * scale); + band.end = Math.round(band.end * scale); + return band; + }), + }); drawPaths.chromosome.chromInfo = { chrom: chromosomeName, x: x, width: width, scale: scale, size: chromInfo.size, - } - return drawPaths + }; + return drawPaths; } async function getChromosomeInfo(chromosomeName, genomeBuild) { - const result = await get('get-chromosome-info', { chromosome: chromosomeName, genome_build: genomeBuild }) - return result + const result = await get("get-chromosome-info", { + chromosome: chromosomeName, + genome_build: genomeBuild, + }); + return result; } -function drawChromosome({ ctx, x, y, width, height, centromere, bands, color, lineColor }) { - const basePosColor = '#000' // dark green +function drawChromosome({ + ctx, + x, + y, + width, + height, + centromere, + bands, + color, + lineColor, +}) { + const basePosColor = "#000"; // dark green const bandColors = { - gneg: '#FFFAF0', - acen: '#673888', - gvar: '#4C6D94', + gneg: "#FFFAF0", + acen: "#673888", + gvar: "#4C6D94", gpos25: lightenColor(basePosColor, 75), gpos50: lightenColor(basePosColor, 50), gpos75: lightenColor(basePosColor, 25), gpos100: basePosColor, - } + }; const chromPath = { - path: drawChromosomeShape({ ctx, x, y, width, height, centromere, color, lineColor }) - } - - ctx.clip(chromPath.path) - const bandPaths = bands.map((band) => { - band.path = drawRect({ + path: drawChromosomeShape({ ctx, - x: x + band.start, - y: y - 5, - width: band.end - band.start, - height: height + 5, - color: bandColors[band.stain], - fillColor: bandColors[band.stain], + x, + y, + width, + height, + centromere, + color, + lineColor, + }), + }; + + ctx.clip(chromPath.path); + const bandPaths = bands + .map((band) => { + band.path = drawRect({ + ctx, + x: x + band.start, + y: y - 5, + width: band.end - band.start, + height: height + 5, + color: bandColors[band.stain], + fillColor: bandColors[band.stain], + }); + band.x = x + band.start; + band.y = y - 5; + band.width = band.end - band.start; + band.height = height + 5; + return band; }) - band.x = x + band.start - band.y = y - 5 - band.width = band.end - band.start - band.height = height + 5 - return band - }).filter((band) => { return band.path !== null }) - ctx.restore() - return { chromosome: chromPath, bands: bandPaths } + .filter((band) => { + return band.path !== null; + }); + ctx.restore(); + return { chromosome: chromPath, bands: bandPaths }; } -function drawChromosomeShape({ ctx, x, y, width, height, centromere, color, lineColor = '#000' }) { +function drawChromosomeShape({ + ctx, + x, + y, + width, + height, + centromere, + color, + lineColor = "#000", +}) { // draw shape of chromosome // define proportions of shape - const endBevelProportion = 0.05 - const centromereIndentProportion = 0.3 + const endBevelProportion = 0.05; + const centromereIndentProportion = 0.3; // compute basic meassurement - const bevelWidth = Math.round(width * endBevelProportion) + const bevelWidth = Math.round(width * endBevelProportion); // cacluate dimensions of the centromere - const centromereLenght = centromere.end - centromere.start - const centromereIndent = Math.round(height * centromereIndentProportion) - const centromereIndentRadius = centromereIndent * 2.5 < centromereLenght / 3 ? Math.round(centromereIndent * 2.5) : centromereLenght / 3 - const centromereCenter = centromere.start + Math.round((centromere.end - centromere.start) / 2) + const centromereLenght = centromere.end - centromere.start; + const centromereIndent = Math.round(height * centromereIndentProportion); + const centromereIndentRadius = + centromereIndent * 2.5 < centromereLenght / 3 + ? Math.round(centromereIndent * 2.5) + : centromereLenght / 3; + const centromereCenter = + centromere.start + Math.round((centromere.end - centromere.start) / 2); - const chromEndRadius = Math.round(height * .7 / 2) + const chromEndRadius = Math.round((height * 0.7) / 2); // path object - const path = new Path2D() + const path = new Path2D(); // draw shape - path.moveTo(x + bevelWidth, y) // move to start + path.moveTo(x + bevelWidth, y); // move to start // handle centromere if (centromere) { - path.lineTo(centromere.start, y) + path.lineTo(centromere.start, y); // indent for centromere - path.arcTo(centromereCenter, y + centromereIndent, centromere.end, y, centromereIndentRadius) - path.lineTo(centromere.end, y) + path.arcTo( + centromereCenter, + y + centromereIndent, + centromere.end, + y, + centromereIndentRadius, + ); + path.lineTo(centromere.end, y); } - path.lineTo(x + width - bevelWidth, y) // line to end cap + path.lineTo(x + width - bevelWidth, y); // line to end cap // right end cap - path.arcTo(x + width, y, x + width, y + (height / 2), chromEndRadius) - path.arcTo(x + width, y + height, x + width - bevelWidth, y + height, chromEndRadius) + path.arcTo(x + width, y, x + width, y + height / 2, chromEndRadius); + path.arcTo( + x + width, + y + height, + x + width - bevelWidth, + y + height, + chromEndRadius, + ); // bottom line if (centromere) { - path.lineTo(centromere.end, y + height) - path.arcTo(centromereCenter, (y + height) - centromereIndent, centromere.start, y + height, centromereIndentRadius) - path.lineTo(centromere.start, y + height) + path.lineTo(centromere.end, y + height); + path.arcTo( + centromereCenter, + y + height - centromereIndent, + centromere.start, + y + height, + centromereIndentRadius, + ); + path.lineTo(centromere.start, y + height); } - path.lineTo(x + bevelWidth, y + height) + path.lineTo(x + bevelWidth, y + height); // left end cap - path.arcTo(x, y + height, x, y + (height / 2), chromEndRadius) - path.arcTo(x, y, x + bevelWidth, y, chromEndRadius) + path.arcTo(x, y + height, x, y + height / 2, chromEndRadius); + path.arcTo(x, y, x + bevelWidth, y, chromEndRadius); // finish figure - path.closePath() + path.closePath(); // setup coloring - ctx.strokeStyle = lineColor - ctx.stroke(path) + ctx.strokeStyle = lineColor; + ctx.stroke(path); if (color !== undefined) { - ctx.fillStyle = color - ctx.fill(path) + ctx.fillStyle = color; + ctx.fill(path); } - return path -} \ No newline at end of file + return path; +} diff --git a/assets/js/track/tooltip.js b/assets/js/track/tooltip.js index ba3fef52..87e55bc8 100644 --- a/assets/js/track/tooltip.js +++ b/assets/js/track/tooltip.js @@ -1,215 +1,251 @@ // functions for handling tooltips -import { getVisibleXCoordinates, getVisibleYCoordinates, isWithinElementBbox } from './utils.js' +import { + getVisibleXCoordinates, + getVisibleYCoordinates, + isWithinElementBbox, +} from "./utils.js"; // make virtual DOM element that represents a annotation element -export function makeVirtualDOMElement ({ x1, x2, y1, y2, canvas }) { - return { getBoundingClientRect: generateGetBoundingClientRect(x1, x2, y1, y2, canvas) } +export function makeVirtualDOMElement({ x1, x2, y1, y2, canvas }) { + return { + getBoundingClientRect: generateGetBoundingClientRect( + x1, + x2, + y1, + y2, + canvas, + ), + }; } // Make a virtual DOM element from a genetic element object -export function generateGetBoundingClientRect (x1, x2, y1, y2, canvas) { - const track = canvas +export function generateGetBoundingClientRect(x1, x2, y1, y2, canvas) { + const track = canvas; return () => ({ width: Math.round(x2 - x1), height: Math.round(y2 - y1), top: y1 + Math.round(track.getBoundingClientRect().y), left: x1 + Math.round(track.getBoundingClientRect().x), right: x2 + Math.round(track.getBoundingClientRect().x), - bottom: y2 + Math.round(track.getBoundingClientRect().y) - }) + bottom: y2 + Math.round(track.getBoundingClientRect().y), + }); } -export function updateVisibleElementCoordinates ({ element, screenPosition, scale }) { - const { x1, x2 } = getVisibleXCoordinates({ canvas: screenPosition, feature: element, scale: scale }) - const { y1, y2 } = getVisibleYCoordinates({ element }) +export function updateVisibleElementCoordinates({ + element, + screenPosition, + scale, +}) { + const { x1, x2 } = getVisibleXCoordinates({ + canvas: screenPosition, + feature: element, + scale: scale, + }); + const { y1, y2 } = getVisibleYCoordinates({ element }); // update coordinates - element.visibleX1 = x1 - element.visibleX2 = x2 - element.visibleY1 = y1 - element.visibleY2 = y2 + element.visibleX1 = x1; + element.visibleX2 = x2; + element.visibleY1 = y1; + element.visibleY2 = y2; } -function showTooltip ({ tooltip, feature }) { - tooltip.tooltip.setAttribute('data-show', '') +function showTooltip({ tooltip, feature }) { + tooltip.tooltip.setAttribute("data-show", ""); if (feature !== undefined) { - const featureElement = tooltip.tooltip.querySelector(`#feature-${feature.id}`) - featureElement.setAttribute('data-show', '') - feature.isDisplayed = true + const featureElement = tooltip.tooltip.querySelector( + `#feature-${feature.id}`, + ); + featureElement.setAttribute("data-show", ""); + feature.isDisplayed = true; } - tooltip.isDisplayed = true + tooltip.isDisplayed = true; } -function hideFeatureInTooltip ({ tooltip, feature }) { - const selectedFeature = tooltip.tooltip.querySelector(`#feature-${feature.id}`) - selectedFeature.removeAttribute('data-show') - feature.isDisplayed = false +function hideFeatureInTooltip({ tooltip, feature }) { + const selectedFeature = tooltip.tooltip.querySelector( + `#feature-${feature.id}`, + ); + selectedFeature.removeAttribute("data-show"); + feature.isDisplayed = false; } -export function hideTooltip (tooltip) { +export function hideTooltip(tooltip) { // skip if tooltip has not been rendered - tooltip.tooltip.removeAttribute('data-show') - for (const feature of tooltip.tooltip.querySelectorAll('.feature')) { - feature.removeAttribute('data-show') + tooltip.tooltip.removeAttribute("data-show"); + for (const feature of tooltip.tooltip.querySelectorAll(".feature")) { + feature.removeAttribute("data-show"); } - tooltip.isDisplayed = false + tooltip.isDisplayed = false; } -export function createHtmlList (information) { - const list = document.createElement('ul') +export function createHtmlList(information) { + const list = document.createElement("ul"); for (const info of information) { - const li = document.createElement('li') - const bold = document.createElement('strong') - bold.innerText = info.title - li.innerText = bold.innerHTML += `: ${info.value}` - list.appendChild(li) + const li = document.createElement("li"); + const bold = document.createElement("strong"); + bold.innerText = info.title; + li.innerText = bold.innerHTML += `: ${info.value}`; + list.appendChild(li); } - return list + return list; } // create popover html element with message -export function createTooltipElement ({ id, title, information = [] }) { +export function createTooltipElement({ id, title, information = [] }) { // create popover base class - const popover = document.createElement('div') - popover.setAttribute('role', 'popover') + const popover = document.createElement("div"); + popover.setAttribute("role", "popover"); if (id !== undefined) { - popover.id = id + popover.id = id; } - popover.classList.add('tooltip') - popover.setAttribute('role', 'popover') + popover.classList.add("tooltip"); + popover.setAttribute("role", "popover"); // create information in container element - const container = document.createElement('div') - container.classList.add('tooltip-content') + const container = document.createElement("div"); + container.classList.add("tooltip-content"); // create title - const titleElem = document.createElement('h4') - titleElem.innerText = title - container.appendChild(titleElem) + const titleElem = document.createElement("h4"); + titleElem.innerText = title; + container.appendChild(titleElem); // create information list - const body = createHtmlList(information) - container.appendChild(body) - popover.appendChild(container) + const body = createHtmlList(information); + container.appendChild(body); + popover.appendChild(container); // return tooltip element - return popover + return popover; } // function for handeling apperance and content of tooltips // element == a the main rendered element, a gene for instance // features == genetic sub components of the parent elements, for instance a exome -function tooltipHandler (event, track) { - event.preventDefault() - event.stopPropagation() - const point = { x: event.offsetX, y: event.offsetY } +function tooltipHandler(event, track) { + event.preventDefault(); + event.stopPropagation(); + const point = { x: event.offsetX, y: event.offsetY }; for (const element of track.geneticElements) { - if ( !element.tooltip) { - continue + if (!element.tooltip) { + continue; } const isInElement = isWithinElementBbox({ element: { x1: element.visibleX1, x2: element.visibleX2, y1: element.y1, - y2: element.y2 + y2: element.y2, }, - point - }) + point, + }); if (isInElement) { // check if pointer is in a feature of element - let selectedFeature + let selectedFeature; for (const feature of element.features) { const isInFeature = isWithinElementBbox({ element: { x1: feature.visibleX1, x2: feature.visibleX2, y1: feature.y1, - y2: feature.y2 + y2: feature.y2, }, - point - }) + point, + }); if (isInFeature && !feature.isDisplayed) { // show feature - selectedFeature = feature + selectedFeature = feature; } else if (!isInFeature && feature.isDisplayed) { - hideFeatureInTooltip({ tooltip: element.tooltip, feature }) + hideFeatureInTooltip({ tooltip: element.tooltip, feature }); } } - showTooltip({ tooltip: element.tooltip, feature: selectedFeature }) + showTooltip({ tooltip: element.tooltip, feature: selectedFeature }); } else { - hideTooltip(element.tooltip) + hideTooltip(element.tooltip); } - element.tooltip.instance.update() + element.tooltip.instance.update(); } } // update tooltip position -function updateTooltipPos (track) { +function updateTooltipPos(track) { for (const element of track.geneticElements) { // skip if tooltip has not been rendered - if ( !element.tooltip ) { - continue + if (!element.tooltip) { + continue; } // update coordinates for the main element updateVisibleElementCoordinates({ element, canvas: track.contentCanvas, screenPosition: track.onscreenPosition, - scale: track.offscreenPosition.scale - }) + scale: track.offscreenPosition.scale, + }); // update coordinates for features on element for (const feature of element.features) { updateVisibleElementCoordinates({ element: feature, canvas: track.contentCanvas, screenPosition: track.onscreenPosition, - scale: track.offscreenPosition.scale - }) + scale: track.offscreenPosition.scale, + }); } // update the virtual DOM element that defines the tooltip hitbox - const xPos = track.contentCanvas.getBoundingClientRect().x + const xPos = track.contentCanvas.getBoundingClientRect().x; element.tooltip.virtualElement = makeVirtualDOMElement({ x1: Math.round(element.visibleX1 + xPos), x2: Math.round(element.visibleX2 + xPos), y1: element.visibleY1, - y2: element.visibleY2 - }) + y2: element.visibleY2, + }); // update tooltip instance - element.tooltip.instance.update() + element.tooltip.instance.update(); } } // teardown tooltips generated for a track -function teardownTooltips (track) { +function teardownTooltips(track) { while (track.geneticElements.length) { - const element = track.geneticElements.shift() + const element = track.geneticElements.shift(); // skip if tooltip has not been rendered - if ( !element.tooltip ) { - continue + if (!element.tooltip) { + continue; } - element.tooltip.instance.destroy() // kill popper - track.trackContainer.querySelector(`#${element.tooltip.tooltip.id}`).remove() + element.tooltip.instance.destroy(); // kill popper + track.trackContainer + .querySelector(`#${element.tooltip.tooltip.id}`) + .remove(); } } - // initialize event listeners for hover function -export function initTrackTooltips (track) { +export function initTrackTooltips(track) { // when mouse is leaving track - track.trackContainer.addEventListener('mouseleave', - () => { - for (const element of track.geneticElements) { - if (element.tooltip) hideTooltip(element.tooltip) - } - }) + track.trackContainer.addEventListener("mouseleave", () => { + for (const element of track.geneticElements) { + if (element.tooltip) hideTooltip(element.tooltip); + } + }); // when mouse is leaving track - track.trackContainer.addEventListener('mousemove', (e) => { tooltipHandler(e, track) }) + track.trackContainer.addEventListener("mousemove", (e) => { + tooltipHandler(e, track); + }); // extend drawOffScreenTrack to teardown old tooltips prior to drawing new - const oldDrawOffscreenTrack = track.drawOffScreenTrack - track.drawOffScreenTrack = async ({ startPos, endPos, maxHeightOrder, data }) => { - teardownTooltips(track) - await oldDrawOffscreenTrack.call(track, { startPos, endPos, maxHeightOrder, data }) - } + const oldDrawOffscreenTrack = track.drawOffScreenTrack; + track.drawOffScreenTrack = async ({ + startPos, + endPos, + maxHeightOrder, + data, + }) => { + teardownTooltips(track); + await oldDrawOffscreenTrack.call(track, { + startPos, + endPos, + maxHeightOrder, + data, + }); + }; // extend instance function to recalculate positions of virtual dom elements - const oldBlit = track.blitCanvas + const oldBlit = track.blitCanvas; track.blitCanvas = (start, end) => { - updateTooltipPos(track) - oldBlit.call(track, start, end) - } + updateTooltipPos(track); + oldBlit.call(track, start, end); + }; } diff --git a/assets/js/track/transcript.js b/assets/js/track/transcript.js index a53be210..f1528bc1 100644 --- a/assets/js/track/transcript.js +++ b/assets/js/track/transcript.js @@ -1,72 +1,79 @@ // Transcript definition -import { BaseAnnotationTrack, lightenColor } from './base.js' -import { initTrackTooltips, createTooltipElement, createHtmlList, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' -import { createPopper } from '@popperjs/core' -import { drawRect, drawLine, drawArrow, drawText } from '../draw.js' -import { getVisibleXCoordinates, isElementOverlapping } from './utils.js' +import { BaseAnnotationTrack, lightenColor } from "./base.js"; +import { + initTrackTooltips, + createTooltipElement, + createHtmlList, + makeVirtualDOMElement, + updateVisibleElementCoordinates, +} from "./tooltip.js"; +import { createPopper } from "@popperjs/core"; +import { drawRect, drawLine, drawArrow, drawText } from "../draw.js"; +import { getVisibleXCoordinates, isElementOverlapping } from "./utils.js"; // add feature information to tooltipElement -function addFeatures (elem, tooltipElement) { - const body = tooltipElement.querySelector('ul') +function addFeatures(elem, tooltipElement) { + const body = tooltipElement.querySelector("ul"); for (const feature of elem.features) { // divide and conquer - const featureContainer = document.createElement('div') - featureContainer.id = `feature-${feature.exon_number}` - featureContainer.classList.add('feature') - featureContainer.appendChild(document.createElement('hr')) + const featureContainer = document.createElement("div"); + featureContainer.id = `feature-${feature.exon_number}`; + featureContainer.classList.add("feature"); + featureContainer.appendChild(document.createElement("hr")); const information = [ - { title: 'exon', value: feature.exon_number }, - { title: 'position', value: `${feature.start}-${feature.end}` } - ] - featureContainer.appendChild(createHtmlList(information)) - body.appendChild(featureContainer) + { title: "exon", value: feature.exon_number }, + { title: "position", value: `${feature.start}-${feature.end}` }, + ]; + featureContainer.appendChild(createHtmlList(information)); + body.appendChild(featureContainer); } } export class TranscriptTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, genomeBuild, colorSchema) { + constructor(x, width, near, far, genomeBuild, colorSchema) { // Dimensions of track canvas - const visibleHeight = 100 // Visible height for expanded canvas, overflows for scroll - const minHeight = 35 // Minimized height + const visibleHeight = 100; // Visible height for expanded canvas, overflows for scroll + const minHeight = 35; // Minimized height - super(width, near, far, visibleHeight, minHeight, colorSchema) + super(width, near, far, visibleHeight, minHeight, colorSchema); // Set inherited variables - this.drawCanvas = document.getElementById('transcript-draw') - this.contentCanvas = document.getElementById('transcript-content') - this.trackTitle = document.getElementById('transcript-titles') - this.trackContainer = document.getElementById('transcript-track-container') + this.drawCanvas = document.getElementById("transcript-draw"); + this.contentCanvas = document.getElementById("transcript-content"); + this.trackTitle = document.getElementById("transcript-titles"); + this.trackContainer = document.getElementById("transcript-track-container"); // Setup html objects now that we have gotten the canvas and div elements - this.setupHTML(x + 1) + this.setupHTML(x + 1); // GENS api parameters - this.apiEntrypoint = 'get-transcript-data' + this.apiEntrypoint = "get-transcript-data"; - this.genomeBuild = genomeBuild - this.maxResolution = 4 + this.genomeBuild = genomeBuild; + this.maxResolution = 4; // Define with of the elements - this.geneLineWidth = 2 - initTrackTooltips(this) + this.geneLineWidth = 2; + initTrackTooltips(this); } // draw feature - _drawFeature (feature, heightOrder, canvasYPos, - color, plotFormat) { + _drawFeature(feature, heightOrder, canvasYPos, color, plotFormat) { // Go trough feature list and draw geometries - const scale = this.offscreenPosition.scale + const scale = this.offscreenPosition.scale; // store feature rendering information - const x = scale * (feature.start - this.offscreenPosition.start) - const y = canvasYPos - this.featureHeight / 2 - const width = Math.round(scale * (feature.end - feature.start)) - const height = Math.round(this.featureHeight) + const x = scale * (feature.start - this.offscreenPosition.start); + const y = canvasYPos - this.featureHeight / 2; + const width = Math.round(scale * (feature.end - feature.start)); + const height = Math.round(this.featureHeight); // Draw the geometry that represents the feature - if (feature.feature === 'exon') { + if (feature.feature === "exon") { // generate feature object const visibleCoords = getVisibleXCoordinates({ - canvas: this.onscreenPosition, feature: feature, scale: scale - }) + canvas: this.onscreenPosition, + feature: feature, + scale: scale, + }); const featureObj = { id: feature.exon_number, start: feature.start, @@ -77,8 +84,8 @@ export class TranscriptTrack extends BaseAnnotationTrack { y2: Math.round(y + height), isDisplayed: false, visibleX1: visibleCoords.x1, - visibleX2: visibleCoords.x2 - } + visibleX2: visibleCoords.x2, + }; drawRect({ ctx: this.drawCtx, x: featureObj.x1, @@ -87,19 +94,25 @@ export class TranscriptTrack extends BaseAnnotationTrack { height: height, lineWidth: 1, fillColor: color, - open: false - }) - return featureObj + open: false, + }); + return featureObj; } } // draw transcript figures - async _drawTranscript (element, color, plotFormat, - drawName = true, drawAsArrow = false, addTooltip = true) { - const canvasYPos = this.tracksYPos(element.height_order) - const scale = this.offscreenPosition.scale + async _drawTranscript( + element, + color, + plotFormat, + drawName = true, + drawAsArrow = false, + addTooltip = true, + ) { + const canvasYPos = this.tracksYPos(element.height_order); + const scale = this.offscreenPosition.scale; // sizes - const textSize = plotFormat.textSize + const textSize = plotFormat.textSize; // store element metadata const transcriptObj = { id: element.transcript_id, @@ -110,33 +123,33 @@ export class TranscriptTrack extends BaseAnnotationTrack { mane: element.mane, scale: scale, color: element.mane ? lightenColor(color, 15) : color, // lighten colors for MANE transcripts - features: [] - } + features: [], + }; // Keep track of latest track if (this.heightOrderRecord.latestHeight !== element.height_order) { this.heightOrderRecord = { latestHeight: element.height_order, latestNameEnd: 0, - latestTrackEnd: 0 - } + latestTrackEnd: 0, + }; } // Draw a line to mark gene's length // cap lines at offscreen canvas start/end const displayedTrStart = Math.round( - (transcriptObj.start > this.offscreenPosition.start + transcriptObj.start > this.offscreenPosition.start ? scale * (transcriptObj.start - this.offscreenPosition.start) - : 0) - ) + : 0, + ); const displayedTrEnd = Math.round( - (this.offscreenPosition.end > transcriptObj.end + this.offscreenPosition.end > transcriptObj.end ? scale * (transcriptObj.end - this.offscreenPosition.start) - : this.offscreenPosition.end) - ) + : this.offscreenPosition.end, + ); // store start and end coordinates - transcriptObj.x1 = displayedTrStart - transcriptObj.x2 = displayedTrEnd - transcriptObj.y1 = canvasYPos - (this.geneLineWidth / 2) - transcriptObj.y2 = canvasYPos + (this.geneLineWidth / 2) + transcriptObj.x1 = displayedTrStart; + transcriptObj.x2 = displayedTrEnd; + transcriptObj.y1 = canvasYPos - this.geneLineWidth / 2; + transcriptObj.y2 = canvasYPos + this.geneLineWidth / 2; // draw transcript backbone drawLine({ ctx: this.drawCtx, @@ -145,147 +158,165 @@ export class TranscriptTrack extends BaseAnnotationTrack { y: canvasYPos, y2: canvasYPos, color: transcriptObj.color, - lineWith: this.geneLineWidth // set width of the element - }) + lineWith: this.geneLineWidth, // set width of the element + }); // Draw gene name - const textYPos = this.tracksYPos(element.height_order) + const textYPos = this.tracksYPos(element.height_order); if (drawName) { - const mane = element.mane ? ' [MANE] ' : '' + const mane = element.mane ? " [MANE] " : ""; drawText({ ctx: this.drawCtx, - text: `${transcriptObj.name}${mane}${element.strand === '+' ? '→' : '←'}`, - x: Math.round(((displayedTrEnd - displayedTrStart) / 2) + displayedTrStart), + text: `${transcriptObj.name}${mane}${element.strand === "+" ? "→" : "←"}`, + x: Math.round( + (displayedTrEnd - displayedTrStart) / 2 + displayedTrStart, + ), y: textYPos + this.featureHeight, - fontProp: textSize - }) + fontProp: textSize, + }); } // draw arrows in gene if (drawAsArrow) { drawArrow({ ctx: this.drawCtx, - x: element.strand === '+' ? displayedTrEnd : displayedTrStart, // xPos + x: element.strand === "+" ? displayedTrEnd : displayedTrStart, // xPos y: canvasYPos, // yPos - dir: element.strand === '+' ? 1 : -1, // direction + dir: element.strand === "+" ? 1 : -1, // direction height: this.featureHeight / 2, // height lineWidth: this.geneLineWidth, // lineWidth - color: transcriptObj.color // color - }) + color: transcriptObj.color, // color + }); } else { // draw features for (const feature of element.features) { const featureObj = this._drawFeature( - feature, element.height_order, - canvasYPos, transcriptObj.color, plotFormat - ) + feature, + element.height_order, + canvasYPos, + transcriptObj.color, + plotFormat, + ); if (featureObj !== undefined) { - transcriptObj.features.push(featureObj) + transcriptObj.features.push(featureObj); } } - transcriptObj.y1 = Math.min(...transcriptObj.features.map(feat => feat.y1)) - transcriptObj.y2 = Math.max(...transcriptObj.features.map(feat => feat.y2)) + transcriptObj.y1 = Math.min( + ...transcriptObj.features.map((feat) => feat.y1), + ); + transcriptObj.y2 = Math.max( + ...transcriptObj.features.map((feat) => feat.y2), + ); } // adapt coordinates to global screen coordinates from coorinates local to canvas updateVisibleElementCoordinates({ element: transcriptObj, screenPosition: this.onscreenPosition, - scale: this.offscreenPosition.scale - }) + scale: this.offscreenPosition.scale, + }); // make a virtual representation of the genetic element const virtualElement = makeVirtualDOMElement({ x1: transcriptObj.visibleX1, x2: transcriptObj.visibleX2, y1: transcriptObj.visibleY1, y2: transcriptObj.visibleY2, - canvas: this.contentCanvas - }) + canvas: this.contentCanvas, + }); // create a tooltip html element and append to DOM const elementInfo = [ { title: element.chrom, value: `${element.start}-${element.end}` }, - { title: 'id', value: element.transcript_id } - ] - if (element.refseq_id) { elementInfo.push({ title: 'refSeq', value: element.refseq_id }) } - if (element.hgnc_id) { elementInfo.push({ title: 'hgnc', value: element.hgnc_id }) } - if ( addTooltip ) { + { title: "id", value: element.transcript_id }, + ]; + if (element.refseq_id) { + elementInfo.push({ title: "refSeq", value: element.refseq_id }); + } + if (element.hgnc_id) { + elementInfo.push({ title: "hgnc", value: element.hgnc_id }); + } + if (addTooltip) { const tooltip = createTooltipElement({ id: `popover-${element.transcript_id}`, title: transcriptObj.name, - information: elementInfo - }) + information: elementInfo, + }); // add features to element - addFeatures(element, tooltip) + addFeatures(element, tooltip); // create tooltip - this.trackContainer.appendChild(tooltip) + this.trackContainer.appendChild(tooltip); transcriptObj.tooltip = { instance: createPopper(virtualElement, tooltip, { modifiers: [ - { name: 'offset', options: { offset: [0, virtualElement.getBoundingClientRect().height] } } - ] + { + name: "offset", + options: { + offset: [0, virtualElement.getBoundingClientRect().height], + }, + }, + ], }), virtualElement: virtualElement, tooltip: tooltip, - isDisplayed: false - } - } else { - transcriptObj.tooltip = false + isDisplayed: false, + }; + } else { + transcriptObj.tooltip = false; } - return transcriptObj + return transcriptObj; } // Draws transcripts in given range - async drawOffScreenTrack ({ startPos, endPos, maxHeightOrder, data }) { + async drawOffScreenTrack({ startPos, endPos, maxHeightOrder, data }) { // store positions used when rendering the canvas this.offscreenPosition = { start: startPos, end: endPos, - scale: (this.drawCanvas.width / - (endPos - startPos)) - } + scale: this.drawCanvas.width / (endPos - startPos), + }; // Set needed height of visible canvas and transcript tooltips - this.setContainerHeight(maxHeightOrder) + this.setContainerHeight(maxHeightOrder); // Keeps track of previous values this.heightOrderRecord = { latestHeight: 0, // Latest height order for annotation latestNameEnd: 0, // Latest annotations end position - latestTrackEnd: 0 // Latest annotations title's end position - } + latestTrackEnd: 0, // Latest annotations title's end position + }; // limit drawing of transcript to pre-defined resolutions - let filteredTranscripts = [] + let filteredTranscripts = []; if (this.getResolution < this.maxResolution + 1) { - filteredTranscripts = data.transcripts.filter( - transc => isElementOverlapping( - transc, { start: startPos, end: endPos } - ) - ) + filteredTranscripts = data.transcripts.filter((transc) => + isElementOverlapping(transc, { start: startPos, end: endPos }), + ); } // dont show tracks with no data in them if (filteredTranscripts.length > 0) { - this.setContainerHeight(this.trackData.max_height_order) + this.setContainerHeight(this.trackData.max_height_order); } else { - this.setContainerHeight(0) + this.setContainerHeight(0); } - this.clearTracks() + this.clearTracks(); // define plot formating parameters const plotFormat = { textSize: 10, - titleMargin: 2 - } + titleMargin: 2, + }; // Go through queryResults and draw appropriate symbols - const drawGeneName = this.getResolution < 3 - const drawTooltips = this.getResolution < 4 - const drawExons = this.getResolution < 4 + const drawGeneName = this.getResolution < 3; + const drawTooltips = this.getResolution < 4; + const drawExons = this.getResolution < 4; for (const transc of filteredTranscripts) { - if (!this.expanded && transc.height_order !== 1) { continue } + if (!this.expanded && transc.height_order !== 1) { + continue; + } // draw base transcript - const color = transc.strand === '+' - ? this.colorSchema.strand_pos - : this.colorSchema.strand_neg + const color = + transc.strand === "+" + ? this.colorSchema.strand_pos + : this.colorSchema.strand_neg; // test create some genetic elements and store them const transcriptObj = await this._drawTranscript( transc, @@ -293,9 +324,9 @@ export class TranscriptTrack extends BaseAnnotationTrack { plotFormat, drawGeneName, // if gene names should be drawn !drawExons, // if transcripts should be represented as arrows - drawTooltips, // if tooltips should be added - ) - this.geneticElements.push(transcriptObj) + drawTooltips, // if tooltips should be added + ); + this.geneticElements.push(transcriptObj); } } } diff --git a/assets/js/track/utils.js b/assets/js/track/utils.js index 621676a8..b08c6cc8 100644 --- a/assets/js/track/utils.js +++ b/assets/js/track/utils.js @@ -1,45 +1,62 @@ // Utility functions -export function getVisibleYCoordinates ({ element, minHeight = 4 }) { - let y1 = Math.round(element.y1) - let y2 = Math.round(element.y2) - const height = y2 - y1 +export function getVisibleYCoordinates({ element, minHeight = 4 }) { + let y1 = Math.round(element.y1); + let y2 = Math.round(element.y2); + const height = y2 - y1; if (height < minHeight) { - y1 = Math.round(y1 - ((minHeight - height) / 2)) - y2 = Math.round(y2 + ((minHeight - height) / 2)) + y1 = Math.round(y1 - (minHeight - height) / 2); + y2 = Math.round(y2 + (minHeight - height) / 2); } - return { y1, y2 } + return { y1, y2 }; } -export function getVisibleXCoordinates ({ canvas, feature, scale, minWidth = 4 }) { - let x1 = Math.round((Math.max(0, feature.start - canvas.start)) * scale) - let x2 = Math.round((Math.min(canvas.end, feature.end - canvas.start)) * scale) +export function getVisibleXCoordinates({ + canvas, + feature, + scale, + minWidth = 4, +}) { + let x1 = Math.round(Math.max(0, feature.start - canvas.start) * scale); + let x2 = Math.round(Math.min(canvas.end, feature.end - canvas.start) * scale); if (x2 - x1 < minWidth) { - x1 = Math.round(x1 - (minWidth - (x2 - x1) / 2)) - x2 = Math.round(x2 + (minWidth - (x2 - x1) / 2)) + x1 = Math.round(x1 - (minWidth - (x2 - x1) / 2)); + x2 = Math.round(x2 + (minWidth - (x2 - x1) / 2)); } - return { x1, x2 } + return { x1, x2 }; } // Check if two geometries are overlapping // each input is an object with start/ end coordinates // f >----------------< // s >---------< -export function isElementOverlapping (first, second) { - if ((first.start > second.start && first.start < second.end) || // - (first.end > second.start && first.end < second.end) || - (second.start > first.start && second.start < first.end) || - (second.end > first.start && second.end < first.end)) { - return true +export function isElementOverlapping(first, second) { + if ( + (first.start > second.start && first.start < second.end) || // + (first.end > second.start && first.end < second.end) || + (second.start > first.start && second.start < first.end) || + (second.end > first.start && second.end < first.end) + ) { + return true; } - return false + return false; } // check if point is within an element -export function isWithinElementBbox ({ element, point }) { - return (element.x1 < point.x && point.x < element.x2) && (element.y1 < point.y && point.y < element.y2) +export function isWithinElementBbox({ element, point }) { + return ( + element.x1 < point.x && + point.x < element.x2 && + element.y1 < point.y && + point.y < element.y2 + ); } -export function isWithinElementVisibleBbox ({ element, point }) { - return (element.visibleX1 < point.x && point.x < element.visibleX2) && (element.visibleY1 < point.y && point.y < element.visibleY2) +export function isWithinElementVisibleBbox({ element, point }) { + return ( + element.visibleX1 < point.x && + point.x < element.visibleX2 && + element.visibleY1 < point.y && + point.y < element.visibleY2 + ); } diff --git a/assets/js/track/utils.test.js b/assets/js/track/utils.test.js index 13309bfd..48b870c7 100644 --- a/assets/js/track/utils.test.js +++ b/assets/js/track/utils.test.js @@ -1,93 +1,122 @@ -import { isElementOverlapping, isWithinElementBbox, getVisibleXCoordinates, getVisibleYCoordinates } from './utils.js' +import { + isElementOverlapping, + isWithinElementBbox, + getVisibleXCoordinates, + getVisibleYCoordinates, +} from "./utils.js"; // Test overlapping elements -describe('Test isElementOverlapping', () => { - test('first is within second', () => { - const first = {start: 100, end: 600}, second = {start: 50, end: 1000} - const resp = isElementOverlapping(first, second) - expect(resp).toBeTruthy() - }) - test('first end is overapping second', () => { - const first = {start: 100, end: 600}, second = {start: 500, end: 1000} - const resp = isElementOverlapping(first, second) - expect(resp).toBeTruthy() - }) - test('first start is overapping second', () => { - const first = {start: 100, end: 600}, second = {start: 20, end: 120} - const resp = isElementOverlapping(first, second) - expect(resp).toBeTruthy() - }) - test('first start and second does not overlapp', () => { - const first = {start: 100, end: 200}, second = {start: 500, end: 900} - const resp = isElementOverlapping(first, second) - expect(resp).not.toBeTruthy() - }) -}) +describe("Test isElementOverlapping", () => { + test("first is within second", () => { + const first = { start: 100, end: 600 }, + second = { start: 50, end: 1000 }; + const resp = isElementOverlapping(first, second); + expect(resp).toBeTruthy(); + }); + test("first end is overapping second", () => { + const first = { start: 100, end: 600 }, + second = { start: 500, end: 1000 }; + const resp = isElementOverlapping(first, second); + expect(resp).toBeTruthy(); + }); + test("first start is overapping second", () => { + const first = { start: 100, end: 600 }, + second = { start: 20, end: 120 }; + const resp = isElementOverlapping(first, second); + expect(resp).toBeTruthy(); + }); + test("first start and second does not overlapp", () => { + const first = { start: 100, end: 200 }, + second = { start: 500, end: 900 }; + const resp = isElementOverlapping(first, second); + expect(resp).not.toBeTruthy(); + }); +}); // test if point is within element -describe('Test if point is within element', () => { - const element = {x1: 10, x2: 90, y1: -10, y2: 10} - test('test point within element bbox, xy', () => { - expect(isWithinElementBbox({element, point: {x: 20, y: 5}})).toBeTruthy() - }) - test('test point is outside element bbox', () =>{ - expect(isWithinElementBbox({element, point: {x: 20, y: 15}})).toBeFalsy() - }) - test('test point is on element bbox edge', () => { - expect(isWithinElementBbox({element, point: {x: 10, y: 10}})).toBeFalsy() - expect(isWithinElementBbox({element, point: {x: 50, y: 10}})).toBeFalsy() - }) -}) +describe("Test if point is within element", () => { + const element = { x1: 10, x2: 90, y1: -10, y2: 10 }; + test("test point within element bbox, xy", () => { + expect( + isWithinElementBbox({ element, point: { x: 20, y: 5 } }), + ).toBeTruthy(); + }); + test("test point is outside element bbox", () => { + expect( + isWithinElementBbox({ element, point: { x: 20, y: 15 } }), + ).toBeFalsy(); + }); + test("test point is on element bbox edge", () => { + expect( + isWithinElementBbox({ element, point: { x: 10, y: 10 } }), + ).toBeFalsy(); + expect( + isWithinElementBbox({ element, point: { x: 50, y: 10 } }), + ).toBeFalsy(); + }); +}); // test getVisibleYCoordinates function -describe('Test getVisibleYCoordinates', () => { - test('test element higher than minHeight', () => { - const element = {y1: 10, y2: 40} - const resp = getVisibleYCoordinates({ element, minHeight: 4 }) - expect(resp).toEqual({ y1: 10, y2: 40 }) - }) +describe("Test getVisibleYCoordinates", () => { + test("test element higher than minHeight", () => { + const element = { y1: 10, y2: 40 }; + const resp = getVisibleYCoordinates({ element, minHeight: 4 }); + expect(resp).toEqual({ y1: 10, y2: 40 }); + }); - test('test element shorter than minHeight', () => { - const element = {y1: 10, y2: 20} - const resp = getVisibleYCoordinates({ element, minHeight: 20 }) - expect(resp).toEqual({ y1: 5, y2: 25 }) - }) -}) + test("test element shorter than minHeight", () => { + const element = { y1: 10, y2: 20 }; + const resp = getVisibleYCoordinates({ element, minHeight: 20 }); + expect(resp).toEqual({ y1: 5, y2: 25 }); + }); +}); // test getVisibleXCoordinates function -describe('Test getVisibleXCoordinates', () => { - const canvas = {start: 100, end: 200} - const scale = 0.1 +describe("Test getVisibleXCoordinates", () => { + const canvas = { start: 100, end: 200 }; + const scale = 0.1; - test('test feature inside visable canvas', () => { - const feature = {start: 120, end: 150} - const resp = getVisibleXCoordinates({ - canvas, feature, scale, minWidth: 1 - }) - expect(resp).toEqual({ x1: 2, x2: 5 }) - }) + test("test feature inside visable canvas", () => { + const feature = { start: 120, end: 150 }; + const resp = getVisibleXCoordinates({ + canvas, + feature, + scale, + minWidth: 1, + }); + expect(resp).toEqual({ x1: 2, x2: 5 }); + }); - test('test feature inside visable canvas, no scale', () => { - const feature = {start: 120, end: 150} - const resp = getVisibleXCoordinates({ - canvas, feature, scale: 1, minWidth: 1 - }) - expect(resp).toEqual({ x1: 20, x2: 50 }) - }) + test("test feature inside visable canvas, no scale", () => { + const feature = { start: 120, end: 150 }; + const resp = getVisibleXCoordinates({ + canvas, + feature, + scale: 1, + minWidth: 1, + }); + expect(resp).toEqual({ x1: 20, x2: 50 }); + }); - test('test feature partly inside visable canvas, caped at begining', () => { - const feature = {start: 90, end: 150} - const resp = getVisibleXCoordinates({ - canvas, feature, scale: 1, minWidth: 1 - }) - expect(resp).toEqual({ x1: 0, x2: 50 }) - }) + test("test feature partly inside visable canvas, caped at begining", () => { + const feature = { start: 90, end: 150 }; + const resp = getVisibleXCoordinates({ + canvas, + feature, + scale: 1, + minWidth: 1, + }); + expect(resp).toEqual({ x1: 0, x2: 50 }); + }); - test('test feature partly inside visable canvas, caped at end', () => { - const feature = {start: 120, end: 600} - const resp = getVisibleXCoordinates({ - canvas, feature, scale: 1, minWidth: 1 - }) - expect(resp).toEqual({ x1: 20, x2: 200 }) - }) -}) + test("test feature partly inside visable canvas, caped at end", () => { + const feature = { start: 120, end: 600 }; + const resp = getVisibleXCoordinates({ + canvas, + feature, + scale: 1, + minWidth: 1, + }); + expect(resp).toEqual({ x1: 20, x2: 200 }); + }); +}); diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 8d482529..38b97564 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -1,64 +1,96 @@ // Variant track definition -import { BaseAnnotationTrack } from './base.js' -import { isElementOverlapping, isWithinElementBbox, isWithinElementVisibleBbox } from './utils.js' -import { drawRect, drawLine, drawWaveLine, drawText } from '../draw.js' -import { initTrackTooltips, createTooltipElement, makeVirtualDOMElement, updateVisibleElementCoordinates } from './tooltip.js' -import { createPopper } from '@popperjs/core' +import { BaseAnnotationTrack } from "./base.js"; +import { + isElementOverlapping, + isWithinElementBbox, + isWithinElementVisibleBbox, +} from "./utils.js"; +import { drawRect, drawLine, drawWaveLine, drawText } from "../draw.js"; +import { + initTrackTooltips, + createTooltipElement, + makeVirtualDOMElement, + updateVisibleElementCoordinates, +} from "./tooltip.js"; +import { createPopper } from "@popperjs/core"; // Draw variants -const VARIANT_TR_TABLE = { del: 'deletion', dup: 'duplication', cnv: 'copy number variation', inv: 'inversion', bnd: 'break end' } +const VARIANT_TR_TABLE = { + del: "deletion", + dup: "duplication", + cnv: "copy number variation", + inv: "inversion", + bnd: "break end", +}; export class VariantTrack extends BaseAnnotationTrack { - constructor (x, width, near, far, caseId, genomeBuild, colorSchema, scoutBaseURL, highlightedVariantId) { + constructor( + x, + width, + near, + far, + caseId, + genomeBuild, + colorSchema, + scoutBaseURL, + highlightedVariantId, + ) { // Dimensions of track canvas - const visibleHeight = 100 // Visible height for expanded canvas, overflows for scroll - const minHeight = 35 // Minimized height + const visibleHeight = 100; // Visible height for expanded canvas, overflows for scroll + const minHeight = 35; // Minimized height - super(width, near, far, visibleHeight, minHeight, colorSchema) + super(width, near, far, visibleHeight, minHeight, colorSchema); // Set inherited variables - this.drawCanvas = document.getElementById('variant-draw') - this.contentCanvas = document.getElementById('variant-content') - this.trackTitle = document.getElementById('variant-titles') - this.trackContainer = document.getElementById('variant-track-container') - this.scoutBaseURL = scoutBaseURL + this.drawCanvas = document.getElementById("variant-draw"); + this.contentCanvas = document.getElementById("variant-content"); + this.trackTitle = document.getElementById("variant-titles"); + this.trackContainer = document.getElementById("variant-track-container"); + this.scoutBaseURL = scoutBaseURL; // Add click menu event listener linking out to the Scout variant - this.trackContainer.addEventListener('click', async (event) => { - for (const element of this.geneticElements) { - const rect = this.contentCanvas.getBoundingClientRect() - const point = { x: (event.clientX - rect.left), y: event.clientY - rect.top } - if (isWithinElementVisibleBbox({ element, point })) { - const url = this.scoutBaseURL + '/document_id/' + element.id - console.log(`Visit ${url}: Scout variant`) - const win = window.open(url, '_blank') - win.focus() + this.trackContainer.addEventListener( + "click", + async (event) => { + for (const element of this.geneticElements) { + const rect = this.contentCanvas.getBoundingClientRect(); + const point = { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; + if (isWithinElementVisibleBbox({ element, point })) { + const url = this.scoutBaseURL + "/document_id/" + element.id; + console.log(`Visit ${url}: Scout variant`); + const win = window.open(url, "_blank"); + win.focus(); + } } - } - }, false) - this.featureHeight = 18 + }, + false, + ); + this.featureHeight = 18; // Setup html objects now that we have gotten the canvas and div elements - this.setupHTML(x + 1) + this.setupHTML(x + 1); - this.trackContainer.style.marginTop = '-1px' - this.genomeBuild = genomeBuild + this.trackContainer.style.marginTop = "-1px"; + this.genomeBuild = genomeBuild; // GENS api parameters - this.apiEntrypoint = 'get-variant-data' + this.apiEntrypoint = "get-variant-data"; this.additionalQueryParams = { - variant_category: 'sv', - case_id: caseId - } + variant_category: "sv", + case_id: caseId, + }; // Initialize highlighted variant - this.highlightedVariantId = highlightedVariantId - initTrackTooltips(this) + this.highlightedVariantId = highlightedVariantId; + initTrackTooltips(this); // Initialize label tracking - this.labelData = [] + this.labelData = []; } // Draw highlight for a given region - drawHighlight (startPos, endPos, color = 'rgb(255, 200, 87, 0.5)') { + drawHighlight(startPos, endPos, color = "rgb(255, 200, 87, 0.5)") { drawRect({ ctx: this.drawCtx, x: startPos, @@ -67,152 +99,166 @@ export class VariantTrack extends BaseAnnotationTrack { height: this.visibleHeight, lineWidth: 0, fillColor: color, - open: false - }) + open: false, + }); } - async drawOffScreenTrack ({ startPos, endPos, maxHeightOrder, data }) { + async drawOffScreenTrack({ startPos, endPos, maxHeightOrder, data }) { // Draws variants in given range - const textSize = 10 + const textSize = 10; // store positions used when rendering the canvas this.offscreenPosition = { start: startPos, end: endPos, - scale: this.drawCanvas.width / - (endPos - startPos) - } - const scale = this.offscreenPosition.scale + scale: this.drawCanvas.width / (endPos - startPos), + }; + const scale = this.offscreenPosition.scale; // Set needed height of visible canvas and transcript tooltips - this.setContainerHeight(maxHeightOrder) + this.setContainerHeight(maxHeightOrder); // Keeps track of previous values this.heightOrderRecord = { latestHeight: 0, // Latest height order for annotation latestNameEnd: 0, // Latest annotations end position - latestTrackEnd: 0 // Latest annotations title's end position - } + latestTrackEnd: 0, // Latest annotations title's end position + }; // limit drawing of annotations to pre-defined resolutions - let filteredVariants = [] + let filteredVariants = []; if (this.getResolution < this.maxResolution + 1) { - filteredVariants = data - .variants - .filter(variant => isElementOverlapping( + filteredVariants = data.variants.filter((variant) => + isElementOverlapping( { start: variant.position, end: variant.end }, - { start: startPos, end: endPos })) + { start: startPos, end: endPos }, + ), + ); } - filteredVariants.sort((a, b) => a.position - b.position) + filteredVariants.sort((a, b) => a.position - b.position); - let heightTracker = Array(200) - let actualMaxHeightOrder = 1 + let heightTracker = Array(200); + let actualMaxHeightOrder = 1; for (const variant of filteredVariants) { - const variantCategory = variant.sub_category // del, dup, sv, str - if (['dup', 'del', 'cnv'].includes(variantCategory)) { - let heightOrder = 1 - while (heightTracker[heightOrder] >= variant.position) - heightOrder += 1 - heightTracker[heightOrder] = variant.end - actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder) + const variantCategory = variant.sub_category; // del, dup, sv, str + if (["dup", "del", "cnv"].includes(variantCategory)) { + let heightOrder = 1; + while (heightTracker[heightOrder] >= variant.position) heightOrder += 1; + heightTracker[heightOrder] = variant.end; + actualMaxHeightOrder = Math.max(actualMaxHeightOrder, heightOrder); } } - - this.trackData.max_height_order = actualMaxHeightOrder + + this.trackData.max_height_order = actualMaxHeightOrder; // dont show tracks with no data in them - if (filteredVariants.length > 0 && - this.getResolution < this.maxResolution + 1 + if ( + filteredVariants.length > 0 && + this.getResolution < this.maxResolution + 1 ) { - this.setContainerHeight(this.trackData.max_height_order) + this.setContainerHeight(this.trackData.max_height_order); } else { - this.setContainerHeight(0) + this.setContainerHeight(0); } - this.clearTracks() + this.clearTracks(); - heightTracker = Array(200) + heightTracker = Array(200); - const labelData = [] + const labelData = []; // Draw track - const drawTooltips = this.getResolution < 4 + const drawTooltips = this.getResolution < 4; for (const variant of filteredVariants) { - const variantCategory = variant.sub_category // del, dup, sv, str - const variantType = variant.variant_type - const variantLength = variant.length - const color = this.colorSchema[variantCategory] || this.colorSchema.default || 'black' - - let heightOrder = 1 - if (['dup', 'del', 'cnv'].includes(variantCategory)) { - while (heightTracker[heightOrder] >= variant.position) - heightOrder += 1 - heightTracker[heightOrder] = variant.end + const variantCategory = variant.sub_category; // del, dup, sv, str + const variantType = variant.variant_type; + const variantLength = variant.length; + const color = + this.colorSchema[variantCategory] || + this.colorSchema.default || + "black"; + + let heightOrder = 1; + if (["dup", "del", "cnv"].includes(variantCategory)) { + while (heightTracker[heightOrder] >= variant.position) heightOrder += 1; + heightTracker[heightOrder] = variant.end; } - const canvasYPos = this.tracksYPos(heightOrder) + const canvasYPos = this.tracksYPos(heightOrder); // Only draw visible tracks - if (!this.expanded && heightOrder !== 1) { continue } + if (!this.expanded && heightOrder !== 1) { + continue; + } // create variant object - const featureHeight = variantCategory === 'del' ? 7 : 8 + const featureHeight = variantCategory === "del" ? 7 : 8; const variantObj = { id: variant.document_id, name: variant.display_name, start: variant.position, end: variant.end, - x1: Math.round(scale * (variant.position - this.offscreenPosition.start)), + x1: Math.round( + scale * (variant.position - this.offscreenPosition.start), + ), x2: Math.round(scale * (variant.end - this.offscreenPosition.start)), y1: canvasYPos, - y2: Math.round((canvasYPos + featureHeight)), + y2: Math.round(canvasYPos + featureHeight), features: [], isDisplayed: false, tooltip: false, - } + }; // get onscreen positions for offscreen xy coordinates updateVisibleElementCoordinates({ element: variantObj, screenPosition: this.onscreenPosition, - scale: this.offscreenPosition.scale - }) + scale: this.offscreenPosition.scale, + }); // create a tooltip html element and append to DOM - if (drawTooltips && ['dup', 'del', 'cnv'].includes(variantCategory)) { + if (drawTooltips && ["dup", "del", "cnv"].includes(variantCategory)) { const tooltip = createTooltipElement({ id: `popover-${variantObj.id}`, title: `${variantType.toUpperCase()}: ${variant.category} - ${VARIANT_TR_TABLE[variantCategory]}`, information: [ - { title: 'Type', value: variant.category }, + { title: "Type", value: variant.category }, { title: variant.chromosome, value: `${variant.position}` }, - { title: 'Ref', value: `${variant.reference}` }, - { title: 'Alt', value: `${variant.alternative}` }, - { title: 'Cytoband start/end', value: `${variant.cytoband_start}/${variant.cytoband_end}` }, - { title: 'Length', value: `${variant.length}` }, - { title: 'Quality', value: `${variant.quality}` }, - { title: 'Rank score', value: `${variant.rank_score}` } - ] - }) - this.trackContainer.appendChild(tooltip) + { title: "Ref", value: `${variant.reference}` }, + { title: "Alt", value: `${variant.alternative}` }, + { + title: "Cytoband start/end", + value: `${variant.cytoband_start}/${variant.cytoband_end}`, + }, + { title: "Length", value: `${variant.length}` }, + { title: "Quality", value: `${variant.quality}` }, + { title: "Rank score", value: `${variant.rank_score}` }, + ], + }); + this.trackContainer.appendChild(tooltip); // make a virtual element as tooltip hitbox const virtualElement = makeVirtualDOMElement({ x1: variantObj.visibleX1, x2: variantObj.visibleX2, y1: variantObj.visibleY1, y2: variantObj.visibleY2, - canvas: this.contentCanvas - }) + canvas: this.contentCanvas, + }); // add tooltip to variantObj variantObj.tooltip = { instance: createPopper(virtualElement, tooltip, { modifiers: [ - { name: 'offset', options: { offset: [0, virtualElement.getBoundingClientRect().height] } } - ] + { + name: "offset", + options: { + offset: [0, virtualElement.getBoundingClientRect().height], + }, + }, + ], }), virtualElement: virtualElement, tooltip: tooltip, - isDisplayed: false - } + isDisplayed: false, + }; } - if (['dup', 'del', 'cnv'].includes(variantCategory)) { - this.geneticElements.push(variantObj) + if (["dup", "del", "cnv"].includes(variantCategory)) { + this.geneticElements.push(variantObj); } // Keep track of latest track @@ -220,44 +266,44 @@ export class VariantTrack extends BaseAnnotationTrack { this.heightOrderRecord = { latestHeight: heightOrder, latestNameEnd: 0, - latestTrackEnd: 0 - } + latestTrackEnd: 0, + }; } // Draw motif line switch (variantCategory) { - case 'del': + case "del": drawWaveLine({ ctx: this.drawCtx, x: variantObj.x1, y: variantObj.y2, x2: variantObj.x2, height: featureHeight, - color - }) - break - case 'cnv': - case 'dup': + color, + }); + break; + case "cnv": + case "dup": drawLine({ ctx: this.drawCtx, x: variantObj.x1, y: variantObj.y1, x2: variantObj.x2, y2: variantObj.y1, - color - }) + color, + }); drawLine({ ctx: this.drawCtx, x: variantObj.x1, y: variantObj.y2, x2: variantObj.x2, y2: variantObj.y2, - color - }) - break - case 'bnd': - case 'inv': + color, + }); + break; + case "bnd": + case "inv": // no support for balanced events - break + break; default: // other types of elements drawLine({ ctx: this.drawCtx, @@ -265,17 +311,19 @@ export class VariantTrack extends BaseAnnotationTrack { y: variantObj.y1, x2: variantObj.x2, y2: variantObj.y1, - color - }) - console.log(`Unhandled variant type ${variantCategory}; drawing default shape`) + color, + }); + console.log( + `Unhandled variant type ${variantCategory}; drawing default shape`, + ); } // Move and display highlighted region if (variant._id === this.highlightedVariantId) { - this.drawHighlight(variantObj.x1, variantObj.x2) + this.drawHighlight(variantObj.x1, variantObj.x2); } - if (['dup', 'del', 'cnv'].includes(variantCategory)) { - const textYPos = this.tracksYPos(heightOrder) + if (["dup", "del", "cnv"].includes(variantCategory)) { + const textYPos = this.tracksYPos(heightOrder); // Draw variant type labelData.push({ text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, @@ -283,8 +331,8 @@ export class VariantTrack extends BaseAnnotationTrack { end: variantObj.end, x: (variantObj.start + variantObj.end) / 2, y: textYPos + this.featureHeight, - fontProp: textSize - }) + fontProp: textSize, + }); /* drawText({ ctx: this.drawCtx, text: `${variant.category} - ${variantType} ${VARIANT_TR_TABLE[variantCategory]}; length: ${variantLength}`, @@ -294,23 +342,34 @@ export class VariantTrack extends BaseAnnotationTrack { }) */ } } - this.labelData = labelData + this.labelData = labelData; } - drawDynamicOverlay () { - const ctx = this.contentCanvas.getContext('2d') - const scale = this.contentCanvas.width / (this.onscreenPosition.end - this.onscreenPosition.start) - const margin = 100 + drawDynamicOverlay() { + const ctx = this.contentCanvas.getContext("2d"); + const scale = + this.contentCanvas.width / + (this.onscreenPosition.end - this.onscreenPosition.start); + const margin = 100; this.labelData.forEach((label) => { - if (label.start < this.onscreenPosition.end && label.end > this.onscreenPosition.start) { + if ( + label.start < this.onscreenPosition.end && + label.end > this.onscreenPosition.start + ) { drawText({ ctx: ctx, text: label.text, - x: Math.max(margin, Math.min(this.contentCanvas.width - margin, scale * (label.x - this.onscreenPosition.start))), + x: Math.max( + margin, + Math.min( + this.contentCanvas.width - margin, + scale * (label.x - this.onscreenPosition.start), + ), + ), y: label.y, - fontProp: label.fontProp - }) + fontProp: label.fontProp, + }); } - }) + }); } } From 1473475eb0e275824ea76fd1af3f9aedbf92c1ac Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 16:36:05 +0200 Subject: [PATCH 133/138] oc to this - looks like a small bug --- assets/js/track/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/track/base.js b/assets/js/track/base.js index dfaa7032..3510505b 100644 --- a/assets/js/track/base.js +++ b/assets/js/track/base.js @@ -224,7 +224,7 @@ export class BaseAnnotationTrack { Object.assign( { // build query parameters - sample_id: oc.sampleName, + sample_id: this.sampleName, region: `${chrom}:1-None`, genome_build: this.genomeBuild, collapsed: false, // allways get all height orders From dee3b61a42979824ff7fcac219a8e30acd7d0a48 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 16:44:09 +0200 Subject: [PATCH 134/138] unused imports --- assets/js/track/ideogram.js | 1 - assets/js/track/variant.js | 1 - 2 files changed, 2 deletions(-) diff --git a/assets/js/track/ideogram.js b/assets/js/track/ideogram.js index d57b8f67..9363e680 100644 --- a/assets/js/track/ideogram.js +++ b/assets/js/track/ideogram.js @@ -5,7 +5,6 @@ import { lightenColor } from "./base.js"; import tippy, { followCursor } from "tippy.js"; import "tippy.js/dist/tippy.css"; import { isElementOverlapping } from "./utils.js"; -import { thisExpression } from "@babel/types"; export class CytogeneticIdeogram { constructor({ targetId, genomeBuild, x, y, width, height }) { diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 38b97564..80392c07 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -3,7 +3,6 @@ import { BaseAnnotationTrack } from "./base.js"; import { isElementOverlapping, - isWithinElementBbox, isWithinElementVisibleBbox, } from "./utils.js"; import { drawRect, drawLine, drawWaveLine, drawText } from "../draw.js"; From 91024a7d661c2636846da4d1354f33a7dd2a6970 Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 17:00:45 +0200 Subject: [PATCH 135/138] replace a couple of map statements with forEach --- assets/js/track/ideogram.js | 25 ++++++++++++++----------- assets/js/track/variant.js | 5 +---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/assets/js/track/ideogram.js b/assets/js/track/ideogram.js index 9363e680..f353a528 100644 --- a/assets/js/track/ideogram.js +++ b/assets/js/track/ideogram.js @@ -46,17 +46,19 @@ export class CytogeneticIdeogram { plugins: [followCursor], }); - // register event handeler for updating popups + // register event handler for updating popups + const ctx = canvas.getContext("2d"); + canvas.addEventListener("mousemove", (event) => { - this.drawPaths !== null && - this.drawPaths.bands.map((bandPath) => { - const ctx = canvas.getContext("2d"); + if (this.drawPaths !== null) { + this.drawPaths.bands.forEach((bandPath) => { if (ctx.isPointInPath(bandPath.path, event.offsetX, event.offsetY)) { - tooltip.querySelector(".ideogram-tooltip-value").innerHTML = - bandPath.id; + tooltip.querySelector(".ideogram-tooltip-value").innerHTML = bandPath.id; } }); + } }); + // register event for moving and zooming region marker this.targetElement.addEventListener("mark-region", (event) => { // if marking a subset of chromosome @@ -117,14 +119,15 @@ export function setupGenericEventManager({ ownerElement, targetElementIds, }) { - // pass directed from owner element to taget elements + // pass directed from owner element to target elements ownerElement.addEventListener(eventName, (event) => { - targetElementIds.map((id) => { - document - .getElementById(id) - .dispatchEvent(new CustomEvent(eventName, { detail: event.detail })); + targetElementIds.forEach((id) => { + document + .getElementById(id) + .dispatchEvent(new CustomEvent(eventName, { detail: event.detail })); }); }); + } function createChromosomeTooltip({ bandId }) { diff --git a/assets/js/track/variant.js b/assets/js/track/variant.js index 80392c07..660fd92f 100644 --- a/assets/js/track/variant.js +++ b/assets/js/track/variant.js @@ -1,10 +1,7 @@ // Variant track definition import { BaseAnnotationTrack } from "./base.js"; -import { - isElementOverlapping, - isWithinElementVisibleBbox, -} from "./utils.js"; +import { isElementOverlapping, isWithinElementVisibleBbox } from "./utils.js"; import { drawRect, drawLine, drawWaveLine, drawText } from "../draw.js"; import { initTrackTooltips, From 78146d45f6f9d172ea44b78c5d664785d1e98deb Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 17:07:13 +0200 Subject: [PATCH 136/138] note the global apiHost in request js --- assets/js/fetch.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/js/fetch.js b/assets/js/fetch.js index 52e9c277..c7f54fed 100644 --- a/assets/js/fetch.js +++ b/assets/js/fetch.js @@ -1,5 +1,7 @@ // Fetch.js // functions for making api requests to Gens +/* global _apiHost */ + async function request(url, params, method = "GET") { // options passed to hte fetch request From 3deeb3928c7f23ff1ad60085e9a0edd3c630cabc Mon Sep 17 00:00:00 2001 From: Daniel Nilsson Date: Fri, 25 Oct 2024 17:18:11 +0200 Subject: [PATCH 137/138] format --- assets/js/fetch.js | 1 - assets/js/track/ideogram.js | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/js/fetch.js b/assets/js/fetch.js index c7f54fed..fbdb3c21 100644 --- a/assets/js/fetch.js +++ b/assets/js/fetch.js @@ -2,7 +2,6 @@ // functions for making api requests to Gens /* global _apiHost */ - async function request(url, params, method = "GET") { // options passed to hte fetch request const options = { diff --git a/assets/js/track/ideogram.js b/assets/js/track/ideogram.js index f353a528..a2466ef3 100644 --- a/assets/js/track/ideogram.js +++ b/assets/js/track/ideogram.js @@ -53,7 +53,8 @@ export class CytogeneticIdeogram { if (this.drawPaths !== null) { this.drawPaths.bands.forEach((bandPath) => { if (ctx.isPointInPath(bandPath.path, event.offsetX, event.offsetY)) { - tooltip.querySelector(".ideogram-tooltip-value").innerHTML = bandPath.id; + tooltip.querySelector(".ideogram-tooltip-value").innerHTML = + bandPath.id; } }); } @@ -121,13 +122,12 @@ export function setupGenericEventManager({ }) { // pass directed from owner element to target elements ownerElement.addEventListener(eventName, (event) => { - targetElementIds.forEach((id) => { - document - .getElementById(id) - .dispatchEvent(new CustomEvent(eventName, { detail: event.detail })); + targetElementIds.forEach((id) => { + document + .getElementById(id) + .dispatchEvent(new CustomEvent(eventName, { detail: event.detail })); }); }); - } function createChromosomeTooltip({ bandId }) { From 3301c1987248d1f55625ce892c27339c26af98a9 Mon Sep 17 00:00:00 2001 From: Jakob Willforss Date: Tue, 19 Nov 2024 10:35:58 +0100 Subject: [PATCH 138/138] Sync remaining diff with Stockholm --- CHANGELOG.md | 51 ++++++++++++++---------- gens/blueprints/gens/templates/gens.html | 1 - gens/blueprints/home/templates/home.html | 2 +- gens/commands/base.py | 2 + gens/db/__init__.py | 2 +- gens/db/annotation.py | 2 +- package-lock.json | 35 ++++++++++++++++ package.json | 6 +++ setup.py | 11 ++--- 9 files changed, 80 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c99638b9..ecdfad95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,28 +6,34 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Added -- `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. -- `--force` flag prints a warning to stderr when overwriting an existing sample. -- `gens delete sample` command -- Height ordering for variants track. + - `--force` flag to `gens loads sample` for overwriting any existing sample in case of key conflict. + - `--force` flag prints a warning to stderr when overwriting an existing sample. + - `gens delete sample` command + - Height ordering for variants track. ### Fixed -- Pan able to exit chrosome when using genome build 17 -- `--force` flag `update_one` call not being called properly -- Incorrect total sample count on home page. -- Some typos and documentation. -- Labels often not being visible on larger variants. + - Pan able to exit chrosome when using genome build 17 + - `--force` flag `update_one` call not being called properly + - Incorrect total sample count on home page. + - Some typos and documentation. + - Labels often not being visible on larger variants. +### Merged for Solna from Lund 2.1.2 +#### Changed + - Changed cached method from simple to file system as it would be thread safe +#### Fixed + - Fixed cache issue that could result in chromosome information not being updated + - Fixed max arg error when searching for some genes + - Fixed bug that prevented updating annotation tracks ## [2.3] ### Added -- Link out to Scout: introduce config variable for base URL -- Link out to Scout: case links on home sample list -- Link out to Scout: click variant to open Scout page - + - Link out to Scout: introduce config variable for base URL + - Link out to Scout: case links on home sample list + - Link out to Scout: click variant to open Scout page ### Changed -- Archive prod docker image with release tag name. Update action versions. + - Archive prod docker image with release tag name. Update action versions. ### Fixed -- Error image background static path -- GitHub action DockerHub push on release + - Error image background static path + - GitHub action DockerHub push on release ## [2.2] ### Added @@ -36,19 +42,24 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/) ### Changed - Use sample id instead of display name for variant retrieval - Hide balanced variants - - Fixed bug that prevented updating annotation tracks + - Keyboard pan speed increased - Don't shrink pan window when attemting to pan over start -## [2.1.1] +## [2.1.1b] ### Added ### Changed - - Changed cached method from simple to file system as it would be thread safe - Changes the main view's page title to be `sample_name` and adds `sample_name` and `case_id` to the header title - Updated external images used in GitHub actions, including tj-actions/branch-names to v7 (fixes a security issue) - Updated Python and MongoDB version used in tests workflow to 3.8 and 7 respectively + +## [2.1.2 Lund only - Solna version in Unreleased/2.4] +### Added +### Changed + - Changed cached method from simple to file system as it would be thread safe ### Fixed - Fixed cache issue that could result in chromosome information not being updated - - Fixed max arg error when searching for some genes + - Fixed max arg error when searching for some genes + - Fixed bug that prevented updating annotation tracks ## [2.1.1] ### Added diff --git a/gens/blueprints/gens/templates/gens.html b/gens/blueprints/gens/templates/gens.html index bfefdc9a..43e0f2d4 100644 --- a/gens/blueprints/gens/templates/gens.html +++ b/gens/blueprints/gens/templates/gens.html @@ -8,7 +8,6 @@
-
{% endmacro %} diff --git a/gens/blueprints/home/templates/home.html b/gens/blueprints/home/templates/home.html index 3b16e5cd..bf398731 100644 --- a/gens/blueprints/home/templates/home.html +++ b/gens/blueprints/home/templates/home.html @@ -36,7 +36,7 @@

Samples

Sample id Case id Genome build - Overivew file + Overview file BAM/BAF found Import date diff --git a/gens/commands/base.py b/gens/commands/base.py index 62f2ac43..f8a14f9a 100644 --- a/gens/commands/base.py +++ b/gens/commands/base.py @@ -9,6 +9,7 @@ from .index import index as index_command from .load import load as load_command from .view import view as view_command +from .delete import delete as delete_command @click.group( @@ -26,3 +27,4 @@ def cli(*args, **kwargs): cli.add_command(index_command) cli.add_command(load_command) cli.add_command(view_command) +cli.add_command(delete_command) diff --git a/gens/db/__init__.py b/gens/db/__init__.py index f5fe0f0c..d7c34aed 100644 --- a/gens/db/__init__.py +++ b/gens/db/__init__.py @@ -9,4 +9,4 @@ from .index import create_index, create_indexes, get_indexes, update_indexes from .samples import COLLECTION as SAMPLES_COLLECTION from .samples import (SampleNotFoundError, get_samples, query_sample, - store_sample) + store_sample, delete_sample) diff --git a/gens/db/annotation.py b/gens/db/annotation.py index b65727bf..1eff37f1 100644 --- a/gens/db/annotation.py +++ b/gens/db/annotation.py @@ -22,7 +22,7 @@ def register_data_update(track_type, name=None): db = app.config["GENS_DB"][UPDATES] LOG.debug(f"Creating timestamp for {track_type}") track = {"track": track_type, "name": name} - db.delete_one(track) # remove old track + db.delete_many(track) # remove old track db.insert_one({**track, "timestamp": datetime.datetime.now()}) diff --git a/package-lock.json b/package-lock.json index a61530e2..3438e244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@babel/preset-env": "^7.13.10", "css-loader": "^5.2.7", "eslint": "^7.22.0", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -29,6 +30,7 @@ "gulp-sourcemaps": "^3.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", + "prettier": "3.3.3", "process": "^0.11.10", "regenerator-runtime": "^0.13.7", "sass": "^1.32.8", @@ -5007,6 +5009,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-standard": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", @@ -11693,6 +11707,8 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "inBundle": true, "license": "ISC", "dependencies": { @@ -11736,6 +11752,8 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "inBundle": true, "license": "MIT" }, @@ -13017,6 +13035,8 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "inBundle": true, "license": "ISC", "dependencies": { @@ -14229,6 +14249,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", diff --git a/package.json b/package.json index 66103eb8..44f51aca 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "scripts": { "build": "gulp build", "lint": "eslint assets", + "lint:fix": "npm run lint -- --fix", + "prettier": "npx prettier assets --check", + "prettier:fix": "npm run prettier -- --write", + "format": "npm run prettier:fix && npm run lint:fix", "watch": "gulp watch", "test": "jest" }, @@ -47,6 +51,7 @@ "@babel/preset-env": "^7.13.10", "css-loader": "^5.2.7", "eslint": "^7.22.0", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -58,6 +63,7 @@ "gulp-sourcemaps": "^3.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", + "prettier": "3.3.3", "process": "^0.11.10", "regenerator-runtime": "^0.13.7", "sass": "^1.32.8", diff --git a/setup.py b/setup.py index acb5b2dc..45c662d5 100644 --- a/setup.py +++ b/setup.py @@ -25,14 +25,9 @@ packages=find_packages(), zip_safe=False, install_requires=[ - "Click>=7.0", - "Flask>=1.1.2, <2.3", - "flask-debugtoolbar>=0.11.0", - "flask-caching>=1.9.0", - "itsdangerous>=1.1.0", - "Jinja2>=2.11.1", - "MarkupSafe>=1.1.1", - "Werkzeug>=1.0.0", + "Click", + "Flask", + "flask-caching", "pymongo>=3.9.0", "gtfparse>=1.2.0", "pysam>=0.15.4",