├── packages ├── web │ ├── src │ │ ├── ui │ │ │ ├── style.css │ │ │ ├── contexts │ │ │ │ ├── index.ts │ │ │ │ └── application.tsx │ │ │ ├── index.ts │ │ │ ├── components │ │ │ │ ├── tracks.scss │ │ │ │ ├── index.ts │ │ │ │ ├── footer.scss │ │ │ │ ├── tracks.tsx │ │ │ │ ├── header.scss │ │ │ │ ├── elements.scss │ │ │ │ ├── footer.tsx │ │ │ │ ├── elements.tsx │ │ │ │ └── header.tsx │ │ │ └── ui.tsx │ │ ├── lib │ │ │ ├── .gitignore │ │ │ └── inject.js │ │ ├── vite-env.d.ts │ │ ├── repo │ │ │ ├── index.ts │ │ │ ├── repo.ts │ │ │ └── local.ts │ │ ├── renderer │ │ │ ├── index.ts │ │ │ ├── renderer.ts │ │ │ └── three.ts │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── layout.ts │ │ │ └── tubemap.ts │ │ ├── utils │ │ │ ├── math.ts │ │ │ └── events.ts │ │ ├── main.scss │ │ ├── main.ts │ │ ├── config.ts │ │ └── pgv.ts │ ├── public │ │ └── examples │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ └── tsconfig.json └── core │ ├── .eslintrc.json │ ├── __tests__ │ ├── index.test.ts │ └── model │ │ ├── index.test.ts │ │ └── tiny.vg.json │ ├── tsconfig.json │ ├── src │ ├── index.ts │ └── model │ │ ├── pgv.ts │ │ ├── vg.ts │ │ └── index.ts │ └── package.json ├── examples ├── x │ ├── x.vg │ ├── x.xg │ ├── chunk_0_ids_1_130.annotate.txt │ ├── x.chunked.xg │ ├── regions.tsv │ ├── x.xg.json │ └── x.chunked.xg.json ├── tiny │ ├── tiny.vg │ ├── tiny.xg │ └── tiny.xg.json ├── README └── sources.json ├── scripts ├── docker │ ├── start.sh │ └── nginx.conf ├── prebuild.sh └── postbuild.js ├── archive ├── BME_230A_final_ppt.pdf └── BME_230A_final_paper.pdf ├── .prettierrc.json ├── .gitignore ├── .dockerignore ├── .stylelintrc.json ├── tsconfig.json ├── .eslintrc.json ├── Dockerfile ├── LICENSE ├── package.json ├── .github └── workflows │ └── deploy.yml ├── devnotes.md ├── README.md └── cli.py /packages/web/src/ui/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/web/public/examples: -------------------------------------------------------------------------------- 1 | ../../../examples/ -------------------------------------------------------------------------------- /packages/web/src/lib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !inject.js 4 | -------------------------------------------------------------------------------- /packages/web/src/ui/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./application" 2 | -------------------------------------------------------------------------------- /packages/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/x/x.vg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/examples/x/x.vg -------------------------------------------------------------------------------- /examples/x/x.xg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/examples/x/x.xg -------------------------------------------------------------------------------- /examples/x/chunk_0_ids_1_130.annotate.txt: -------------------------------------------------------------------------------- 1 | thread_0 1 2 | thread_1 1 3 | x[0] 1 4 | -------------------------------------------------------------------------------- /examples/tiny/tiny.vg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/examples/tiny/tiny.vg -------------------------------------------------------------------------------- /examples/tiny/tiny.xg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/examples/tiny/tiny.xg -------------------------------------------------------------------------------- /scripts/docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | nginx -g "daemon off;" 4 | -------------------------------------------------------------------------------- /examples/x/x.chunked.xg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/examples/x/x.chunked.xg -------------------------------------------------------------------------------- /packages/web/src/ui/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PGV user interface. 3 | */ 4 | export * from "./ui" 5 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | Example graphs. Generated by pgv CLI. 2 | ========= 3 | 4 | DO NOT MODIFY BY HAND. 5 | -------------------------------------------------------------------------------- /archive/BME_230A_final_ppt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/archive/BME_230A_final_ppt.pdf -------------------------------------------------------------------------------- /packages/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.json" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /archive/BME_230A_final_paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w-gao/pgv/HEAD/archive/BME_230A_final_paper.pdf -------------------------------------------------------------------------------- /examples/x/regions.tsv: -------------------------------------------------------------------------------- 1 | ids 1 131 ./examples/x/chunk_0_ids_1_130.vg ./examples/x/chunk_0_ids_1_130.annotate.txt 2 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/tracks.scss: -------------------------------------------------------------------------------- 1 | .tracks { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /packages/web/src/repo/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PGV repository. 3 | */ 4 | export * from "./repo" 5 | export * from "./local" 6 | -------------------------------------------------------------------------------- /packages/web/src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PGV renderers. 3 | */ 4 | export * from "./renderer" 5 | export * from "./three" 6 | -------------------------------------------------------------------------------- /packages/web/src/layout/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Variation graph layout algorithms. 3 | */ 4 | export * from "./layout" 5 | export * from "./tubemap" 6 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./elements" 2 | export * from "./header" 3 | export * from "./tracks" 4 | export * from "./footer" 5 | -------------------------------------------------------------------------------- /packages/core/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "vitest" 2 | import { sum } from "@pgv/core" 3 | 4 | it("should work", () => { 5 | expect(sum(1, 4)).toEqual(5) 6 | }) 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "doubleQuote": true, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": false, 6 | "printWidth": 80, 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript's % is strange. This is not. 3 | */ 4 | export function mod(n: number, m: number): number { 5 | const remain = n % m 6 | return remain >= 0 ? remain : remain + m 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "moduleResolution": "node" 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the sum of a and b. 3 | * @param a 4 | * @param b 5 | * @returns 6 | */ 7 | export function sum(a: number, b: number): number { 8 | return a + b 9 | } 10 | 11 | export * from "./model" 12 | -------------------------------------------------------------------------------- /packages/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, splitVendorChunkPlugin } from "vite" 2 | import preact from "@preact/preset-vite" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [preact(), splitVendorChunkPlugin()], 7 | }) 8 | -------------------------------------------------------------------------------- /packages/web/src/utils/events.ts: -------------------------------------------------------------------------------- 1 | export type EventType = "Test" | "Test2" 2 | 3 | // interface EventListener {} 4 | 5 | // interface EventDispatcher { 6 | // addListener(ev: EventType, listener: EventListener): void 7 | // removeListener(ev: EventType, listener: EventListener): void 8 | 9 | // on(ev: EventType, ...args: any[]): void 10 | // } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | node_modules 10 | dist 11 | dist-ssr 12 | *.local 13 | coverage 14 | *.tsbuildinfo 15 | 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | vgtests 27 | -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Variation Graph Visualization 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | coverage 16 | *.tsbuildinfo 17 | 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | vgtests 29 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pgv/core", 3 | "version": "0.0.1", 4 | "author": "William Gao ", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "dev": "tsc --watch", 13 | "build": "tsc -b" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^4.9.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/web/src/layout/layout.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from "@pgv/core/src/model/vg" 2 | import { PGVGraph } from "@pgv/core/src/model/pgv" 3 | 4 | /** 5 | * An interface for the graph layout algorithm. 6 | */ 7 | export interface ILayout { 8 | name: string 9 | 10 | // Apply layout to the input graph. 11 | apply(g: Graph): PGVGraph 12 | 13 | // If applicable, reset layout. 14 | reset(): void 15 | } 16 | -------------------------------------------------------------------------------- /scripts/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8000 default_server; 3 | listen [::]:8000 default_server; 4 | 5 | root /pgv/ui; 6 | index index.html; 7 | 8 | server_name _; 9 | 10 | location / { 11 | try_files $uri $uri/ =404; 12 | 13 | # kill cache 14 | add_header Last-Modified $date_gmt; 15 | add_header Cache-Control 'no-store, no-cache'; 16 | if_modified_since off; 17 | expires off; 18 | etag off; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/model/pgv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Useful models for PGV. 3 | */ 4 | 5 | import { Node, Edge, Path } from "./vg" 6 | 7 | /** 8 | * Extension of vg structures to include coordinate info. 9 | */ 10 | export type PGVNode = Node & { 11 | x: number 12 | y: number 13 | width: number 14 | height: number 15 | } 16 | 17 | export type PGVEdge = Edge 18 | 19 | export type PGVGraph = { 20 | nodes: PGVNode[] 21 | edges: PGVEdge[] 22 | paths: Path[] 23 | } 24 | -------------------------------------------------------------------------------- /scripts/prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pull in the tube map source code directly - it is not published on npm but I 4 | # want it as a dependency. 5 | kTubemapPath="packages/web/src/lib/" 6 | curl -o $kTubemapPath/tubemap.js https://raw.githubusercontent.com/vgteam/sequenceTubeMap/dedf6cad8417e94acea671c9d52e03f3ebe4dfbf/src/util/tubemap.js 7 | 8 | # Inject code that I need to get the layout from the tube map. 9 | cat $kTubemapPath/inject.js >> $kTubemapPath/tubemap.js 10 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss", 3 | "rules": { 4 | "selector-class-pattern": null, 5 | "custom-property-empty-line-before": null, 6 | "declaration-empty-line-before": null, 7 | "scss/at-function-pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", 8 | "scss/at-mixin-pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", 9 | "scss/dollar-variable-pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", 10 | "scss/no-duplicate-dollar-variables": true, 11 | "scss/selector-no-redundant-nesting-selector": true, 12 | "scss/no-global-function-names": null 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "packages", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "target": "ESNext", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "composite": true, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true 18 | }, 19 | "references": [ 20 | { "path": "./packages/core" } 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/sources.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "identifier": "tiny", 4 | "name": "tiny example from vg", 5 | "xgFile": "tiny.xg", 6 | "region": null, 7 | "jsonFile": "tiny.xg.json" 8 | }, 9 | { 10 | "identifier": "x", 11 | "name": "x.vg.xg", 12 | "xgFile": "x.chunked.xg", 13 | "region": "1:100", 14 | "jsonFile": "x.chunked.xg.json" 15 | }, 16 | { 17 | "identifier": "K-3138", 18 | "name": "K-3138.xg", 19 | "xgFile": "K-3138.xg", 20 | "region": null, 21 | "jsonFile": "K-3138.json" 22 | } 23 | ] -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "prettier" 11 | ], 12 | "extends": [ 13 | "prettier" 14 | ], 15 | "rules": { 16 | "prettier/prettier": "warn", 17 | "@typescript-eslint/naming-convention": "off", 18 | "curly": "warn", 19 | "eqeqeq": "warn", 20 | "no-throw-literal": "warn" 21 | }, 22 | "ignorePatterns": [ 23 | "out", 24 | "dist", 25 | "**/*.d.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pgv/web", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "author": "William Gao ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "postbuild": "node ../../scripts/postbuild.js", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "@pgv/core": "0.0.1", 15 | "@preact/preset-vite": "^2.5.0", 16 | "@preact/signals-core": "^1.2.3", 17 | "@types/three": "^0.148.0", 18 | "d3": "^5.9.2", 19 | "d3-selection-multi": "^1.0.1", 20 | "preact": "^10.13.2", 21 | "sass": "^1.60.0", 22 | "three": "^0.148.0", 23 | "vite": "^4.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "jsxImportSource": "preact" 20 | }, 21 | "include": ["src"], 22 | "references": [ 23 | { "path": "../core" }, 24 | { "path": "./tsconfig.node.json" } 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 12px 0; 3 | background: #f6f6f6; 4 | text-align: center; 5 | font-size: 0.8rem; 6 | color: #9d9d9d; 7 | 8 | > span { 9 | &:not(:last-child)::after { 10 | content: '-'; 11 | margin: 0 20px; 12 | color: #929292; 13 | 14 | @media only screen and (max-width: 425px) { 15 | content: ''; 16 | margin: 0; 17 | 18 | & { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | } 23 | } 24 | } 25 | 26 | &__link { 27 | color: rgb(43, 101, 249); 28 | text-decoration: none; 29 | 30 | &:hover { 31 | color: rgb(45, 137, 229); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/tracks.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "preact/hooks" 2 | import { effect } from "@preact/signals-core" 3 | import { usePGV } from "../contexts" 4 | import "./tracks.scss" 5 | 6 | /** 7 | * The Tracks component of the UI. Contains a list of user-added HTML elements. 8 | */ 9 | export function Tracks() { 10 | const ref = useRef(null) 11 | 12 | // TODO: the tracks signal should support add, reorder, and remove operations. 13 | // It should probably also take in JSX components. 14 | 15 | const { tracksSignal } = usePGV() 16 | effect(() => { 17 | if (tracksSignal.value && ref.current) { 18 | ref.current.appendChild(tracksSignal.value) 19 | } 20 | }) 21 | 22 | return ( 23 |
24 | {/* Tracks will be populated here. */} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/web/src/main.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | color: #242424; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | #app { 14 | min-height: 500px; 15 | background: #fefefe; 16 | } 17 | 18 | // TODO: move this to its own component. 19 | 20 | // 24 | 25 | .alert { 26 | display: flex; 27 | justify-content: space-between; 28 | margin: 0; 29 | padding: 10px 20px; 30 | background: #ffb1b1; 31 | border: 1px rgb(255 137 10) solid; 32 | border-radius: 3px; 33 | } 34 | 35 | .alert__close { 36 | cursor: pointer; 37 | } 38 | -------------------------------------------------------------------------------- /packages/web/src/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import { PGVEdge, PGVNode } from "@pgv/core/src/model/pgv" 2 | import { Path } from "@pgv/core/src/model/vg" 3 | 4 | /** 5 | * An interface for the renderer. 6 | */ 7 | export interface IRenderer { 8 | /** 9 | * Draw a graph. 10 | * 11 | * @param nodes The nodes of the graph. 12 | * @param edges The edges of the graph. 13 | * @param refPaths If applicable, a collection of starting paths that 14 | * can be used to influence the graph layout. The 15 | * paths should not be drawn. 16 | */ 17 | drawGraph(nodes: PGVNode[], edges: PGVEdge[], refPaths?: Path[]): void 18 | 19 | /** 20 | * Draw paths. 21 | * 22 | * @param p The paths of the graph. 23 | */ 24 | drawPaths(p: Path[]): void 25 | 26 | /** 27 | * Clear all rendered graphs. 28 | */ 29 | clear(): void 30 | } 31 | -------------------------------------------------------------------------------- /packages/web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { PGV } from "./pgv" 2 | import "./main.scss" 3 | 4 | /** 5 | * Main entry. 6 | */ 7 | ;(function () { 8 | const root = document.querySelector("#app") 9 | if (root === null) { 10 | alert("cannot start app: missing container") 11 | return 12 | } 13 | 14 | const app = new PGV(root, { 15 | debug: true, 16 | repos: [ 17 | { 18 | type: "demo", 19 | id: "demo0", 20 | name: "local demo [examples]", 21 | config: { 22 | baseUrl: "./examples", 23 | }, 24 | }, 25 | // { 26 | // type: "api", 27 | // id: "demo1", 28 | // name: "local server", 29 | // }, 30 | ], 31 | layout: "tubemap", 32 | renderer: "three", 33 | }) 34 | 35 | console.log(app) 36 | })() 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | LABEL maintainer="William Gao " 3 | 4 | ARG VG_VERSION=v1.46.0 5 | 6 | ENV DEBIAN_FRONTEND noninteractive 7 | WORKDIR /pgv 8 | 9 | RUN apt-get update && apt-get install -y \ 10 | python3 \ 11 | nginx \ 12 | vim \ 13 | wget 14 | 15 | # Install Node.js 16 | # RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install -y gnupg nodejs 17 | # Install yarn 18 | # RUN npm install -g yarn 19 | 20 | # remove APT cache 21 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 22 | 23 | # Download vg 24 | RUN wget -O /usr/local/bin/vg "https://github.com/vgteam/vg/releases/download/${VG_VERSION}/vg" \ 25 | && chmod +x /usr/local/bin/vg 26 | 27 | 28 | EXPOSE 8000 29 | 30 | # Set up nginx 31 | COPY scripts/docker/nginx.conf /etc/nginx/sites-enabled/default 32 | COPY scripts/docker/start.sh start.sh 33 | 34 | # Copy over build files 35 | COPY ./packages/web/dist ./ui 36 | COPY cli.py ./cli.py 37 | 38 | # Configure pgv CLI to use the correct path 39 | ENV PGV_DEFAULT_PATH "/pgv/ui/examples" 40 | 41 | CMD ["./start.sh"] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 William Gao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pgv", 3 | "version": "0.1.0", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "description": "a web-based, interactive pangenome visualization tool", 9 | "repository": "git@github.com:w-gao/pgv.git", 10 | "author": "William Gao ", 11 | "license": "MIT", 12 | "scripts": { 13 | "web:dev": "yarn workspace @pgv/web dev", 14 | "web:build": "yarn workspace @pgv/web build", 15 | "web:preview": "yarn workspace @pgv/web preview --port 8000", 16 | "core:dev": "yarn workspace @pgv/core dev", 17 | "core:build": "yarn workspace @pgv/core build", 18 | "lint:ts": "eslint 'packages/**/{src,__tests__}/**/*.{js,jsx,ts,tsx}'", 19 | "lint:css": "stylelint --allow-empty-input 'packages/**/src/**/*.{css,scss}'", 20 | "test": "vitest", 21 | "coverage": "vitest run --coverage" 22 | }, 23 | "devDependencies": { 24 | "@typescript-eslint/eslint-plugin": "^5.48.2", 25 | "@typescript-eslint/parser": "^5.48.2", 26 | "@vitest/coverage-c8": "^0.28.1", 27 | "eslint": "^8.32.0", 28 | "eslint-config-prettier": "^8.6.0", 29 | "eslint-plugin-prettier": "^4.2.1", 30 | "prettier": "^2.8.3", 31 | "stylelint": "^15.4.0", 32 | "stylelint-config-standard-scss": "^7.0.1", 33 | "typescript": "^4.9.4", 34 | "vitest": "^0.27.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 8px; 3 | border: 1px #929292 solid; 4 | background: #f6f6f6; 5 | 6 | &__container { 7 | display: flex; 8 | flex-flow: row wrap; 9 | gap: 8px; 10 | } 11 | 12 | &__nav-container { 13 | display: flex; 14 | gap: 8px; 15 | 16 | > button { 17 | cursor: pointer; 18 | padding: 2px 5px; 19 | width: 45px; 20 | } 21 | } 22 | 23 | &__status-bar { 24 | display: flex; 25 | flex-flow: row wrap; 26 | font-size: 14px; 27 | margin-top: 8px; 28 | 29 | &__entry { 30 | > span { 31 | font-weight: bold; 32 | } 33 | 34 | &:not(:last-child)::after { 35 | content: '-'; 36 | margin: 0 8px; 37 | color: #929292; 38 | } 39 | } 40 | 41 | // On even smaller screens, it might be cleaner to display this vertically. 42 | @media only screen and (max-width: 425px) { 43 | flex-direction: column; 44 | 45 | &__entry { 46 | &:not(:last-child)::after { 47 | content: ''; 48 | margin: 0; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/web/src/config.ts: -------------------------------------------------------------------------------- 1 | export type RepoConfig = { 2 | type: "demo" | "api" 3 | id: string 4 | name: string 5 | config?: any 6 | } 7 | 8 | export type Config = { 9 | /** 10 | * Enable debug logging, etc. 11 | */ 12 | debug: boolean 13 | 14 | /** 15 | * Whether the app is embedded in another webpage or not. Set to true to 16 | * hide components like the footer. 17 | */ 18 | embedded: boolean 19 | 20 | /** 21 | * List of repository configurations. 22 | */ 23 | repos: RepoConfig[] 24 | 25 | /** 26 | * The layout engine. 27 | */ 28 | layout: "tubemap" 29 | 30 | /** 31 | * The renderer. 32 | */ 33 | renderer: "three" 34 | } 35 | 36 | export function setDefaultOptions(config?: Partial): Config { 37 | if (config === undefined) { 38 | config = {} 39 | } 40 | 41 | if (config.debug === undefined) { 42 | config.debug = false 43 | } 44 | 45 | if (config.embedded === undefined) { 46 | config.embedded = false 47 | } 48 | 49 | if (config.repos === undefined) { 50 | config.repos = [] 51 | } 52 | 53 | if (config.layout === undefined) { 54 | config.layout = "tubemap" 55 | } 56 | 57 | if (config.renderer === undefined) { 58 | config.renderer = "three" 59 | } 60 | 61 | return config as Config 62 | } 63 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/elements.scss: -------------------------------------------------------------------------------- 1 | // Used by Header component 2 | .form-group { 3 | display: flex; 4 | flex-direction: column; 5 | 6 | > select { 7 | width: 200px; 8 | height: 25px; 9 | border: 1px #929292 solid; 10 | border-radius: 3px; 11 | } 12 | } 13 | 14 | // ToolTip component 15 | .tooltip { 16 | display: inline-block; 17 | position: relative; 18 | 19 | &::after { 20 | display: inline-block; 21 | cursor: pointer; 22 | content: '?'; 23 | width: 20px; 24 | height: 20px; 25 | font-family: 'Courier New', Courier, monospace; 26 | font-weight: bold; 27 | background: #f0f0f0; 28 | text-decoration: none; 29 | text-align: center; 30 | border: 1px solid; 31 | border-radius: 50%; 32 | } 33 | 34 | > .content { 35 | display: none; 36 | position: absolute; 37 | 38 | // top: 50%; 39 | // left: 100%; 40 | // transform: translate(0, -50%); 41 | top: 30px; 42 | left: 50%; 43 | transform: translate(-50%, 0); 44 | z-index: 999999; 45 | padding: 10px; 46 | background-color: #f0f0f0; 47 | font-weight: normal; 48 | font-size: 14px; 49 | line-height: 14px; 50 | border-radius: 8px; 51 | box-sizing: border-box; 52 | box-shadow: 0 1px 5px rgb(0 0 0 / 50%); 53 | 54 | min-width: 300px; 55 | 56 | @media only screen and (max-width: 425px) { 57 | min-width: 200px; 58 | } 59 | } 60 | 61 | &:hover > .content { 62 | display: block; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/web/src/ui/contexts/application.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext } from "preact" 2 | import { useContext } from "preact/hooks" 3 | import { Signal } from "@preact/signals-core" 4 | import { Config } from "../../config" 5 | import { PGV } from "../../pgv" 6 | import { GraphDesc } from "../../repo" 7 | import { UI } from "../ui" 8 | 9 | /** 10 | * Signals to update the status bar. 11 | * 12 | * If the property is... 13 | * - a value, then the field is updated to the new value 14 | * - undefined, then the field is cleared 15 | */ 16 | export interface StatusBarUpdateSignals { 17 | nodes: Signal 18 | edges: Signal 19 | paths: Signal 20 | region: Signal 21 | selectedPath: Signal<[number, string] | undefined> 22 | selectedNode: Signal<[string, number, number] | undefined> 23 | } 24 | 25 | export interface AppState { 26 | app: PGV 27 | ui: UI 28 | config: Config 29 | 30 | // Headers 31 | graphsSignal: Signal 32 | statusBarSignals: StatusBarUpdateSignals 33 | 34 | // Tracks 35 | tracksSignal: Signal 36 | } 37 | 38 | const ApplicationContext = createContext({} as AppState) 39 | 40 | export function ApplicationProvider({ 41 | children, 42 | state, 43 | }: { 44 | children: ComponentChildren 45 | state: AppState 46 | }) { 47 | return ( 48 | 49 | {children} 50 | 51 | ) 52 | } 53 | 54 | export function usePGV() { 55 | return useContext(ApplicationContext) 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/model/vg.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | /** 4 | * Variation graph (vg) toolkit data structures. 5 | * 6 | * See: https://github.com/vgteam/libvgio/blob/master/deps/vg.proto 7 | */ 8 | 9 | /** 10 | * The unique ID of a node. This should be an int64 in vg, but becomes a string 11 | * in the JSON output. For our purpose, the type doesn't matter as long as it's 12 | * unique. 13 | */ 14 | export type NodeId = number | string 15 | 16 | // Represents a node that stores a sequence. 17 | export type Node = { 18 | id: NodeId 19 | name?: string 20 | sequence: string 21 | } 22 | 23 | // Represents an edge connecting two nodes. 24 | export type Edge = { 25 | from: NodeId 26 | to: NodeId 27 | 28 | // two flags to store the orientation of the edge. 29 | from_start?: boolean 30 | to_end?: boolean 31 | 32 | overlap?: number 33 | } 34 | 35 | // Represents a mapping in a path. 36 | export type Mapping = { 37 | position: { 38 | node_id: NodeId 39 | offset?: number 40 | is_reverse?: boolean 41 | } 42 | 43 | // A collection of edits. 44 | edit: { 45 | from_length: number 46 | to_length: number 47 | sequence?: string 48 | }[] 49 | 50 | rank: number | string 51 | } 52 | 53 | export type Path = { 54 | name: string 55 | mapping: Mapping[] 56 | freq?: number 57 | indexOfFirstBase?: number 58 | } 59 | 60 | export type Graph = { 61 | nodes: Node[] 62 | edges: Edge[] 63 | paths: Path[] 64 | } 65 | 66 | export type Read = { 67 | sequence: string 68 | path: Path 69 | score: number 70 | identity: number 71 | } 72 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "preact/hooks" 2 | import { usePGV } from "../contexts" 3 | import "./footer.scss" 4 | 5 | /** 6 | * footer component. 7 | */ 8 | export function Footer() { 9 | const { config } = usePGV() 10 | 11 | // Don't display the footer if our app is embedded. 12 | if (config.embedded) { 13 | return <> 14 | } 15 | 16 | // Assume the build process generated a "env.json" file at the root directory. 17 | const [env, setEnv] = useState() 18 | 19 | useEffect(() => { 20 | fetch("env.json") 21 | .then(resp => resp.json()) 22 | .then(resp => setEnv(resp)) 23 | .catch(() => { 24 | /** ignore */ 25 | }) 26 | }, []) 27 | 28 | let info 29 | if (env) { 30 | const build_date = env["BUILD_DATE"] || "N/a" 31 | const branch = env["BRANCH"] || "dev" 32 | const commit_ref = (env["COMMIT_REF"] as string) || "N/a" 33 | info = ( 34 | <> 35 | Current build: {branch}@{commit_ref.slice(0, 7)} ({build_date}) 36 | 37 | ) 38 | } else { 39 | info = <>unknown build 40 | } 41 | 42 | return ( 43 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: demo 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow one concurrent deployment 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v3 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | - name: Run prebuild script 31 | run: ./scripts/prebuild.sh 32 | - name: Build core 33 | run: yarn core:build 34 | - name: Build web 35 | run: yarn web:build --base /pgv 36 | - name: Upload production artifact 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: dist 40 | path: ./packages/web/dist 41 | deploy: 42 | needs: build 43 | runs-on: ubuntu-latest 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | steps: 48 | - name: Download artifact 49 | uses: actions/download-artifact@v3 50 | with: 51 | name: dist 52 | path: . 53 | - name: Inspect 54 | run: ls -al 55 | - name: Setup Pages 56 | uses: actions/configure-pages@v1 57 | - name: Upload artifact 58 | uses: actions/upload-pages-artifact@v1 59 | with: 60 | path: . 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v1 64 | -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable */ 3 | console.log("Running post build script...") 4 | 5 | const fs = require("fs") 6 | const path = require("path") 7 | const date = new Date() 8 | 9 | const rootDir = path.join(path.dirname(__filename), "..") 10 | let version = undefined 11 | try { 12 | version = "v" + require(path.join(rootDir, "package.json")).version 13 | } catch {} 14 | 15 | const filename = path.join(rootDir, "packages/web/dist", "env.json") 16 | data = JSON.stringify({ 17 | SITE_VERSION: version, 18 | BUILD_DATE: date.toLocaleDateString("en-US", {dateStyle: "medium", timeZone: "America/Los_Angeles"}), 19 | BUILD_TIME: date.toLocaleTimeString("en-US", {timeZone: "America/Los_Angeles"}), 20 | 21 | BRANCH: process.env.GITHUB_REF_NAME || process.env.BRANCH, 22 | COMMIT_REF: process.env.GITHUB_WORKFLOW_SHA || process.env.COMMIT_REF, 23 | 24 | // GitHub Actions 25 | GITHUB_WORKFLOW_SHA: process.env.GITHUB_WORKFLOW_SHA, 26 | GITHUB_REF: process.env.GITHUB_REF, 27 | GITHUB_REF_NAME: process.env.GITHUB_REF_NAME, 28 | GITHUB_JOB: process.env.GITHUB_JOB, 29 | GITHUB_REPOSITORY: process.env.GITHUB_REPOSITORY, 30 | GITHUB_REPOSITORY_OWNER: process.env.GITHUB_REPOSITORY_OWNER, 31 | GITHUB_ACTOR: process.env.GITHUB_ACTOR, 32 | GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME, 33 | GITHUB_RUN_ID: process.env.GITHUB_RUN_ID, 34 | 35 | // Netlify 36 | NETLIFY_BRANCH: process.env.BRANCH, 37 | NETLIFY_URL: process.env.URL, 38 | NETLIFY_COMMIT_REF: process.env.COMMIT_REF, 39 | NETLIFY_HEAD: process.env.HEAD, 40 | NETLIFY_CONTEXT: process.env.CONTEXT, 41 | NETLIFY_BUILD_ID: process.env.BUILD_ID, 42 | NETLIFY_PULL_REQUEST: process.env.PULL_REQUEST, 43 | NETLIFY_DEPLOY_ID: process.env.DEPLOY_ID, 44 | }) 45 | 46 | fs.writeFile(filename, data, function (err) { 47 | if (err) return console.log(err) 48 | 49 | console.log(data) 50 | console.log("=> " + filename) 51 | }) 52 | 53 | console.log("Complete!") 54 | -------------------------------------------------------------------------------- /packages/core/__tests__/model/index.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { parseGraph } from "../../src/model/index" 3 | import * as tinyJson from "./tiny.vg.json" 4 | 5 | describe("parseGraph", () => { 6 | it("should parse nodes for tiny.vg", () => { 7 | const graph = parseGraph(tinyJson) 8 | expect(graph.nodes).toEqual([ 9 | { id: 5, sequence: "C" }, 10 | { id: 7, sequence: "A" }, 11 | { id: 12, sequence: "ATAT" }, 12 | { id: 8, sequence: "G" }, 13 | { id: 1, sequence: "CAAATAAG" }, 14 | { id: 4, sequence: "T" }, 15 | { id: 6, sequence: "TTG" }, 16 | { id: 15, sequence: "CCAACTCTCTG" }, 17 | { id: 2, sequence: "A" }, 18 | { id: 10, sequence: "A" }, 19 | { id: 9, sequence: "AAATTTTCTGGAGTTCTAT" }, 20 | { id: 11, sequence: "T" }, 21 | { id: 13, sequence: "A" }, 22 | { id: 14, sequence: "T" }, 23 | { id: 3, sequence: "G" }, 24 | ]) 25 | }) 26 | 27 | it("should parse edges for tiny.vg", () => { 28 | const graph = parseGraph(tinyJson) 29 | expect(graph.edges).toEqual([ 30 | { from: 5, to: 6 }, 31 | { from: 7, to: 9 }, 32 | { from: 12, to: 13 }, 33 | { from: 12, to: 14 }, 34 | { from: 8, to: 9 }, 35 | { from: 1, to: 2 }, 36 | { from: 1, to: 3 }, 37 | { from: 4, to: 6 }, 38 | { from: 6, to: 7 }, 39 | { from: 6, to: 8 }, 40 | { from: 2, to: 4 }, 41 | { from: 2, to: 5 }, 42 | { from: 10, to: 12 }, 43 | { from: 9, to: 10 }, 44 | { from: 9, to: 11 }, 45 | { from: 11, to: 12 }, 46 | { from: 13, to: 15 }, 47 | { from: 14, to: 15 }, 48 | { from: 3, to: 4 }, 49 | { from: 3, to: 5 }, 50 | ]) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/web/src/repo/repo.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from "@pgv/core/src/model/vg" 2 | 3 | /** 4 | * A description for a graph. Contains name, identifier, and basic stats but 5 | * not the graph itself. 6 | */ 7 | export type GraphDesc = { 8 | name: string 9 | identifier: string 10 | 11 | region?: string 12 | 13 | // basic stats 14 | // numNodes?: number 15 | // numEdges?: number 16 | // numPaths?: number 17 | // length?: number 18 | } 19 | 20 | export type DownloadGraphConfig = { 21 | region: string 22 | // TODO: can also specify options for "vg index", "vg chunk", etc 23 | } 24 | 25 | /** 26 | * A repository interface to get graphs from. 27 | */ 28 | export interface IRepo { 29 | /** 30 | * A short description of the repo that is displayed on the UI. 31 | */ 32 | displayName: string 33 | 34 | /** 35 | * Whether or not this repo supports user to upload graph / haplotype files. 36 | */ 37 | supportsUpload: boolean 38 | 39 | /** 40 | * If applicable, connect to the repository and return a session ID. 41 | * 42 | * If the connection failed, null should be returned. 43 | */ 44 | connect(): Promise 45 | 46 | /** 47 | * Return a list of graph descriptions available in this repo. 48 | */ 49 | getGraphDescs(): Promise 50 | 51 | /** 52 | * Return the graph description of the given graph, or undefined if the 53 | * graph does not exist. 54 | * 55 | * @param identifier Unique identifier of the graph to retrieve. 56 | */ 57 | getGraphDesc(identifier: string): Promise 58 | 59 | /** 60 | * Given an identifier from getGraphDescs(), return the actual graph object. 61 | * 62 | * @param identifier Unique identifier of the graph to retrieve. 63 | * @param config Configuration used to retrieve the graph. 64 | */ 65 | downloadGraph( 66 | identifier: string 67 | // config: DownloadGraphConfig 68 | ): Promise 69 | } 70 | -------------------------------------------------------------------------------- /packages/web/src/layout/tubemap.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from "@pgv/core/src/model/vg" 2 | import { PGVGraph, PGVNode } from "@pgv/core/src/model" 3 | import { ILayout } from "." 4 | import { createLayout, vgExtractNodes, vgExtractTracks } from "../lib/tubemap" 5 | import { UI } from "../ui" 6 | 7 | /** 8 | * Use sequence-tube-map as the underlying layout structure. 9 | */ 10 | export class TubeMapLayout implements ILayout { 11 | name: string 12 | 13 | /** 14 | * @param ui Take in the UI to display the tube-map as a track. 15 | */ 16 | constructor(ui: UI) { 17 | this.name = "tubemap" 18 | 19 | const svgElement = document.createElementNS( 20 | "http://www.w3.org/2000/svg", 21 | "svg" 22 | ) 23 | svgElement.id = "tubeMapSVG" 24 | svgElement.setAttribute("style", "width: 100%;") 25 | ui.addTrack(svgElement) 26 | } 27 | 28 | apply(g: Graph): PGVGraph { 29 | // Slight diff in semantics between tubemap and pgv, but the format should be the same. 30 | // @ts-ignore 31 | g.node = g.nodes 32 | // @ts-ignore 33 | g.path = g.paths 34 | 35 | const nodes = vgExtractNodes(g) 36 | const tracks = vgExtractTracks(g) 37 | 38 | // We're not quite ready for rendering reads yet. 39 | // const gam = [] 40 | // const reads = vgExtractReads(nodes, tracks, gam) 41 | 42 | const layout = createLayout({ 43 | svgID: "#tubeMapSVG", 44 | nodes: nodes, 45 | tracks: tracks, 46 | region: [], 47 | })! 48 | 49 | const pgvNodes: PGVNode[] = [] 50 | 51 | for (let node of layout) { 52 | pgvNodes.push({ 53 | id: node.id, 54 | sequence: node.seq, 55 | x: node.x, 56 | y: node.y, 57 | width: node.width, 58 | height: node.height, 59 | }) 60 | } 61 | 62 | return { 63 | nodes: pgvNodes, 64 | edges: g.edges, 65 | paths: g.paths, 66 | } 67 | } 68 | 69 | reset(): void { 70 | const element = document.getElementById("tubeMapSVG") 71 | if (element) { 72 | element.innerHTML = "" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/web/src/repo/local.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from "@pgv/core/src/model/vg" 2 | import { parseGraph } from "@pgv/core/src/model" 3 | import { IRepo, GraphDesc } from "." 4 | 5 | /** 6 | * A local, static repo that stores some small, synthetic examples. 7 | */ 8 | export class ExampleDataRepo implements IRepo { 9 | displayName: string 10 | supportsUpload: boolean 11 | 12 | private descs: Map = new Map() 13 | private graphs: Map = new Map() 14 | 15 | private readonly baseUrl: string 16 | 17 | constructor(name: string, config: any) { 18 | this.displayName = name 19 | this.supportsUpload = false 20 | 21 | this.baseUrl = config && config["baseUrl"] ? config["baseUrl"] : "data" 22 | } 23 | 24 | async connect(): Promise { 25 | const data = await fetch(`${this.baseUrl}/sources.json`) 26 | const json = await data.json() 27 | 28 | for (let graph of json) { 29 | const identifier = graph["identifier"] 30 | const name = graph["name"] 31 | const jsonFile = graph["jsonFile"] 32 | const region = graph["region"] ?? undefined 33 | 34 | this.descs.set(identifier, { 35 | identifier: identifier, 36 | name: name, 37 | region: region, 38 | }) 39 | this.graphs.set( 40 | identifier, 41 | `${this.baseUrl}/${identifier}/${jsonFile}` 42 | ) 43 | } 44 | 45 | return "local" 46 | } 47 | 48 | async getGraphDescs(): Promise { 49 | return Array.from(this.descs.values()) 50 | } 51 | 52 | async getGraphDesc(identifier: string): Promise { 53 | return this.descs.get(identifier) 54 | } 55 | 56 | async downloadGraph( 57 | identifier: string 58 | // config: DownloadGraphConfig 59 | ): Promise { 60 | const url = this.graphs.get(identifier) 61 | 62 | if (url === undefined) { 63 | return Promise.reject() 64 | } 65 | 66 | // fetch from URL 67 | let data = await fetch(url) 68 | 69 | // convert to JSON 70 | let obj = await data.json() 71 | 72 | return parseGraph(obj) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/web/src/ui/components/elements.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "preact/hooks" 2 | import { ComponentChildren } from "preact" 3 | import "./elements.scss" 4 | 5 | // This file contains all the reusable UI elements of PGV. 6 | 7 | /** 8 | * select box component. 9 | */ 10 | export function FormSelect(props: { 11 | id: string 12 | text: string 13 | options: { id: string; name: string }[] 14 | defaultEmpty?: boolean 15 | onSelect?: (id: string) => void 16 | }) { 17 | const { id, text, options, defaultEmpty, onSelect } = props 18 | const selectRef = useRef(null) 19 | 20 | // When "options" change, reset selection. 21 | useEffect(() => { 22 | if (selectRef.current) { 23 | selectRef.current.selectedIndex = 0 24 | } 25 | }, [options]) 26 | 27 | // When user selects an option, invoke the callback. 28 | const onChange = () => { 29 | const select = selectRef.current 30 | if (select === null) { 31 | console.warn("ref to but no listener is defined" 41 | ) 42 | } 43 | } 44 | 45 | // If we select the first element by default, make sure to invoke the callback. 46 | if ( 47 | defaultEmpty === false && 48 | options.length > 0 && 49 | onSelect !== undefined 50 | ) { 51 | let id = options[0].id 52 | onSelect(id) 53 | } 54 | 55 | return ( 56 |
57 | 58 | 68 |
69 | ) 70 | } 71 | 72 | /** 73 | * A small UI element that shows the children as a tooltip message on hover. 74 | * 75 | * Useful for displaying help information. 76 | */ 77 | export function ToolTip(props: { children: ComponentChildren }) { 78 | return ( 79 |
80 |
{props.children}
81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/model/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { Edge, Graph, Mapping, Node, Path } from "./vg" 3 | 4 | export * from "./vg" 5 | export * from "./pgv" 6 | 7 | type Edit = { 8 | from_length: number 9 | to_length: number 10 | sequence?: string 11 | } 12 | 13 | /** 14 | * Parse the input into a typed vg graph. 15 | * 16 | * @param obj The raw graph object. 17 | */ 18 | export function parseGraph(obj: any): Graph { 19 | const nodes: Node[] = [] 20 | const edges: Edge[] = [] 21 | const paths: Path[] = [] 22 | 23 | const nodesObj = obj["node"] 24 | const edgesObj = obj["edge"] 25 | const pathsObj = obj["path"] 26 | 27 | // Nodes 28 | for (let nodeObj of nodesObj) { 29 | let node: Node = { 30 | id: nodeObj["id"], 31 | sequence: nodeObj["sequence"], 32 | } 33 | 34 | if (nodeObj["name"] !== undefined) { 35 | node.name = nodeObj["name"] 36 | } 37 | 38 | nodes.push(node) 39 | } 40 | 41 | // Edges 42 | for (let edgeObj of edgesObj) { 43 | let edge: Edge = { 44 | from: edgeObj["from"], 45 | to: edgeObj["to"], 46 | } 47 | 48 | if (edgeObj["from_start"] !== undefined) { 49 | edge.from_start = true 50 | } 51 | 52 | if (edgeObj["to_end"] !== undefined) { 53 | edge.to_end = true 54 | } 55 | 56 | edges.push(edge) 57 | } 58 | 59 | // Paths 60 | for (let pathObj of pathsObj) { 61 | let mappings: Mapping[] = [] 62 | 63 | for (let mappingObj of pathObj["mapping"]) { 64 | let edits = [] 65 | for (let editObj of mappingObj["edit"]) { 66 | let edit: Edit = { 67 | from_length: editObj["from_length"], 68 | to_length: editObj["to_length"], 69 | } 70 | 71 | if (editObj["sequence"] !== undefined) { 72 | edit.sequence = editObj["sequence"] 73 | } 74 | 75 | edits.push(edit) 76 | } 77 | 78 | let mapping: Mapping = { 79 | position: { 80 | node_id: mappingObj["position"]["node_id"], 81 | }, 82 | edit: edits, 83 | rank: mappingObj["rank"], 84 | } 85 | 86 | mappings.push(mapping) 87 | } 88 | 89 | let path: Path = { 90 | name: pathObj["name"], 91 | mapping: mappings, 92 | } 93 | 94 | if (pathObj["freq"] !== undefined) { 95 | path.freq = pathObj["freq"] 96 | } 97 | 98 | if (pathObj["indexOfFirstBase"] !== undefined) { 99 | path.indexOfFirstBase = pathObj["indexOfFirstBase"] 100 | } 101 | 102 | paths.push(path) 103 | } 104 | 105 | return { 106 | nodes: nodes, 107 | edges: edges, 108 | paths: paths, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /devnotes.md: -------------------------------------------------------------------------------- 1 | # dev notes 2 | 3 | Notes for development. 4 | 5 | 6 | ## MVP checklist 7 | 8 | - [X] visualization of the graph itself from the nodes and edges (no haplotypes) 9 | - [X] layout 10 | - [X] use tubemap as the layout engine 11 | - [X] nodes 12 | - [X] text for the sequence 13 | - [X] edges (curves) 14 | - [ ] improved curve calculation 15 | - [X] visualization of haplotypes on the z-axis (paths) 16 | - should be simple to do if we can visualize the graph 17 | - [X] controls 18 | - [X] better camera positioning 19 | - [X] move graph with left, right arrow keys (simulate window move) 20 | - [X] select haplotypes with up, down arrow keys 21 | - [ ] interactivity 22 | - [X] navigation buttons for mobile 23 | - [X] display number of nodes, edges, paths, selected path 24 | - [X] hover node should show coverage (% of paths that walks over this node) 25 | - [ ] demo graphs 26 | - [ ] find/generate better graphs to display different SVs 27 | - [ ] take screenshots for paper 28 | 29 | 30 | Backend 31 | 32 | - [ ] server (likely out of scope) 33 | - [ ] hook into tube-maps server 34 | - [ ] move window with arrow keys (actually dynamically download graph) 35 | 36 | 37 | ## vg notes 38 | 39 | ### Running a vg container 40 | 41 | ```console 42 | docker run -it --name pgv-vg \ 43 | -v "$(pwd)":/vg/pgv \ 44 | quay.io/vgteam/vg:latest \ 45 | bash 46 | ``` 47 | 48 | ```console 49 | docker start pgv-vg -i 50 | ``` 51 | 52 | ### Data flow 53 | 54 | ```console 55 | # Construct vg graph from a FASTA and VCF file (reference + variants). 56 | vg construct -r z.fa -v z.vcf.gz > z.vg 57 | 58 | 59 | # Get some basic stats on the graph. 60 | vg stats -z z.xg 61 | vg stats -N z.xg 62 | 63 | 64 | # Index the graph. 65 | vg index z.vg -x z.xg 66 | 67 | 68 | # Construct a subgraph given a region. 69 | vg chunk -x z.xg -g -c 20 -r 200 -T -b chunk -E regions.tsv > z.chunk.vg 70 | 71 | # Index and generate a DOT and JSON file for the subgraph. 72 | vg index z.chunk.vg -x z.chunk.xg 73 | vg view -dnp z.chunk.xg | dot -Tsvg -o z.chunk.svg 74 | 75 | vg view -j z.chunk.xg > z.chunk.json 76 | ``` 77 | 78 | 79 | ### Cactus example 80 | 81 | ```console 82 | vg chunk -x cactus.xg -c 20 -r 24:27 | vg view -j - | jq . > cactus_r24\:27_c20.json 83 | ``` 84 | 85 | 86 | ## Docker image 87 | 88 | ### Build the image 89 | 90 | ``` 91 | # Build project 92 | yarn core:build 93 | yarn web:build 94 | 95 | # Build image (with latest tag) 96 | docker build -t pgv . 97 | ``` 98 | 99 | ### Run the image locally 100 | 101 | ``` 102 | docker run -it --name pgv --rm -v "$(pwd)/examples":/pgv/ui/examples -p 8000:8000 pgv:latest 103 | ``` 104 | 105 | ### Publish to quay.io 106 | 107 | ``` 108 | # Login first 109 | docker login quay.io 110 | 111 | # Tag image 112 | docker image tag pgv quay.io/wlgao/pgv:latest 113 | 114 | # Push to quay.io 115 | docker image push quay.io/wlgao/pgv 116 | ``` 117 | 118 | 119 | ### Build and publish to Docker Hub (for multiple platforms) 120 | 121 | ``` 122 | # Login first 123 | docker login docker.io 124 | 125 | # Create a builder instance 126 | docker buildx create --use 127 | 128 | # Build and push 129 | docker buildx build --platform linux/amd64,linux/arm64 -t wlgao/pgv --push . 130 | ``` 131 | -------------------------------------------------------------------------------- /packages/web/src/pgv.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from "@pgv/core/src/model/vg" 2 | import { Config, setDefaultOptions } from "./config" 3 | import { ILayout, TubeMapLayout } from "./layout" 4 | import { IRenderer, ThreeRenderer } from "./renderer" 5 | import { GraphDesc, IRepo, ExampleDataRepo } from "./repo" 6 | import { UI } from "./ui" 7 | 8 | /** 9 | * Represents an instance of the PGV app. 10 | */ 11 | export class PGV { 12 | private readonly _config: Config 13 | 14 | /** 15 | * A list of available data sources. 16 | */ 17 | private readonly _repos: Map 18 | 19 | /** 20 | * The currently active repo. 21 | */ 22 | private _currentRepo?: IRepo 23 | 24 | public get currentRepo() { 25 | return this._currentRepo 26 | } 27 | 28 | private readonly _ui: UI 29 | private readonly _layout: ILayout 30 | private readonly _renderer: IRenderer 31 | 32 | constructor(root: HTMLElement, config?: Partial) { 33 | this._config = setDefaultOptions(config) 34 | 35 | this._repos = new Map() 36 | for (let repoConfig of this._config.repos) { 37 | let repo 38 | switch (repoConfig.type) { 39 | case "demo": 40 | default: 41 | repo = new ExampleDataRepo( 42 | repoConfig.name, 43 | repoConfig.config 44 | ) 45 | } 46 | this._repos.set(repoConfig.id, repo) 47 | } 48 | 49 | // The UI: preact-based component system. 50 | this._ui = new UI(root, this, this._config) 51 | 52 | // For now, config.layout === "tubemap" and config.renderer === "three". 53 | this._layout = new TubeMapLayout(this._ui) 54 | this._renderer = new ThreeRenderer(this._ui) 55 | 56 | // TODO: we ought to show spinner and hide UI when this is loading, but this is fairly quick at the moment. 57 | if (this._renderer instanceof ThreeRenderer) { 58 | this._renderer.initialize().then(() => { 59 | console.log("renderer loaded") 60 | }) 61 | } 62 | } 63 | 64 | /** 65 | * Switch the current repo. 66 | * 67 | * @param key The string identifier of the repo. 68 | */ 69 | async switchRepo(key: string): Promise { 70 | if (this._currentRepo !== undefined) { 71 | this._layout.reset() 72 | this._renderer.clear() 73 | // this.currentRepo.disconnect() 74 | } 75 | 76 | let repo = this._repos.get(key) 77 | if (!repo) { 78 | return Promise.reject("weird... repo is gone") 79 | } 80 | 81 | await repo.connect() 82 | this._currentRepo = repo 83 | return repo 84 | } 85 | 86 | render(desc: GraphDesc, graph: Graph) { 87 | // Clear whatever we might have. 88 | this._renderer.clear() 89 | 90 | // Show the selected region on the UI. 91 | this._ui.updateRegion(desc.region) 92 | 93 | // Apply the layout. 94 | const g = this._layout.apply(graph) 95 | 96 | // Render the graph. 97 | this._renderer.drawGraph(g.nodes, g.edges, undefined) 98 | 99 | // Draw the paths. 100 | this._renderer.drawPaths(g.paths) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/web/src/ui/ui.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact" 2 | import { batch, signal, Signal } from "@preact/signals-core" 3 | import { Config } from "../config" 4 | import { PGV } from "../pgv" 5 | import { GraphDesc } from "../repo" 6 | import { Header, Tracks, Footer } from "./components" 7 | import { ApplicationProvider, StatusBarUpdateSignals } from "./contexts" 8 | import "./style.css" 9 | 10 | /** 11 | * Represents a "track" that can be displayed in the UI. 12 | */ 13 | export interface Track { 14 | /** 15 | * Returns the DOM element that is injected into the UI. 16 | */ 17 | getElement(): Element 18 | 19 | /** 20 | * An event to update the view triggered by user (UI or keyboard). 21 | * 22 | * The track doesn't have to handle this event. 23 | */ 24 | updateRegion(region: string): void 25 | } 26 | 27 | /** 28 | * The user interface. 29 | * 30 | * Use the method calls to update what is shown on the UI. 31 | * 32 | * Register events to handle user input from the UI. (WIP) 33 | * 34 | * This _should_ be the only place that interfaces with the DOM. 35 | */ 36 | export class UI { 37 | private graphsSignal: Signal 38 | private statusBarSignals: StatusBarUpdateSignals 39 | private tracksSignal: Signal 40 | 41 | // TODO: the UI shouldn't take in the app; instead, the app should register events. 42 | constructor(root: HTMLElement, app: PGV, config: Config) { 43 | this.graphsSignal = signal([]) 44 | this.statusBarSignals = { 45 | nodes: signal(undefined), 46 | edges: signal(undefined), 47 | paths: signal(undefined), 48 | region: signal(undefined), 49 | selectedPath: signal(undefined), 50 | selectedNode: signal(undefined), 51 | } 52 | 53 | this.tracksSignal = signal(null) 54 | 55 | render( 56 | 66 |
67 | 68 |