├── 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