|
import io |
|
from pathlib import Path |
|
from typing import Callable, Optional, cast |
|
from urllib.parse import parse_qsl |
|
|
|
import altair as alt |
|
import polars as pl |
|
import reacton.core |
|
import solara |
|
import solara.lab |
|
from cmap import Catalog, Colormap |
|
from ipyvuetify.extra import FileInput |
|
|
|
from make_link import encode_url |
|
from viewer import AxisProperties, ColorTransform, ProteinView, RoutedView |
|
|
|
DEFAULT_CMAP = "tol:rainbow_PuRd" |
|
NORM_CATEGORIES = ["linear", "diverging", "categorical"] |
|
VMIN_DEFAULT = 0.0 |
|
VMAX_DEFAULT = 1.0 |
|
HIGHLIGHT_COLOR = "#e933f8" |
|
MISSING_DATA_COLOR = "#8c8c8c" |
|
CMAP_OPTIONS = list(Catalog().namespaced_keys()) |
|
|
|
|
|
BASE_URL = "https://huggingface.co/spaces/Jhsmit/ipymolstar-annotate-colors" |
|
|
|
pth = Path(__file__).parent |
|
|
|
|
|
@solara.component |
|
def FileInputComponent( |
|
on_file: Callable[[solara.components.file_drop.FileInfo | None], None], |
|
): |
|
"""Adaptation of _FileDrop.""" |
|
|
|
file_info, set_file_info = solara.use_state(None) |
|
wired_files, set_wired_files = solara.use_state( |
|
cast(Optional[list[solara.components.file_drop.FileInfo]], None) |
|
) |
|
|
|
file_drop = FileInput.element(on_file_info=set_file_info, multiple=False) |
|
|
|
def wire_files(): |
|
if not file_info: |
|
set_wired_files([]) |
|
return |
|
|
|
real = cast(FileInput, solara.get_widget(file_drop)) |
|
|
|
|
|
real.version += 1 |
|
real.reset_stats() |
|
|
|
set_wired_files( |
|
cast(list[solara.components.file_drop.FileInfo], real.get_files()) |
|
) |
|
|
|
solara.use_effect(wire_files, [file_info]) |
|
|
|
def handle_file(): |
|
if not wired_files: |
|
on_file(None) |
|
return |
|
if on_file: |
|
f = wired_files[0].copy() |
|
f["data"] = None |
|
on_file(f) |
|
|
|
solara.lab.use_task(handle_file, dependencies=[wired_files]) |
|
|
|
return file_drop |
|
|
|
|
|
@solara.component |
|
def ColorPickerMenuButton(title: str, color: solara.Reactive[str]): |
|
local_color = solara.use_reactive(color.value) |
|
|
|
def on_open(value: bool): |
|
if not value: |
|
color.set(local_color.value) |
|
|
|
btn = solara.Button(title, color=local_color.value) |
|
with solara.lab.Menu( |
|
activator=btn, close_on_content_click=False, on_open_value=on_open |
|
): |
|
solara.v.ColorPicker( |
|
v_model=local_color.value, |
|
on_v_model=local_color.set, |
|
) |
|
|
|
|
|
empty_frame = pl.DataFrame() |
|
R_DEFAULT = "" |
|
V_DEFAULT = "" |
|
|
|
|
|
|
|
|
|
@solara.component |
|
def MainApp(): |
|
dark_effective = solara.lab.use_dark_effective() |
|
title = solara.use_reactive("My annotated protein view") |
|
description = solara.use_reactive("") |
|
molecule_id = solara.use_reactive("1QYN") |
|
data = solara.use_reactive(empty_frame) |
|
warning_text = solara.use_reactive("") |
|
|
|
residue_column = solara.use_reactive(R_DEFAULT) |
|
color_column = solara.use_reactive(V_DEFAULT) |
|
|
|
label = solara.use_reactive("value") |
|
unit = solara.use_reactive("au") |
|
|
|
highlight_color = solara.use_reactive(HIGHLIGHT_COLOR) |
|
missing_data_color = solara.use_reactive(MISSING_DATA_COLOR) |
|
autoscale_y = solara.use_reactive(True) |
|
|
|
cmap_name = solara.use_reactive(DEFAULT_CMAP) |
|
reverse = solara.use_reactive(False) |
|
full_cmap_name = cmap_name.value + "_r" if reverse.value else cmap_name.value |
|
cmap = Colormap(full_cmap_name) |
|
|
|
vmin = solara.use_reactive(VMIN_DEFAULT) |
|
vmax = solara.use_reactive(VMAX_DEFAULT) |
|
norm_type = solara.use_reactive(NORM_CATEGORIES[0]) |
|
|
|
rc = reacton.core.get_render_context() |
|
|
|
def on_file(file_info: solara.components.file_drop.FileInfo | None): |
|
if not file_info: |
|
data.set(pl.DataFrame()) |
|
return |
|
|
|
try: |
|
df = pl.read_csv(file_info["file_obj"]) |
|
except Exception as e: |
|
warning_text.set(str(e)) |
|
return |
|
if len(df.columns) < 2: |
|
warning_text.set(f"Expected at least 2 columns, got {len(df.columns)}") |
|
data.set(pl.DataFrame()) |
|
return |
|
|
|
warning_text.set("") |
|
|
|
|
|
|
|
|
|
with rc: |
|
residue_column.set(df.columns[0]) |
|
color_column.set(df.columns[1]) |
|
data.set(df) |
|
|
|
colors = ColorTransform( |
|
name=full_cmap_name, |
|
norm_type=norm_type.value, |
|
vmin=vmin.value, |
|
vmax=vmax.value, |
|
missing_data_color=missing_data_color.value, |
|
highlight_color=highlight_color.value, |
|
) |
|
|
|
axis_properties = AxisProperties( |
|
label=label.value, |
|
unit=unit.value, |
|
autoscale_y=autoscale_y.value, |
|
) |
|
|
|
if data.value.is_empty(): |
|
data_view = pl.DataFrame({"residue_number": [], "value": []}) |
|
else: |
|
data_view = pl.DataFrame( |
|
{ |
|
"residue_number": data.value[residue_column.value], |
|
"value": data.value[color_column.value], |
|
} |
|
) |
|
|
|
def load_example_data(): |
|
bio = io.BytesIO(Path("example_data.csv").read_bytes()) |
|
bio.seek(0) |
|
file_info = solara.components.file_drop.FileInfo( |
|
name="example_data.csv", |
|
size=Path("example_data.csv").stat().st_size, |
|
file_obj=bio, |
|
data=None, |
|
) |
|
|
|
|
|
|
|
with rc: |
|
molecule_id.set("6GOX") |
|
title.set("SecA HDX-MS protein local flexibility") |
|
on_file(file_info) |
|
vmin.set(4e4) |
|
vmax.set(1e4) |
|
autoscale_y.set(False) |
|
description.set(Path("example_data_description.md").read_text()) |
|
|
|
with solara.AppBar(): |
|
with solara.Tooltip("Load example data and settings"): |
|
solara.Button( |
|
icon_name="mdi-test-tube", |
|
icon=True, |
|
on_click=load_example_data, |
|
) |
|
|
|
query_string = encode_url( |
|
title=title.value, |
|
molecule_id=molecule_id.value, |
|
colors=colors, |
|
axis_properties=axis_properties, |
|
data=data_view, |
|
description=description.value, |
|
) |
|
|
|
ProteinView( |
|
title.value, |
|
molecule_id=molecule_id.value, |
|
data=data_view, |
|
colors=colors, |
|
axis_properties=axis_properties, |
|
dark_effective=dark_effective, |
|
description=description.value, |
|
) |
|
|
|
with solara.Sidebar(): |
|
with solara.Card("Settings"): |
|
solara.InputText(label="Title", value=title) |
|
solara.InputText(label="PDB ID", value=molecule_id) |
|
solara.Text("Choose .csv data file:") |
|
FileInputComponent(on_file) |
|
|
|
if warning_text.value: |
|
solara.Warning(warning_text.value) |
|
|
|
if not data.value.is_empty(): |
|
with solara.Row(): |
|
solara.Select( |
|
label="Residue Column", |
|
value=residue_column, |
|
values=list(data.value.columns), |
|
) |
|
solara.Select( |
|
label="Color Column", |
|
value=color_column, |
|
values=list(data.value.columns), |
|
) |
|
|
|
with solara.Row(): |
|
solara.InputText(label="Label", value=label) |
|
solara.InputText(label="Unit", value=unit) |
|
|
|
solara.Text("Colors") |
|
with solara.Row(gap="10px", justify="space-around"): |
|
ColorPickerMenuButton("Highlight", highlight_color) |
|
ColorPickerMenuButton("Missing data", missing_data_color) |
|
|
|
def set_cmap_name(name: str): |
|
try: |
|
Colormap(name) |
|
cmap_name.set(name) |
|
except TypeError: |
|
pass |
|
|
|
|
|
solara.v.Autocomplete( |
|
v_model=cmap_name.value, |
|
on_v_model=set_cmap_name, |
|
items=CMAP_OPTIONS, |
|
) |
|
|
|
solara.Select( |
|
label="Normalization type", value=norm_type, values=NORM_CATEGORIES |
|
) |
|
|
|
with solara.Row(): |
|
|
|
def set_vmin(value: float): |
|
if norm_type.value == "diverging": |
|
vmin.set(value) |
|
vmax.set(-value) |
|
else: |
|
vmin.set(value) |
|
|
|
solara.InputFloat( |
|
label="vmin", |
|
value=vmin.value, |
|
on_value=set_vmin, |
|
disabled=norm_type.value == "categorical", |
|
) |
|
solara.InputFloat( |
|
label="vmax", |
|
value=vmax, |
|
disabled=norm_type.value in ["diverging", "categorical"], |
|
) |
|
with solara.GridFixed(columns=2): |
|
with solara.Tooltip("Reverses the color map"): |
|
solara.Checkbox(label="Reverse", value=reverse) |
|
|
|
with solara.Tooltip( |
|
"Uncheck to use color range as the scatterplot y scale" |
|
): |
|
solara.Checkbox(label="Autoscale Y", value=autoscale_y) |
|
|
|
solara.Image(cmap._repr_png_(height=24), width="100%") |
|
solara.InputTextArea( |
|
label="Description", value=description, continuous_update=True |
|
) |
|
solara.Div(style={"height": "10px"}) |
|
|
|
solara.Button( |
|
label="Open view in new tab", |
|
attributes={"href": BASE_URL + "?" + query_string, "target": "_blank"}, |
|
block=True, |
|
) |
|
|
|
|
|
@solara.component |
|
def Page(): |
|
route = solara.use_router() |
|
solara.Style(Path("style.css")) |
|
|
|
dark_effective = solara.lab.use_dark_effective() |
|
dark_previous = solara.use_previous(dark_effective) |
|
if dark_previous != dark_effective: |
|
if dark_effective: |
|
alt.themes.enable("dark") |
|
else: |
|
alt.themes.enable("default") |
|
|
|
|
|
if route.search: |
|
query_dict = {k: v for k, v in parse_qsl(route.search)} |
|
|
|
if "molecule_id" in query_dict: |
|
RoutedView() |
|
|
|
else: |
|
MainApp() |
|
|
|
|
|
|
|
|