├── ggshu ├── tests │ ├── __init__.py │ ├── test_ggshu.py │ ├── test_write.py │ ├── .ipynb_checkpoints │ │ └── conftest-checkpoint.py │ ├── conftest.py │ └── test_ggdata.py ├── schema.png ├── ggshu │ ├── __init__.py │ ├── aes.py │ ├── ggdata.py │ ├── geoms.py │ └── jupyter.py ├── pyproject.toml └── README.md ├── rust-toolchain.toml ├── .gitignore ├── assets ├── hover.png ├── met_grad.png ├── arrow_grad.png ├── hist_legend.png ├── rect_legend.png ├── hist_legend_right.png ├── fonts │ ├── FiraSans-Bold.ttf │ ├── Assistant-Regular.ttf │ ├── FiraMono-Medium.ttf │ ├── FiraMono-LICENSE │ └── Assistant-LICENSE └── flux_kcat.metabolism.json ├── docs ├── img │ ├── schema.png │ ├── favicon.png │ ├── geom_column.png │ ├── shu_coli_case.png │ ├── geom_dist_screen.png │ ├── shu_cauto_case.png │ ├── geom_arrow_screen.png │ └── geom_boxpoint_screen.png ├── assets │ ├── gallery.js │ └── gallery.css ├── gallery.md ├── file_formats.md ├── developers.md ├── index.md └── plotting.md ├── mkdocs.yml ├── .cargo └── config.toml ├── LICENSE-MIT ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── src ├── cli.rs ├── geom.rs ├── info.rs ├── tests.rs ├── main.rs ├── picking.rs ├── screenshot.rs ├── funcplot.rs ├── legend │ └── setup.rs └── gui.rs ├── Cargo.toml ├── README.md └── LICENSE-APACHE /ggshu/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *json 3 | __pycache__ 4 | *svg 5 | docs/build 6 | -------------------------------------------------------------------------------- /assets/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/hover.png -------------------------------------------------------------------------------- /ggshu/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/ggshu/schema.png -------------------------------------------------------------------------------- /assets/met_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/met_grad.png -------------------------------------------------------------------------------- /docs/img/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/schema.png -------------------------------------------------------------------------------- /assets/arrow_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/arrow_grad.png -------------------------------------------------------------------------------- /docs/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/favicon.png -------------------------------------------------------------------------------- /assets/hist_legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/hist_legend.png -------------------------------------------------------------------------------- /assets/rect_legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/rect_legend.png -------------------------------------------------------------------------------- /docs/img/geom_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/geom_column.png -------------------------------------------------------------------------------- /docs/img/shu_coli_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/shu_coli_case.png -------------------------------------------------------------------------------- /assets/hist_legend_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/hist_legend_right.png -------------------------------------------------------------------------------- /docs/img/geom_dist_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/geom_dist_screen.png -------------------------------------------------------------------------------- /docs/img/shu_cauto_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/shu_cauto_case.png -------------------------------------------------------------------------------- /assets/fonts/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/fonts/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /docs/img/geom_arrow_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/geom_arrow_screen.png -------------------------------------------------------------------------------- /assets/fonts/Assistant-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/fonts/Assistant-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/assets/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /docs/img/geom_boxpoint_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biosustain/shu/HEAD/docs/img/geom_boxpoint_screen.png -------------------------------------------------------------------------------- /ggshu/tests/test_ggshu.py: -------------------------------------------------------------------------------- 1 | from ggshu import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == "0.1.0" 6 | -------------------------------------------------------------------------------- /assets/flux_kcat.metabolism.json: -------------------------------------------------------------------------------- 1 | {"reactions": ["ENO", "GAPD", "PFK", "PGI", "PGK", "PGM", "PYK", "TPI"], "colors": [30.0, 10.0, 20.0, 200.0, 100.0, 50.0, 40.0, 120.0], "sizes": [0.2, 0.4, 0.1, 0.5, 0.6, 0.7, 0.3, 0.8]} -------------------------------------------------------------------------------- /ggshu/ggshu/__init__.py: -------------------------------------------------------------------------------- 1 | from .aes import aes 2 | from .geoms import GeomArrow as geom_arrow 3 | from .geoms import GeomHist as geom_hist 4 | from .geoms import GeomKde as geom_kde 5 | from .geoms import GeomMetabolite as geom_metabolite 6 | from .geoms import GeomBoxPoint as geom_boxpoint 7 | from .geoms import GeomColumn as geom_column 8 | from .ggdata import PlotData as ggmap 9 | from .jupyter import Shu 10 | 11 | __version__ = "0.1.0" 12 | -------------------------------------------------------------------------------- /ggshu/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ggshu" 3 | version = "0.2.1" 4 | description = "Grammar of Graphics for shu." 5 | readme = "README.md" 6 | authors = [{name="carrascomj", email="carrascomurielj@gmail.com"}] 7 | maintainers = [{name="carrascomj", email="carrascomurielj@gmail.com"}] 8 | requires-python = ">=3.10" 9 | dependencies = [ 10 | "pandas==2.*", 11 | "numpy", 12 | "IPython==8.18.1", 13 | "requests>=2.32.3", 14 | ] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3" 17 | ] 18 | [project.optional-dependencies] 19 | dev = ["pytest==7.2.0", "black==22.10.0", "isort==5.10.1"] 20 | 21 | [project.urls] 22 | Homepage = "https://biosustain.github.io/shu/" 23 | Documentation = "https://biosustain.github.io/shu/plotting.html" 24 | Repository = "https://github.com/biosustain/shu.git" 25 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Shu 2 | site_description: High-dimensional metabolic maps 3 | site_author: Jorge Carrasco Muriel 4 | site_url: https://biosustain.github.io/shu/docs 5 | repo_url: https://github.com/biosustain/shu 6 | repo_name: biosustain/shu 7 | 8 | nav: 9 | - Home: 'index.md' 10 | - Plotting tutorial: 'plotting.md' 11 | - Gallery: 'gallery.md' 12 | - Developers' guide: 'developers.md' 13 | - File formats: 'file_formats.md' 14 | 15 | theme: 16 | name: 'material' 17 | palette: 18 | primary: black 19 | accent: amber 20 | 21 | markdown_extensions: 22 | - pymdownx.highlight 23 | - pymdownx.superfences 24 | - admonition 25 | 26 | 27 | extra_css: 28 | - assets/gallery.css 29 | extra_javascript: 30 | - assets/gallery.js 31 | 32 | plugins: 33 | - search 34 | - mkdocs-gallery 35 | 36 | -------------------------------------------------------------------------------- /ggshu/README.md: -------------------------------------------------------------------------------- 1 | # ggshu 2 | 3 | > ⚠️ This is alpha software and very subject to change! 4 | 5 | A utility package to transform dataframes into [shu](https://github.com/biosustain/shu/) data. 6 | 7 | ![Shu grammar graphics schema](schema.png) 8 | 9 | ## Example 10 | 11 | ```python 12 | ( 13 | ggmap( 14 | df_cond, 15 | aes(reaction="r", color="flux", size="flux", condition="cond", y="kcat"), 16 | ) 17 | # plot flux to color and size of reactions 18 | + geom_arrow() 19 | # plot kcat as histogram shows on left side of reactions 20 | + geom_hist(side="left") 21 | # plot conc to color of metabolites 22 | + geom_metabolite(aes=aes(color="conc", metabolite="m")) 23 | # plot km as density plots shows on hover on metabolites 24 | + geom_kde(aes=aes(y="km"), mets=True) 25 | ).to_json("shu_data") 26 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below. 2 | 3 | # NOTE: For maximum performance, build using a nightly compiler 4 | # If you are using rust stable, remove the "-Zshare-generics=y" below. 5 | 6 | [target.x86_64-unknown-linux-gnu] 7 | linker = "clang" 8 | rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"] 9 | 10 | [target.x86_64-pc-windows-msvc] 11 | linker = "rust-lld.exe" 12 | rustflags = ["-Zshare-generics=n"] 13 | 14 | [target.wasm32-unknown-unknown] 15 | runner = "wasm-server-runner" 16 | rustflags = ["--cfg=web_sys_unstable_apis"] 17 | 18 | # Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' 19 | # In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. 20 | #[profile.dev] 21 | #debug = 1 22 | -------------------------------------------------------------------------------- /docs/assets/gallery.js: -------------------------------------------------------------------------------- 1 | /* Simple vanilla-JS lightbox for the gallery */ 2 | (function () { 3 | function initItem(item) { 4 | const overlay = item.querySelector('.overlay'); 5 | const close = overlay.querySelector('.close'); 6 | 7 | const open = () => { item.classList.add('active'); }; 8 | const closeO = () => { item.classList.remove('active'); }; 9 | 10 | item.addEventListener('click', open); 11 | item.addEventListener('keydown', e => { 12 | if ((e.key === 'Enter' || e.key === ' ') && !item.classList.contains('active')) { 13 | e.preventDefault(); open(); 14 | } 15 | }); 16 | close.addEventListener('click', e => { e.stopPropagation(); closeO(); }); 17 | overlay.addEventListener('click', closeO); 18 | document.addEventListener('keydown', e => { 19 | if (e.key === 'Escape' && item.classList.contains('active')) closeO(); 20 | }); 21 | } 22 | 23 | document.addEventListener('DOMContentLoaded', () => { 24 | document.querySelectorAll('.gallery-item').forEach(initItem); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /ggshu/ggshu/aes.py: -------------------------------------------------------------------------------- 1 | """Aesthetics, map from df variables to grammar variables.""" 2 | 3 | from typing import Dict, Optional 4 | 5 | Aesthetics = Dict[str, str] 6 | 7 | 8 | def aes( 9 | reaction: Optional[str] = None, 10 | metabolite: Optional[str] = None, 11 | condition: Optional[str] = None, 12 | y: Optional[str] = None, 13 | color: Optional[str] = None, 14 | size: Optional[str] = None, 15 | stack: Optional[str] = None, 16 | ymin: Optional[str] = None, 17 | ymax: Optional[str] = None, 18 | ) -> Aesthetics: 19 | """Map from dataframe variables to grammar graphics variables.""" 20 | # instead of using **kwargs, we specify the exact accepted aes 21 | # so that the users get notified by the LSP (or a runtime error) 22 | # if something is wrong 23 | aesthetics = { 24 | "reaction": reaction, 25 | "metabolite": metabolite, 26 | "condition": condition, 27 | "y": y, 28 | "ymin": ymin, 29 | "ymax": ymax, 30 | "color": color, 31 | "size": size, 32 | "stack": stack, 33 | } 34 | return {k: v for k, v in aesthetics.items() if v is not None} 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 The Novo Nordisk Foundation Center for Biosustainability 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/assets/gallery.css: -------------------------------------------------------------------------------- 1 | /* ==== GRID ========================================================= */ 2 | .gallery{ 3 | display:grid; 4 | grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); 5 | gap:1rem; 6 | margin-block:1.5rem; 7 | } 8 | .gallery-item{ 9 | position:relative; 10 | cursor:pointer; 11 | outline:3px solid transparent; 12 | border-radius:6px; 13 | } 14 | .gallery-item img{ 15 | width:100%; aspect-ratio:1/1; 16 | object-fit:cover; 17 | border-radius:inherit; 18 | transition:transform .2s linear, outline-color .2s linear; 19 | } 20 | .gallery-item:focus-visible img, 21 | .gallery-item:hover img{ transform:scale(1.04); } 22 | 23 | .gallery-item:focus-visible{ outline-color:var(--md-primary-fg-color,orange); } 24 | 25 | /* ==== OVERLAY ====================================================== */ 26 | .overlay{ 27 | position:fixed; inset:0; 28 | display:none; align-items:center; justify-content:center; 29 | background:rgba(0,0,0,.85); 30 | padding:2rem; 31 | z-index:1000; 32 | } 33 | .gallery-item.active .overlay{ display:flex; } 34 | 35 | .overlay .full{ 36 | max-width:45vw; max-height:80vh; 37 | box-shadow:0 4px 16px rgba(0,0,0,.4); 38 | margin-right:2rem; 39 | border-radius:6px; 40 | } 41 | .overlay pre{ 42 | max-height:80vh; overflow:auto; 43 | background:#eeeeee; color:#dcdcdc; 44 | padding:1rem; border-radius:6px; 45 | } 46 | /* .overlay .highlight{ */ 47 | /* max-height:80vh; */ 48 | /* overflow:auto; */ 49 | /* border-radius:6px; */ 50 | /* } */ 51 | .overlay .code-panel{ 52 | max-height:80vh; 53 | overflow:auto; 54 | background:#f9f9f6; /* gentle off-white */ 55 | border-radius:6px; 56 | padding:1rem; 57 | box-shadow:0 2px 6px rgba(0,0,0,.25); 58 | } 59 | 60 | .overlay .code-panel pre{ 61 | background:transparent; 62 | margin:0; 63 | } 64 | 65 | .close{ 66 | position:absolute; top:1rem; right:1rem; 67 | background:none; border:none; 68 | font-size:2.4rem; color:#fff; cursor:pointer; 69 | } 70 | -------------------------------------------------------------------------------- /ggshu/tests/test_write.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from ggshu import aes, geom_arrow, geom_kde, ggmap, geom_metabolite 4 | 5 | 6 | def test_writing_does_not_raise(df_cond): 7 | file_name = "some_tmp_data" 8 | ( 9 | ggmap( 10 | df_cond, 11 | aes(reaction="r", color="flux", size="flux", y="kcat", condition="cond"), 12 | ) 13 | + geom_arrow() 14 | + geom_metabolite(aes=aes(color="conc", metabolite="m")) 15 | + geom_kde(aes=aes(y="km"), mets=True) 16 | ).to_json(file_name) 17 | assert os.path.exists(file_name + ".metabolism.json") 18 | with open(file_name + ".metabolism.json") as f: 19 | data = json.load(f) 20 | data = list(data.keys()) 21 | assert all( 22 | a in data 23 | for a in [ 24 | "colors", 25 | "sizes", 26 | "conditions", 27 | "reactions", 28 | "metabolites", 29 | "kde_met_y", 30 | ] 31 | ), f"Should contain all aes: {data}" 32 | os.remove(file_name + ".metabolism.json") 33 | 34 | 35 | def test_writing_tmp(df_cond): 36 | file_name = "some_tmp_data" 37 | df_reac = df_cond[["r", "flux", "kcat", "cond"]] 38 | df_reac = df_reac[~df_reac.r.isna()] 39 | df_met = df_cond[["m", "conc", "km", "cond"]] 40 | df_met = df_met[~df_met.m.isna()] 41 | ( 42 | ( 43 | ggmap( 44 | df_reac, 45 | aes( 46 | reaction="r", color="flux", size="flux", y="kcat", condition="cond" 47 | ), 48 | ) 49 | + geom_arrow() 50 | ) 51 | / ( 52 | ggmap( 53 | df_met, 54 | aes(color="conc", metabolite="m", condition="cond"), 55 | ) 56 | + geom_metabolite() 57 | + geom_kde(aes=aes(y="km"), mets=True) 58 | ) 59 | ).to_json(file_name) 60 | assert os.path.exists(file_name + ".metabolism.json") 61 | os.remove(file_name + ".metabolism.json") 62 | -------------------------------------------------------------------------------- /ggshu/tests/.ipynb_checkpoints/conftest-checkpoint.py: -------------------------------------------------------------------------------- 1 | """Fixtures to aid tests.""" 2 | 3 | import pandas as pd 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def df(): 9 | """Provide dataframe for input.""" 10 | return pd.DataFrame( 11 | { 12 | "r": ["a", "a", "b", "b", "c", "c", None, None, None, None], 13 | "flux": [1, 2, 3, 4, 6, 6, None, None, None, None], 14 | "kcat": [2, 4, 6, 7, 9, 10, None, None, None, None], 15 | "conc": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 16 | "m": ["d", "e", "f", "g", "h", "d", "e", "f", "g", "h"], 17 | } 18 | ) 19 | 20 | 21 | @pytest.fixture 22 | def df_cond(): 23 | """Provide dataframe with conditions for input.""" 24 | return pd.DataFrame( 25 | { 26 | "r": [ 27 | "ACKr", 28 | "ACKr", 29 | "FTHFLi", 30 | "FTHFLi", 31 | "PTAr", 32 | "PTAr", 33 | None, 34 | None, 35 | None, 36 | None, 37 | None, 38 | None, 39 | None, 40 | None, 41 | ], 42 | "flux": [1, 2, 3, 4, 6, 6, None, None, None, None, None, None, None, None], 43 | "kcat": [2, 4, 6, 7, 9, 10, None, None, None, None, None, None, None, None], 44 | "km": [ 45 | None, 46 | None, 47 | None, 48 | None, 49 | None, 50 | None, 51 | None, 52 | None, 53 | None, 54 | None, 55 | 1, 56 | 2, 57 | 3, 58 | 4, 59 | ], 60 | "conc": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, None, None, None], 61 | "cond": ["x", "y", "x", "y", "x", "y", "x", "y", "x", "y", "", "", "", ""], 62 | "m": ["thf_c", "h2o_c", "glc_c", "methf_c", "accoa_c", "thf_c", "h2o_c", "glc_c", "methf_c", "accoa_c", "thf_c", "thf_c", "methf_c", "methf_c"], 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master, dev] 6 | pull_request: 7 | branches: [master, dev] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | # Run cargo test 14 | test: 15 | name: Test Suite 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | - name: Cache 21 | uses: actions/cache@v3 22 | with: 23 | path: | 24 | ~/.cargo/bin/ 25 | ~/.cargo/registry/index/ 26 | ~/.cargo/registry/cache/ 27 | ~/.cargo/git/db/ 28 | target/ 29 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 30 | - name: Install stable toolchain 31 | uses: dtolnay/rust-toolchain@nightly 32 | - name: Install Dependencies 33 | run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev 34 | - name: Run cargo test 35 | run: cargo test 36 | 37 | python-test: 38 | name: Python test suite 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v5 42 | - name: Set up Python 3.10 43 | uses: actions/setup-python@v6 44 | with: 45 | python-version: '3.10' 46 | - name: Install dependencies 47 | run: | 48 | cd ggshu 49 | python -m pip install ".[dev]" 50 | - name: Functional test 51 | run: | 52 | cd ggshu 53 | python -m pytest 54 | 55 | # Run cargo fmt --all -- --check 56 | format: 57 | name: Format 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Checkout sources 61 | uses: actions/checkout@v2 62 | - name: Install stable toolchain 63 | uses: actions-rs/toolchain@v1 64 | with: 65 | toolchain: stable 66 | profile: minimal 67 | components: rustfmt 68 | override: true 69 | - name: Run cargo fmt 70 | uses: actions-rs/cargo@v1 71 | with: 72 | command: fmt 73 | args: --all -- --check 74 | -------------------------------------------------------------------------------- /ggshu/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures to aid tests.""" 2 | 3 | import pandas as pd 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def df(): 9 | """Provide dataframe for input.""" 10 | return pd.DataFrame( 11 | { 12 | "r": ["a", "a", "b", "b", "c", "c", None, None, None, None], 13 | "iso": ["a1", "a1", "b1", "b2", "c1", "c2", None, None, None, None], 14 | "flux": [1, 2, 3, 4, 6, 6, None, None, None, None], 15 | "kcat": [2, 4, 6, 7, 9, 10, None, None, None, None], 16 | "conc": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 17 | "m": ["d", "e", "f", "g", "h", "d", "e", "f", "g", "h"], 18 | } 19 | ) 20 | 21 | 22 | @pytest.fixture 23 | def df_cond(): 24 | """Provide dataframe with conditions for input.""" 25 | return pd.DataFrame( 26 | { 27 | "r": [ 28 | "ACKr", 29 | "ACKr", 30 | "FTHFLi", 31 | "FTHFLi", 32 | "PTAr", 33 | "PTAr", 34 | None, 35 | None, 36 | None, 37 | None, 38 | None, 39 | None, 40 | None, 41 | None, 42 | ], 43 | "flux": [1, 2, 3, 4, 6, 6, None, None, None, None, None, None, None, None], 44 | "kcat": [2, 4, 6, 7, 9, 10, None, None, None, None, None, None, None, None], 45 | "km": [ 46 | None, 47 | None, 48 | None, 49 | None, 50 | None, 51 | None, 52 | None, 53 | None, 54 | None, 55 | None, 56 | 1, 57 | 2, 58 | 3, 59 | 4, 60 | ], 61 | "conc": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, None, None, None], 62 | "cond": ["x", "y", "x", "y", "x", "y", "x", "y", "x", "y", "", "", "", ""], 63 | "m": ["thf_c", "h2o_c", "glc_c", "methf_c", "accoa_c", "thf_c", "h2o_c", "glc_c", "methf_c", "accoa_c", "thf_c", "thf_c", "methf_c", "methf_c"], 64 | } 65 | ) 66 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Module that handles CLI to supply input files as arguments to the executable. 2 | use bevy::prelude::{App, Entity, FileDragAndDrop}; 3 | use bevy::window::PrimaryWindow; 4 | use std::env; 5 | use std::io; 6 | use std::path::PathBuf; 7 | use thiserror::Error; 8 | 9 | pub struct CliArgs { 10 | pub map_path: Option, 11 | pub data_path: Option, 12 | } 13 | 14 | #[derive(Error, Debug)] 15 | pub enum InitCliError { 16 | #[error("supplied path is invalid.")] 17 | InvalidPathError(#[from] io::Error), 18 | #[error("window not initialized.")] 19 | UninitWindow, 20 | } 21 | 22 | pub fn parse_args() -> CliArgs { 23 | let args: Vec = env::args().collect(); 24 | // the last args take priority 25 | let (map_path, data_path) = args.iter().skip(1).zip(args.iter().skip(2)).fold( 26 | (None, None), 27 | |(map, data), (arg, next)| match arg.as_str() { 28 | "--map" | "-m" => (Some(PathBuf::from(next)), data), 29 | "--data" | "-d" => (map, Some(PathBuf::from(next))), 30 | _ => (map, data), 31 | }, 32 | ); 33 | 34 | CliArgs { 35 | map_path, 36 | data_path, 37 | } 38 | } 39 | 40 | /// Generate `FileDragAndDrop` such that the map and/or data 41 | /// if supplied as CLI args are later loaded. 42 | pub fn handle_cli_args(app: &mut App, cli_args: CliArgs) -> Result<(), InitCliError> { 43 | let (win, _) = app 44 | .world_mut() 45 | .query::<(Entity, &PrimaryWindow)>() 46 | .iter(app.world()) 47 | .next() 48 | .ok_or(InitCliError::UninitWindow)?; 49 | // paths are canonicalized so that they are not interpreted 50 | // to be in the assets directory by bevy's `AssetLoader`. 51 | if let Some(map_path) = cli_args.map_path { 52 | app.world_mut().send_event(FileDragAndDrop::DroppedFile { 53 | window: win, 54 | path_buf: map_path.canonicalize()?, 55 | }); 56 | } 57 | 58 | if let Some(data_path) = cli_args.data_path { 59 | app.world_mut().send_event(FileDragAndDrop::DroppedFile { 60 | window: win, 61 | path_buf: data_path.canonicalize()?, 62 | }); 63 | } 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shu" 3 | version = "0.9.2" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "High-dimensional metabolic maps." 7 | categories = ["visualization", "science"] 8 | repository = "https://github.com/biosustain/shu" 9 | readme = "README.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | bevy = {version="0.15", features = ["multi_threaded", "bevy_render", "bevy_core_pipeline", "bevy_asset", "bevy_sprite", "bevy_winit", "png", "x11", "bevy_ui", "tga", "bmp", "jpeg", "webgl2"], default-features=false } 15 | bevy_egui = "0.31" 16 | bevy_pancam = { version = "0.16.0", features = ["bevy_egui"] } 17 | bevy_prototype_lyon = {git="https://github.com/Nilirad/bevy_prototype_lyon", rev = "d2dc33d"} 18 | colorgrad = "0.6.2" 19 | itertools = "0.13.0" 20 | fastrand = "2.1.0" 21 | serde = "1.0.208" 22 | serde_json = "1.0.125" 23 | proc-macro2 = "1.0.86" 24 | chrono = "0.4.38" 25 | roarsvg = "0.5.0" 26 | anyhow = "1.0.80" 27 | thiserror = "1.0.69" 28 | image = "0.25" 29 | 30 | # dependencies exclusive for wasm32 31 | [target.'cfg(target_arch = "wasm32")'.dependencies] 32 | wasm-bindgen = { version = "0.2.92", features = ["serde", "serde-serialize"] } 33 | console_error_panic_hook = "0.1" 34 | wasm-bindgen-futures = "0.4.33" 35 | async-std = "1.12.0" 36 | 37 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 38 | version = "0.3.4" 39 | features = [ 40 | 'CanvasRenderingContext2d', 41 | 'CssStyleDeclaration', 42 | 'Document', 43 | 'Element', 44 | 'EventTarget', 45 | 'File', 46 | 'FileList', 47 | 'HtmlLabelElement', 48 | 'HtmlInputElement', 49 | 'HtmlElement', 50 | 'MouseEvent', 51 | 'Node', 52 | 'Window', 53 | 'console', 54 | ] 55 | 56 | # Enable a small amount of optimization in debug mode 57 | [profile.dev] 58 | opt-level = 1 59 | 60 | # Enable high optimizations for dependencies (incl. Bevy), but not for our code: 61 | [profile.dev.package."*"] 62 | opt-level = 3 63 | 64 | [profile.wasm-release] 65 | # Use release profile as default values 66 | inherits = "release" 67 | 68 | # Optimize with size in mind, also try "s", sometimes it is better. 69 | # This doesn't increase compilation times compared to -O3, great improvements 70 | opt-level = "z" 71 | 72 | # Do a second optimization pass removing duplicate or unused code from dependencies. 73 | # Slows compile times, marginal improvements 74 | lto = "fat" 75 | 76 | # When building crates, optimize larger chunks at a time 77 | # Slows compile times, marginal improvements 78 | codegen-units = 1 79 | 80 | -------------------------------------------------------------------------------- /docs/gallery.md: -------------------------------------------------------------------------------- 1 | ![Arrow plot](img/geom_arrow_screen.png) 2 | 3 | ```python 4 | import pandas as pd 5 | from ggshu import aes, ggmap, geom_arrow 6 | 7 | df = pd.DataFrame({ 8 | "reactions": ["PFK", "ENO", "PYK", "GAPD", "PGI", "PGK", "PGM", "TPI"], 9 | "flux": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8], 10 | "kcat": [20, 30, 40, 10, 200, 100, 50, 120] 11 | }) 12 | 13 | ( 14 | ggmap(df, aes(reaction="reactions", size="flux", color="kcat")) 15 | + geom_arrow() 16 | ) 17 | ``` 18 | 19 | ![Box point plot](img/geom_boxpoint_screen.png) 20 | 21 | ```python 22 | import pandas as pd 23 | from ggshu import aes, ggmap, geom_kde, geom_boxpoint 24 | 25 | 26 | df = pd.DataFrame({ 27 | "reactions": ["PFK", "PFK", "PFK", "PFK", "PFK", "TPI", "TPI", "TPI", "TPI", "TPI"], 28 | "flux": [1.0, 2.0, 2.0, 2.0, 3.0, 5.0, 4.0, 3.0, 3.0, 3.0], 29 | "kcat": [20, 30, 40, 10, 200, 100, 50, 120, 120, 120] 30 | }) 31 | 32 | ( 33 | ggmap(df, aes(reaction="reactions", y="flux", color="kcat")) 34 | # distributions on the left side 35 | + geom_kde(side="left") 36 | # boxpoint (by default, on the right side) 37 | + geom_boxpoint() 38 | ) 39 | ``` 40 | 41 | ![Column plot](img/geom_column.png) 42 | 43 | ```python 44 | import pandas as pd 45 | from ggshu import aes, ggmap, geom_column 46 | 47 | 48 | df = pd.DataFrame({ 49 | "reactions": ["MTHFC", "MTHFC", "FTHFLi", "FTHFLi", "MTHFD", "MTHFD"], 50 | "conditions": ["x", "y", "x", "y", "x", "y"], 51 | "flux": [1.0, 2.0, 3.0, 4.0, 9.0, 5.0], 52 | # if ymin/ymax are missing, the error bar in that direction is not plotted 53 | "flux_min": [None, 1.0, 1.0, 1.0, 4.0, 4.0], 54 | "flux_max": [2.0, None, 4.0, 4.5, 10.0, None] 55 | }) 56 | 57 | plot = ( 58 | ggmap(df, aes(y="flux", ymax="flux_max", ymin="flux_min", reaction="reactions")) 59 | + geom_column() 60 | ) 61 | # save to JSON 62 | plot.to_json("columnexp") 63 | # alternatively, if in a jupyter notebook 64 | # this must be in one cell 65 | view = Shu(height=800) 66 | view.show(offline=True) 67 | # change the map in another cell 68 | # cauto map: https://github.com/biosustain/shu_case_studies/blob/master/maps/cauto_map.json 69 | with open("../../shu_case_studies/maps/cauto_map.json") as r: 70 | cauto_map = json.load(r) 71 | view.load_map(cauto_map) 72 | # display the data 73 | plot.show(view) 74 | ``` 75 | 76 | ![Publication case study 1](img/shu_coli_case.png) 77 | 78 | ```url 79 | **Check out** the [jupyter notebook](https://github.com/biosustain/shu_case_studies/blob/master/notebooks/1_shu_maud.ipynb). 80 | ``` 81 | 82 | ![Publication case study 2](img/shu_cauto_case.png) 83 | 84 | ```url 85 | **Check out** the [jupyter notebook](https://github.com/biosustain/shu_case_studies/blob/master/notebooks/2_shu_omics.ipynb). 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/file_formats.md: -------------------------------------------------------------------------------- 1 | # File Formats 2 | 3 | For a quick start, see the [map example](https://github.com/biosustain/shu/blob/master/assets/ecoli_core_map.json) (or any escher map at 4 | https://escher.github.io/) and the [data example](https://github.com/biosustain/shu/blob/master/assets/flux_kcat.metabolism.json). 5 | 6 | ## Map 7 | 8 | Shu uses the same format as escher for the maps. Maps from escher can be imported 9 | using the Map button (web app) or with drag and drop (native app). 10 | 11 | The only difference is that the histogram position, rotation and scale (which 12 | does not exist in escher) can be exported to the map (only native app for now) 13 | using the `Export` drop down on the `Settings` window. This allows to save a 14 | map with the correct manually fixed positions where different data can be 15 | plotted for the same or different projects. 16 | 17 | For the full JSON specification (ending with the extension _.json_), please refer 18 | to the source code represented by the `EscherMap` struct found at [the map source code](https://github.com/biosustain/shu/blob/master/src/escher.rs). 19 | 20 | ## Data 21 | 22 | The input data that contains the variables to be plotted in the map is a JSON ending withe _.metabolism.json_ extension. The full specification is the following struct (source code at [the data source code](https://github.com/biosustain/shu/blob/master/src/data.rs)): 23 | 24 | ```rust 25 | pub struct Data { 26 | /// Vector of reactions' identifiers 27 | reactions: Option>, 28 | // TODO: generalize this for any Data Type and use them (from escher.rs) 29 | /// Numeric values to plot as reaction arrow colors. 30 | colors: Option>, 31 | /// Numeric values to plot as reaction arrow sizes. 32 | sizes: Option>, 33 | /// Numeric values to plot as histogram. 34 | y: Option>>, 35 | /// Numeric values to plot as histogram. 36 | left_y: Option>>, 37 | /// Numeric values to plot on a hovered histogram popup. 38 | hover_y: Option>>, 39 | /// Numeric values to plot as KDE. 40 | kde_y: Option>>, 41 | /// Numeric values to plot as KDE. 42 | kde_left_y: Option>>, 43 | /// Numeric values to plot on a hovered KDE popup. 44 | kde_hover_y: Option>>, 45 | /// Numeric values to plot as boxpoint. 46 | box_y: Option>, 47 | /// Numeric values to plot as boxpoint. 48 | box_left_y: Option>, 49 | /// Vector of identifiers for horizontal ordering of the box boxpoints (right). 50 | box_variant: Option>, 51 | /// Vector of identifiers for horizontal ordering of the box boxpoints (left). 52 | box_left_variant: Option>, 53 | /// Numeric values to plot a column plot (right). 54 | column_y: Option>, 55 | column_ymin: Option>, 56 | column_ymax: Option>, 57 | /// Numeric values to plot a column plot (left). 58 | left_column_y: Option>, 59 | left_column_ymin: Option>, 60 | left_column_ymax: Option>, 61 | /// Categorical values to be associated with conditions. 62 | conditions: Option>, 63 | /// Categorical values to be associated with conditions. 64 | met_conditions: Option>, 65 | /// Vector of metabolites' identifiers 66 | metabolites: Option>, 67 | // TODO: generalize this for any Data Type and use them (from escher.rs) 68 | /// Numeric values to plot as metabolite circle colors. 69 | met_colors: Option>, 70 | /// Numeric values to plot as metabolite circle sizes. 71 | met_sizes: Option>, 72 | /// Numeric values to plot as histogram on hover. 73 | met_y: Option>>, 74 | /// Numeric values to plot as density on hover. 75 | kde_met_y: Option>>, 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /src/geom.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::Component; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// When in a Entity with `Aesthetics`, it will plot whatever aes to 5 | /// the arrows in the map. 6 | #[derive(Component)] 7 | pub struct GeomArrow {} 8 | 9 | /// Side of the arrow a plot (box point, histogram or legend) is referring to. 10 | #[derive(Hash, PartialEq, Eq, Debug, Clone, Deserialize, Serialize, Default, Component)] 11 | pub enum Side { 12 | Left, 13 | #[default] 14 | Right, 15 | /// for hovering instances 16 | Up, 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub enum HistPlot { 21 | Hist, 22 | Kde, 23 | // Point estimate. 24 | BoxPoint, 25 | } 26 | 27 | /// When in a Entity with `Aesthetics`, it will plot whatever aes to 28 | /// a histogram/KDE/box on the side of the arrows in the map. 29 | #[derive(Component, Clone, Debug)] 30 | pub struct GeomHist { 31 | pub side: Side, 32 | pub rendered: bool, 33 | pub mean: Option, 34 | 35 | pub in_axis: bool, 36 | pub plot: HistPlot, 37 | } 38 | 39 | impl GeomHist { 40 | pub fn left(plot: HistPlot) -> Self { 41 | Self { 42 | side: Side::Left, 43 | rendered: false, 44 | in_axis: false, 45 | mean: None, 46 | plot, 47 | } 48 | } 49 | pub fn right(plot: HistPlot) -> Self { 50 | Self { 51 | side: Side::Right, 52 | rendered: false, 53 | mean: None, 54 | in_axis: false, 55 | plot, 56 | } 57 | } 58 | pub fn up(plot: HistPlot) -> Self { 59 | Self { 60 | side: Side::Up, 61 | rendered: false, 62 | in_axis: false, 63 | mean: None, 64 | plot, 65 | } 66 | } 67 | } 68 | 69 | /// When in a Entity with `Aesthetics`, it will plot whatever aes to 70 | /// the circles in the map. 71 | #[derive(Component)] 72 | pub struct GeomMetabolite; 73 | 74 | /// Component applied to all Hist-like entities (spawned by a GeomKde, GeomHist, etc. aesthetic) 75 | /// This allow us to query for systems like normalize or drag. 76 | #[derive(Component)] 77 | pub struct HistTag { 78 | pub side: Side, 79 | pub node_id: u64, 80 | pub follow_scale: bool, 81 | } 82 | 83 | #[derive(Component)] 84 | pub struct VisCondition { 85 | pub condition: Option, 86 | } 87 | 88 | /// Component that indicates the plot position and axis. 89 | #[derive(Debug, Component)] 90 | pub struct Xaxis { 91 | pub id: String, 92 | pub arrow_size: f32, 93 | pub xlimits: (f32, f32), 94 | pub side: Side, 95 | pub node_id: u64, 96 | pub conditions: Vec, 97 | } 98 | 99 | /// Component that indicates a Y-categorical axis. 100 | /// Used to plot boxpoints next to each other, indicating 101 | /// isozymes for instance (x-axis is conditions). 102 | #[derive(Component)] 103 | pub struct YCategory { 104 | pub idx: Vec, 105 | pub tags: Vec>, 106 | } 107 | 108 | /// Component that marks something susceptible of being dragged/rotated. 109 | #[derive(Debug, Component, Default)] 110 | pub struct Drag { 111 | pub dragged: bool, 112 | pub rotating: bool, 113 | pub scaling: bool, 114 | } 115 | 116 | impl std::fmt::Display for Side { 117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 118 | write!( 119 | f, 120 | "{}", 121 | match self { 122 | Side::Right => "right", 123 | Side::Left => "left", 124 | Side::Up => "up", 125 | } 126 | ) 127 | } 128 | } 129 | 130 | impl std::fmt::Display for Xaxis { 131 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 132 | write!(f, "Xaxis = [ id = {}, side = {} ]", self.id, self.side) 133 | } 134 | } 135 | 136 | /// Component of all popups. 137 | #[derive(Component)] 138 | pub struct PopUp; 139 | 140 | /// Component of all popups. 141 | #[derive(Component, Debug)] 142 | pub struct AnyTag { 143 | pub id: u64, 144 | } 145 | 146 | /// Mark aesthetics as pertaining to mets. 147 | /// Used to filter removal queries. 148 | #[derive(Component, Clone)] 149 | pub struct AesFilter {} 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shu 2 | 3 | 4 | 5 | 6 | [![docs](https://img.shields.io/badge/docs-deployed-black?link=https%3A%2F%2Fbiosustain.github.io%2Fshu%2Fdocs%2F)](https://biosustain.github.io/shu/docs) 7 | [![wasm](https://img.shields.io/github/deployments/biosustain/shu/github-pages)](https://biosustain.github.io/shu) 8 | [![crates](https://img.shields.io/crates/v/shu.svg)](https://crates.io/crates/shu) 9 | 10 | ## What? 11 | 12 | App to plot multidimensional data to a metabolic map. Metabolic maps are graphs with metabolites 13 | as nodes and reactions as edges. 14 | 15 | For a **quick start**, we provide a web application at https://biosustain.github.io/shu. There 16 | are example maps and data available at https://github.com/biosustain/shu_case_studies, that 17 | can be load with the buttons at the top-right. 18 | 19 | For a more comprehensive guide, visit the [documentation](https://biosustain.github.io/shu/docs/plotting). 20 | 21 | > Paper to cite: https://doi.org/10.1093/bioinformatics/btae140 22 | 23 | ## Why? 24 | 25 | [Escher](https://escher.github.io/#/) is great. In fact, the format 26 | of the map is exactly the same as escher's. However, escher only allows for plotting 2 (+2 with tooltips) 27 | kinds of data: reaction data and metabolite data. **Shu** attempts to provide ways of plotting at least 28 | 6: 29 | 30 | - [x] Reaction sizes. 31 | - [x] Reaction colors. 32 | - [x] Reaction right sides. 33 | - [x] Reaction left sides. 34 | - [x] Metabolite sizes. 35 | - [x] Metabolite colors. 36 | 37 | (+2 with hovers): 38 | - [x] Hover reactions. 39 | - [x] Hover metabolites. 40 | 41 | with special focus on being able to plot **distributions** (not just points) and **n-conditions**. Escher also has the 42 | distinction between color and size, it is simply that they are not independently accessible from the GUI. 43 | 44 | ## How to use 45 | 46 | Shu is distributed both through a web app (preview build at https://biosustain.github.io/shu) and as standalone 47 | native application. To use the latter, download the [latest release for your operating system](https://github.com/biosustain/shu/releases/latest), unpack if necessary and run it as executable. 48 | 49 | Documentation for the rust crate and the python API will be made available once they are published. In the meantime, a preview 50 | of the python documentation can be found at https://biosustain.github.io/shu/docs. 51 | 52 | ### Building from source (latest master commit) 53 | 54 | Install [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) and run 55 | 56 | ```bash 57 | git clone https://github.com/biosustain/shu.git 58 | cd shu 59 | cargo install --path . 60 | ``` 61 | 62 | This may possibly require extra dependencies. Check the bevy setup, **shu** uses `lld` 63 | to fasten linking times. 64 | 65 | ## API design 66 | 67 | Shu follows a Grammar of Graphics design like [ggplot](https://ggplot2.tidyverse.org/) or [plotnine](https://plotnine.readthedocs.io/en/stable/index.html). 68 | See the [python API](ggshu/README.rst) for the full analogy. The particular implementation 69 | is an Entity Component System in [bevy](https://bevyengine.org/): 70 | 71 | * Each aesthetic is a *component* (`Gsize`, `Gcolor`, etc.) containing its data (see [`src/aesthetics.rs`](src/aesthetics.rs)). Identifiers are stored in the `Aesthetic` *component*. 72 | * *Entities* with `Aesthetic`, other aes components and Geom component (`GeomArrow`, `GeomMetabolite`, etc. in [`src/geom.rs`](src/geom.rs)) are 73 | processed and plotted by a *system* (in [`src/aesthetics.rs`](src/aesthetics.rs)). 74 | * The accepted aesthetics for a given geom are made explicit in the *queries* of the *systems*. 75 | 76 | Data handling (`df`, ad `map_file`) lives in [`src/data.rs`](src/data.rs) and 77 | [`src/escher.rs`](src/escher.rs) and the GUI componets lives in [`src/gui.rs`](src/gui.rs). 78 | 79 | ## License 80 | 81 | Copyright 2023 The Novo Nordisk Foundation Center for Biosustainability. 82 | 83 | Licensed under either of 84 | 85 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 86 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 87 | 88 | at your option. 89 | 90 | ### Contribution 91 | 92 | Unless you explicitly state otherwise, any contribution intentionally submitted 93 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 94 | be dual licensed as above, without any additional terms or conditions. 95 | -------------------------------------------------------------------------------- /assets/fonts/FiraMono-LICENSE: -------------------------------------------------------------------------------- 1 | Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /ggshu/tests/test_ggdata.py: -------------------------------------------------------------------------------- 1 | """Tests for highest level API.""" 2 | 3 | import pytest 4 | import numpy as np 5 | from ggshu import aes, geom_arrow, geom_kde, ggmap, geom_metabolite, geom_boxpoint 6 | 7 | 8 | def test_ggmap_can_be_built(df): 9 | _ = ( 10 | ggmap( 11 | df, aes(reaction="r", color="flux", size="flux", y="kcat", metabolite="m") 12 | ) 13 | + geom_arrow() 14 | + geom_kde(side="left") 15 | ) 16 | 17 | 18 | def test_plotting_dist_data_is_coerced(df): 19 | plotting_data = ( 20 | ggmap( 21 | df, aes(reaction="r", color="flux", size="flux", y="kcat", metabolite="m") 22 | ) 23 | + geom_arrow() 24 | + geom_metabolite(aes=aes(color="conc")) 25 | + geom_kde(side="left") 26 | ).plotting_data 27 | assert plotting_data["colors"].name, "flux" 28 | assert isinstance(plotting_data["colors"].to_list()[0], float) 29 | assert plotting_data["colors"].name, "conc" 30 | assert isinstance(plotting_data["met_colors"].to_list()[0], float) 31 | 32 | 33 | def test_plotting_data_has_expected_keys(df): 34 | plotting_data = ( 35 | ggmap( 36 | df, aes(reaction="r", color="flux", size="flux", y="kcat", metabolite="m") 37 | ) 38 | + geom_arrow() 39 | + geom_metabolite(aes=aes(color="conc")) 40 | + geom_kde(side="left") 41 | ).plotting_data 42 | assert [ 43 | key in plotting_data for key in ["kde_left_y", "colors", "sizes", "met_colors"] 44 | ] 45 | assert ( 46 | "met_sizes" not in plotting_data 47 | ), "Sizes should not have passed to metabolites" 48 | 49 | 50 | def test_plotting_metabolites_are_correctly_added_from_geoms(df): 51 | plotting_data = ( 52 | ggmap(df, aes(reaction="r", color="flux", size="flux", y="kcat")) 53 | + geom_arrow() 54 | + geom_metabolite(aes=aes(color="conc", metabolite="m")) 55 | + geom_boxpoint(side="left", aes=aes(stack="iso")) 56 | ).plotting_data 57 | assert [ 58 | key in plotting_data 59 | for key in [ 60 | "kde_left_y", 61 | "colors", 62 | "sizes", 63 | "met_colors", 64 | "metabolites", 65 | "reactions", 66 | ] 67 | ] 68 | assert ( 69 | "met_sizes" not in plotting_data 70 | ), "Sizes should not have passed to metabolites" 71 | 72 | 73 | def test_passing_conditions_to_geoms_raises(df_cond): 74 | with pytest.raises(AssertionError): 75 | _ = ( 76 | ggmap(df_cond, aes(reaction="r", color="flux", size="flux", y="kcat")) 77 | + geom_arrow() 78 | + geom_metabolite(aes=aes(color="conc", metabolite="m", condition="flux")) 79 | ) 80 | 81 | 82 | def test_mixed_conditions_one_dataframe_works(df_cond): 83 | plotting_data = ( 84 | ggmap( 85 | df_cond, 86 | aes(reaction="r", color="flux", size="flux", y="kcat", condition="cond"), 87 | ) 88 | + geom_arrow() 89 | + geom_metabolite(aes=aes(color="conc", metabolite="m")) 90 | + geom_kde(aes=aes(y="km"), mets=True) 91 | ).plotting_data 92 | assert len(plotting_data["conditions"]) == 6 93 | assert len(plotting_data["met_conditions"]) == 14 94 | assert len(plotting_data["kde_met_y"]) == 12 95 | assert ( 96 | plotting_data["kde_met_y"].apply(lambda x: len(x) == 1 and np.isnan(x[0])) 97 | ).sum() == 10 98 | assert plotting_data["colors"].name, "flux" 99 | 100 | 101 | def test_mixed_conditions_two_dataframe_works(df_cond): 102 | df_reac = df_cond[["r", "flux", "kcat", "cond"]] 103 | df_reac = df_reac[~df_reac.r.isna()] 104 | df_met = df_cond[["m", "conc", "km", "cond"]] 105 | df_met = df_met[~df_met.m.isna()] 106 | plotting_data = (( 107 | ggmap( 108 | df_reac, 109 | aes(reaction="r", color="flux", size="flux", y="kcat", condition="cond"), 110 | ) 111 | + geom_arrow() 112 | ) / ( 113 | ggmap( 114 | df_met, 115 | aes(color="conc", metabolite="m", condition="cond"), 116 | ) 117 | + geom_metabolite() 118 | + geom_kde(aes=aes(y="km"), mets=True) 119 | )).plotting_data 120 | assert len(plotting_data["conditions"]) == 6 121 | assert len(plotting_data["met_conditions"]) == 12 122 | assert len(plotting_data["kde_met_y"]) == 12 123 | assert ( 124 | plotting_data["kde_met_y"].apply(lambda x: len(x) == 1 and np.isnan(x[0])) 125 | ).sum() == 10 126 | assert plotting_data["colors"].name, "flux" 127 | assert isinstance(plotting_data["colors"].to_list()[0], float) 128 | assert isinstance(plotting_data["colors"].to_list()[0], float) 129 | -------------------------------------------------------------------------------- /assets/fonts/Assistant-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Assistant Project Authors, with Reserved Font Name 'Source'. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | //! Information to show in the UI. 2 | use crate::funcplot::{lerp, IgnoreSave}; 3 | use bevy::color::palettes::css::DARK_GRAY; 4 | use bevy::color::Srgba; 5 | use std::time::Duration; 6 | 7 | use bevy::prelude::*; 8 | 9 | pub struct InfoPlugin; 10 | impl Plugin for InfoPlugin { 11 | fn build(&self, app: &mut App) { 12 | let app = app 13 | .insert_resource(Info { 14 | msg: None, 15 | timer: Timer::new(Duration::from_secs(3), TimerMode::Once), 16 | }) 17 | .add_systems(Update, (pop_infobox, display_information)); 18 | 19 | // display the info messages in different positions for native and WASM 20 | #[cfg(not(target_arch = "wasm32"))] 21 | app.add_systems( 22 | Startup, 23 | |asset_server: Res, commands: Commands| { 24 | spawn_info_box(asset_server, commands, 2.0, 1.0) 25 | }, 26 | ); 27 | 28 | #[cfg(target_arch = "wasm32")] 29 | app.add_systems( 30 | Startup, 31 | |asset_server: Res, commands: Commands| { 32 | spawn_info_box(asset_server, commands, 6.5, 0.5) 33 | }, 34 | ); 35 | } 36 | } 37 | 38 | #[derive(Resource)] 39 | /// Information about IO. 40 | pub struct Info { 41 | msg: Option<&'static str>, 42 | timer: Timer, 43 | } 44 | 45 | impl Info { 46 | /// Sends a message to be logged in the CLI and displayed in the GUI. 47 | pub fn notify(&mut self, msg: &'static str) { 48 | info!(msg); 49 | self.msg = Some(msg); 50 | self.timer.reset(); 51 | } 52 | pub fn close(&mut self) { 53 | self.msg = None; 54 | } 55 | pub fn displaying(&self) -> bool { 56 | self.msg.is_some() 57 | } 58 | } 59 | 60 | #[derive(Component)] 61 | pub struct InfoBox; 62 | 63 | /// Spawn the UI components to show I/O feedback to the user. 64 | /// The top argument is the top of the screen in percent to allow for different 65 | /// positioning on WASM (would collide with the buttons otherwise). 66 | fn spawn_info_box(asset_server: Res, mut commands: Commands, top: f32, right: f32) { 67 | let font = asset_server.load("fonts/Assistant-Regular.ttf"); 68 | commands 69 | .spawn(( 70 | Node { 71 | position_type: PositionType::Absolute, 72 | right: Val::Percent(right), 73 | top: Val::Percent(top), 74 | padding: UiRect { 75 | right: Val::Px(8.), 76 | left: Val::Px(8.), 77 | top: Val::Px(3.), 78 | bottom: Val::Px(3.), 79 | }, 80 | ..Default::default() 81 | }, 82 | bevy::ui::FocusPolicy::Block, 83 | GlobalZIndex(10), 84 | BackgroundColor(Color::Srgba(DARK_GRAY)), 85 | )) 86 | .insert(InfoBox) 87 | .insert(Interaction::default()) 88 | .with_children(|p| { 89 | p.spawn(( 90 | Text(String::new()), 91 | bevy::ui::FocusPolicy::Block, 92 | GlobalZIndex(12), 93 | IgnoreSave, 94 | TextFont::from_font(font).with_font_size(20.), 95 | TextColor(Color::Srgba(Srgba::hex("F49596").unwrap())), 96 | )); 97 | }); 98 | } 99 | 100 | /// Show information about I/O in a popup. 101 | fn display_information( 102 | mut writer: TextUiWriter, 103 | info_state: Res, 104 | mut info_query: Query<&Children, With>, 105 | ) { 106 | for child in info_query.single_mut().iter() { 107 | let msg = info_state.msg.unwrap_or_default(); 108 | *writer.text(*child, 0) = msg.to_string(); 109 | } 110 | } 111 | 112 | /// Popup-like mouse interactions for the infobox. 113 | fn pop_infobox( 114 | time: Res