GUI UI Elements (#816)

This commit is contained in:
Charlotte Weaver
2019-06-27 17:30:57 -07:00
committed by GitHub
parent 5effe4bbbb
commit 8afb22a017
15 changed files with 819 additions and 95 deletions
+7
View File
@@ -130,3 +130,10 @@ uninstall :
pip uninstall -y cellxgene || :
.PHONY : install install-dev install-release-test install-release uninstall
# GUI
build-assets :
pyside2-rcc server/gui/cellxgene.qrc -o server/gui/cellxgene_rc.py
.PHONY : build-assets
+67 -53
View File
@@ -1,4 +1,5 @@
import errno
import functools
import logging
from os import devnull
from os.path import splitext, basename, getsize
@@ -13,22 +14,56 @@ from server.app.util.errors import ScanpyFileError
from server.app.util.utils import custom_format_warning
from server.utils.utils import find_available_port, is_port_available
# anything bigger than this will generate a special message
BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
BIG_FILE_SIZE_THRESHOLD = 100 * 2 ** 20 # 100MB
def common_args(func):
"""
Decorator to contain CLI args that will be common to both CLI and GUI: title and engine args.
"""
@click.option("--title", "-t", help="Title to display (if omitted will use file name).")
@click.option(
"--layout",
"-l",
default=[],
multiple=True,
show_default=True,
help="Layout name, eg, 'umap'."
)
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
@click.option(
"--max-category-items",
default=1000,
metavar="",
show_default=True,
help="Categories with more distinct values than this will not be displayed.",
)
@click.option(
"--diffexp-lfc-cutoff",
default=0.01,
show_default=True,
help="Relative expression cutoff used when selecting top N differentially expressed genes",
)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def parse_engine_args(layout, obs_names, var_names, max_category_items, diffexp_lfc_cutoff):
return {
"layout": layout,
"max_category_items": max_category_items,
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
"obs_names": obs_names,
"var_names": var_names,
}
@click.command()
@click.argument("data", metavar="<data file>", type=click.Path(exists=True, file_okay=True, dir_okay=False))
@click.option(
"--layout",
"-l",
default=[],
multiple=True,
show_default=True,
help="Layout name, eg, 'umap'."
)
@click.option("--title", "-t", help="Title to display (if omitted will use file name).", metavar="")
@click.option(
"--verbose",
"-v",
@@ -49,22 +84,7 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
)
@click.option("--port", "-p", help="Port to run server on, if not specified cellxgene will find an available port.",
metavar="", show_default=True)
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
@click.option("--host", default="127.0.0.1", help="Host IP address")
@click.option(
"--max-category-items",
default=1000,
metavar="",
show_default=True,
help="Categories with more distinct values than this will not be displayed.",
)
@click.option(
"--diffexp-lfc-cutoff",
default=0.01,
show_default=True,
help="Relative expression cutoff used when selecting top N differentially expressed genes",
)
@click.option(
"--scripts",
default=[],
@@ -72,20 +92,21 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
help="Additional script files to include in html page",
show_default=True,
)
@common_args
def launch(
data,
layout,
title,
verbose,
debug,
obs_names,
var_names,
open_browser,
port,
host,
layout,
obs_names,
var_names,
max_category_items,
diffexp_lfc_cutoff,
scripts,
title,
scripts
):
"""Launch the cellxgene data viewer.
This web app lets you explore single-cell expression data.
@@ -98,6 +119,7 @@ def launch(
> cellxgene launch <your data file> --title <your title>"""
e_args = parse_engine_args(layout, obs_names, var_names, max_category_items, diffexp_lfc_cutoff)
# Startup message
click.echo("[cellxgene] Starting the CLI...")
@@ -112,22 +134,22 @@ def launch(
else:
warnings.formatwarning = custom_format_warning
if scripts:
click.echo(r"""
/ / /\ \ \__ _ _ __ _ __ (_)_ __ __ _
\ \/ \/ / _` | '__| '_ \| | '_ \ / _` |
\ /\ / (_| | | | | | | | | | | (_| |
\/ \/ \__,_|_| |_| |_|_|_| |_|\__, |
|___/
The --scripts flag is intended for developers to include google analytics etc. You could be opening yourself to a
security risk by including the --scripts flag. Make sure you trust the scripts that you are including.
""")
scripts_pretty = ", ".join(scripts)
click.confirm(f"Are you sure you want to inject these scripts: {scripts_pretty}?", abort=True)
if not verbose:
sys.tracebacklimit = 0
if scripts:
click.echo(r"""
/ / /\ \ \__ _ _ __ _ __ (_)_ __ __ _
\ \/ \/ / _` | '__| '_ \| | '_ \ / _` |
\ /\ / (_| | | | | | | | | | | (_| |
\/ \/ \__,_|_| |_| |_|_|_| |_|\__, |
|___/
The --scripts flag is intended for developers to include google analytics etc. You could be opening yourself to a
security risk by including the --scripts flag. Make sure you trust the scripts that you are including.
""")
scripts_pretty = ", ".join(scripts)
click.confirm(f"Are you sure you want to inject these scripts: {scripts_pretty}?", abort=True)
if not title:
file_parts = splitext(basename(data))
title = file_parts[0]
@@ -170,16 +192,8 @@ security risk by including the --scripts flag. Make sure you trust the scripts t
mpl.use("TkAgg")
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
args = {
"layout": layout,
"max_category_items": max_category_items,
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
"obs_names": obs_names,
"var_names": var_names,
}
try:
server.attach_data(ScanpyEngine(data, args), title=title)
server.attach_data(ScanpyEngine(data, e_args), title=title)
except ScanpyFileError as e:
raise click.ClickException(f"{e}")
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="logo.png">images/cellxgene_logo.png</file>
<file alias="collapsed.svg">images/properties_contract.svg</file>
<file alias="expanded.svg">images/properties_expand.svg</file>
<file alias="icon.png">images/properties_expand.svg</file>
</qresource>
</RCC>
+440
View File
@@ -0,0 +1,440 @@
# -*- coding: utf-8 -*-
# Resource object code
#
# Created: Wed Jun 19 15:02:04 2019
# by: The Resource Compiler for PySide2 (Qt v5.12.3)
#
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore
qt_resource_data = b"\
\x00\x00\x0d\xde\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x01;\x00\x00\x00j\x08\x06\x00\x00\x00\xc3y\xf6!\
\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\
\x01\x00\x9a\x9c\x18\x00\x00\x00\x01sRGB\x00\xae\xce\
\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfc\
a\x05\x00\x00\x0dsIDATx\x01\xed\xdd\x7fn\
\x1b\xc7\x15\x07\xf07C\xdbQ\x93\x00f\xfe\x88e7\
\x05L\x9d J/PJ9@\xe4^@\x12\x90H\
(\x0aT\xce\x09,\x9d\xc0\xf2\x1fE \xb9\x80\xd9\x0b\
\xd4\xf2\x01,2'\xb0|\x023@\x0bG\xea\x1fQ\
\xff\x09\x04\xd1;\xaf\xef-\xc9X\xa48\xb3?\xb8\xfc\
e~?\x80b\x85\xbb\xab%g\xdf\xbe\x9d\x9d\x99\x1d\
\x12\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\xc0\x9c24\x22\xa7\xab\x87L3l\xf1xk\
de\x03\x00\xe3g\x09\x00`\x0e \xd9\x01\xc0\x5c@\
\xb2\x03\x80\xb9\x80d\x07\x00s\x01\xc9\x0e\x00\xe6\xc2\x0d\
\x02\x18\xe0m\xf5i\xd5Z\xae\xfb\x96KW{\xed\xee\
\xf1\xd6f\xef6?T\xac-\xbd!\xbf\xa6\xf4r/\
\x11\xc0\x04\xccD\xb2\xbb\xf3\xf2;\x1a\x85\xb3\xaf\x9f\x12\
\x00\xcc\x07\xdc\xc6\x02\xc0\x5c@\xb2\x03\x80\xb9\x80d\x07\
\x00s\x01\xc9\x0e\x00\xe6\x02\x92\x1d\x00\xcc\x05$;\x00\
\x98\x0b31\xf4\x04CD\x00`X\xa8\xd9\x01\xc0\x5c\
\xc0\x13\x14c\xf0\xa6\xfa\xac\xfcI\xa9\xb5\xee\x98\x96\xad\
\xa1\x0a3W\x88\xcc\xb91t.\xbf\xcb\xbf\xa6\x11E\
\xd1\x8b{\x8d\xbf4i\x08\xfa\xd4\x831\xbc&?\xf7\
\xe5\xefW\x88\xb8,\x7f\xbb\xa9\xcb\x9c\xa3\xd7\xcc\xe6\xe8\
^\xe3\xbb\x06\xc1o\xbaef-}\xa9\xc7E\xcbk\
\xd019\xad\x1e\xac9k\xca\x83\xfeF\xc9\xb5N\xee\
4\xfez\x92f\x7f\x1a\x0b\xb7JQ\xb5\xe4\x5c\xb5\xff\
8\xe9~\x1d\xd1kr\xb6\x91\xf68\xbd]=\xdc\xf0\
.tQ\xe3jL\xf5\xc6\x07-wc0b\xfeQ\
\x02\xa46L\xfc%\xc5^dm\xe3\x8b\x97\xdf\x1e\xd1\
\x04a\xf2N\x8f\x22&\xef\x8c\x1f\x9f2v\x97\x8cY\
O\xb3\xbe\x04\x9e\x9e`\x9bY\x82NO\x9eO\xed\xbb\
\x1d&~(\x05^N\xb1I\xd3\x92\xa9}~\xfc\xdd\
^h\xa5\x0f\xfdq\xb1\xff|}\xb0v\x83\xe9q\xfb\
\xc4\xf4\x93\xb2\xda\xd5\xb2\x92x~Eq\x82\xb8\xce\x11\
\xef\xdd;\xde\xde\x0d\xfd\x9d<\xc7\x89$v\x16_~\
\xfb\xcf\xd0J\xa1\xf3L\xf6\xf5\xfd\xdd\xe3\xed\xfd\xf6\xb1\
t\xcf\x92?+\xef\x7f~\xbc\xfd=e\xf0\xdf\xd5\xa7\
\x8f\x8a\x8e\xbdQ\xc1m\xec\x88\x9c}}\xb8S\xb2\xa5\
Wi\x13\x9db\xa6\xaa&\x0b\x0d\xa04\xebkr\xf9\
\xd8^\xbe\x92\x93m7e\xb0\xa9\x8a\xae/'\xc9\x1b\
\xdd\x9e\xa6\x88&\x04\xca)\xcb\xb6g\xabO\x1f\xdf`\
\xf3<\xe9\xe4WZVg\xab\x87\xcf\x0cS\xe2\xba>\
\x9al>\xb1\xad7Y\x8f\x13\xb1\xab\xe9\xbe\xf3\x96\x8b\
\xeeK\xe3\xb0}\xd12\x95\xa4\xf5\x1d\x99\x87\xb2~\x9d\
R\xd0\xd8\xd1\x0b\xc0,\xc5\xdeDncC\xcf\xba\x0e\
\xea\x8c\x98\xb5gcO\xf5j\xc7\xbcK9i@H\
\xc2\xa3\xd0\x15\xf0\xac\xfa\xf7e\xb2\xa5z\x86@\xeb\xa7\
\xb5\xb0\xba\x04\xdd\xca\xb0\xb7\xcfE\xd0\x93\x92\xb9\xb5{\
V=\x5c\xb9\xd3\xd8JuK\xd8\xd5\xaeQ^\xd6%\
14\xee\xf4\xd56\xfb\x9dvj\x22\x94\x81\x94\xf1F\
\xde{\xa0\xd3\xea?\xd6\xc9\xbaZ\xde\xdb\x1c\xdd\xf7\xc7\
\xb6\xb5,\x09oe\xa9\xb1y\x9ee[\xa9E\xads\
\xc6$\xad\x17\xdc\x9fW\x0f\x1ej\x8d\xd0\xb7N\xbb\xbc\
\xad&\xc5\x0a\xe53\x91\xd8C\xcd\xae`oW\x7f\xd8\
\x90\x90\xd9\xa5!i\xc2\x93`\xa8\x0e\xdc\x87\x04\x1b\xdb\
\x1b\xcf\x87Ht]\x1at\xcfi\xc24!\xc8I\xa6\
'W\x99-\xd5%\xe1-\xa7\xdd\xf6\xfd\x89g*\x9a\
\x18B\xb5\xe2\xa2\x8eM\xfa\xf7\xf6\xb4\xaa\x89\x8e\x86\xb7\
\xfc\xa9\xbdLU\xdb\xefS\xa1\x1c\x0c\x19\x7f\x19^)\
o\x1a\x8e\xc6\xde\xf10\xb5\xf9\xac\x90\xec\x0a\x14\x07\x02\
\xd9<A9P\xa9T\x1a\xf8\xb7\xe2v@2\x15*\
\xc6\xf2\xe9\xea\xc1.MH\xbb\x86\xda\x93\x10R'\xbc\
A'\x9e^$\xb4=n\xd0\xfaE\x1e\x9b4\xda\xed\
d\xc5\xd0[L\xdf\xc5o\x04\xca\xbe}\x15\x1c{K\
\xd2\x0c\x93\xa9\x96=\x0c$\xbb\x22Y\xaa\x86\x02A\xda\
}\xce\xb51\x9b\x1c?\xd0\x1f\xc7\x1cl|\xd6[\x8a\
\xfe\xa0\x8bk\x0b\x09\xed\x80\xb2]\xc3Q\xb4\xe9\x5c\xb4\
\x92f?\xf2\xcev\xc6y\x85\xbdJ{1\xe32\xe9\
\x95\x98\xf0\xbc5\x0c\xf9\xac_\xbc\xdc\xbe\xd6\xeb\xd7\xae\
\xd5\xf5\xad\xdb\xe7\xea\xf1\xd1\xb2\x8b\x7fgnR\x0ei\
\xf6w\xf58\xc9\xcf\xa6\xdc\xb3\x06o\xdf}\x17\xbf$\
\xda\x99\x94-\x1e\xe4\x9d[{\xad\xec\xe3v\xb6\xa46\
h\xf9\x0c\xd2\xd1\xf60\xed\xbe\x98\xcc\xdf\xc6\x15{\x18\
zR\xa0\xc4\x9a\x03\xb7V\xee\xd5{\x86(\x1c\xbd]\
=\xd0\x1e*\xefvrBW\xe5\x9f\xc6o\xffo\xdc\
F\xb8\x13\x9d\x8f\xee\xd6\xb7\x1f\xf4\xbd\x98\xb4\x9f\xf2\x82\
mim\xa8F\x13\xa0=\x99\xf2\xfe\xa8\xef\xfdu\x13\
\xde\xb56\xbcP\xa2[\xacoo\x0c\xdaG\x89K\xeb\
\x1c,6jF\x1c\xc9\xf1\xe9iCj\xc8\xbejr\
\x5c\xebr\x92W(\x83\xe4Z$\xef\xc9q\xda\xed{\
\xb1\xf6\xf3\xca\xe1\xbe$\x8b\x9d\x81[\xb4/~\x95,\
\xed\x5c\xce\x91\xf4\xeeo\xd5\xfa^>\x92\xda\xfcm)\
\xbf5\xff\x96\xe6\xcb\xfeW\xda\xb5\xba\x10>Z\xcc\x18\
{rH\xca\xbf\xb3\x97\x1b\xf2\xab\xb7\x8d\xb0(3[\
\xb3\xd3\xce\x85,?\xa3\xf6\xef\xb8\x16b*\xbe\xe5\xcc\
\xee\xc5\xa0\xb1Xz\xa2k\x8d\xc2\xbf\x1d\xf7^a\x13\
\xae\xac\xce\xb9\x81C\x07\xdaC#\xfc\xb5\x14\x09\xba?\
\xd1\x04\xe9\xfbKS\xc3\xd3Z\xc0\xe0DG'\xbeD\
\xf7\x8bl#\x89\xaeJ!lw\x07%\x11}\xcd\x19\
J\xac\x09]\x95\x14\x0b\x9aX\x17=CU>\xe2\x9b\
\xbb\x14 5\xae5J\xaf9 \xd1\xc5$N\x9e\x84\
6\xd4\xf1\xa0\xfd\xafI[\xde7\xa1m\xc2\xb1\xe7\xad\
\xb5\x1a\x09\xf2\xb1\xc4\x1enc\x0br\xc3r5\xb8\x82\
1\x0d\xdf\xa2\x88\xf8\x85o\x99\x9c\xd8\xb7\xbb\xbf\xc7\xb7\
\xb0\x01\x9aPCW}\xc7\xf4\xa3o\x99$\xbb*M\
X(\xe1i\x8dF\x13\x9d\xf4L\x0eLt\xbf\xf2\xcd\
\x15\xdf\xdf\xbd\xa0w\xe1\xf6?M>\x8do3%\xb4\
\x90\xa4X`\xc3\xdeD\xf3Y\xdc\xe3\x1a\xbau\xbe^\
\xe3\xca\xa9\x99ee\x8d=\xb9`xo7u\x8ch\
8\xf6\xf8\xb5\x7f[[\xd4g\x0a\xc2mlA\xe4\x16\
c\xd9\x04n\x93\xd8\xb9\x93\xc0\xc2\x9a\xd4\x1e\x1a\x83\x16\
9\xb2\xbf\xd5\xfa\x8cu\xcb\xa1[Xc\xccIh\xfc\
\x925\xe66\xf9Uh\x0a\xf8niu\xa8\xc2\xc7\xd4\
\xd2\xb2\xe8M\x5c\x9dD\x17\x1a\x96\x91TnL\xce{\
\x22\xe6!5\xa0`ME\xda\xa9\xce\xf3\x8e3\xb3L\
\xa9{\xaa\x0be\xdfU\xa41\xc0\xbbX\x9f\x92\x18&\
\xf6\xf4B\x96uhMVHv\x05)\x91\xb9\xcf\xc1\
\xe5%\xef\x81\x94+b\x83\xd2`S\x09\x8f\xf72\x8f\
$)\xe4\xeeq\x1cG\xc0\xa5\xe1Ix\x95k+\xa6\
Ht\xed\xd5\xa8\x1c.6\x7f\xad;\x0fi\x96(\x87\
\xda\x07\xe5v\xea\x19\xd9\x12\xe5\x11\xaa]\x8d\x96\xad\x84\
\x96j;\xa31\xa5\x1d\xcai\x81.\xf4s\x8d4\xf6\
p\x1b;&\x11EC\x1fH&\x1ei\xa0w\x02n\
*xni\xdfK\x99\xe8:\xebV\xc2\x8bM\xa1'\
\xd9\xe4\x12\xd2\xe8\x1863\xff\x99\x90\xec`j]\xb8\
[\xfb\xbe\xce\x1by}s\x1aj\xa1\x83\xf1\x07\x97\xec\
\xf8\x03\xf8LHv0\xb5>\xb5\x97\xcf|\xb5$\xe9\
\xb4x\x9e\xb6\xdd\xcb\x14\x5cs\x83\xd9\x846\xbb\x82H\
\x8f\xeaO6\xd0\xa0&mvC_\x19\x13OZ\xe6\
\xa6\xac\xd3\xa4\x9c.haj\x92\x82>\x00\xef\x88B\
\xc3,R?_)\xbd\x9f\xe7&\xd4\xb1Sp\xadE\
n\xf9\x9al\x02\xb7\xce:\xf06g\xfb\x94td5\
i\x12\x92\xf6;T\xec1\x8f#\xf6\x90\xec\x0a\x92\x94\
\x88$\x11\xde'\x0a\x8f\x90O\xde\x89\x0eI\x08\xf5*\
\x9a\x17\x8b\xf5\xad\x874\xe34\xd1\xc5\x0f\xdf_\xc5:\
\xe5\x11\x9f\xf4\x0d\x84M\x99\xf0\x5c3\xd4\x93X\xe0p\
\x8eX\xd2\x85O\x12\xd6\x93{\xc7\x83\xc7\xbfM\xafp\
\x19\xb2t\xf2,&L\xc20i\xb8\x8d-\x08\x9b(\
\x98\xc8\x22\xeb\xbe\xf2-{\xbbrP\xd3y\xc9\x06\xfd\
\x5c\x9dr'8|\x85\xe2\x1e\xb1\xf5\xb4\x8f\xdeL\xea\
\xf1\xb0$\xbeD\xe78ZY<\xde~@\xd7\x1f?\
\xea&\xbc\x8a\xefoF\xae\x14.7\xa9A\x16Y\x1e\
r\x1c\xc2\x8f}\xc9q\xa2Y\xe3\x06\x0f\x8d\xea\xcaR\
\x86\x93\x8a=$\xbb\x82,\xb8\x85\xa3\xd0rc\xfcc\
\xaf\xacI\xf7\xf4\x82\x0eQ\x09=m!\xcaif\xc7\
h\xcf\x83\xd7\xaa\xa7\x9d7o\x5cB\x89\xae[s\x8b\
\x9f\x92\xc8\x98\xf0\xfe\xd0\xd8:I*\xb7\xd0\x03\xe9Y\
{\x22?r\xb7j\xa1\xe5\xfa\xd8W\x9a\xb2o\xcf\x19\
w\xf0f\x8c\x13\x00x\xb5\xcb?\xf8\x9cp\xf9\x13\xdb\
zL\x09\xba\x03\xc3'\x11{Hv\x05\xd1\x91\xefr\
B5|\xcb}\x01~\x1a\xbff*\xbe\xed\xe2)\xb3\
\xaf\xfe\x7f`\xf4\xbd\x8a'`\x94\xa41\xe8\xc4\xd7@\
\xd3\x07\xd4;s\x91-\xc7\x13)\xea|kS M\
\xa2\xeb\xca\x93\xf0\xa4\xfd/i2\x84G:\x8f[\xff\
\xab:\x83\x8a\xcdX\x13K\x8a\x85\xf6\xfb\x89\xe7,|\
4\xa8\x96\xa3\xaf\xe9\xb2x\xf2W2\xf14\x5c\xd30\
\xd1jR\x19\xea\xf1\x93\xe4\xfc<\x14{:\xd9,M\
(\xf6f\xb6\xcdnT\x13z\x0e#\xe2h\xcf\x9aR\
\xd5\xb7\xbc3K\xeb\x86\x84\xc5\x89\x8e\xed\x92+\xcdZ\
b\x97\xbe\xeb\x9d\x0fm\xc1\xdd\xdao\x99\xd6Nh,\
\x97\x06\x9d\x9c \x1br\x0b\xdcp\xdcnX\xb6:\xd6\
\xcc\xb4\x96\xb9\xbf\xa3\xc4\xba\xfd\xb3\xea\xe1\xeb\xac\x13f\
\x16)K\xa2\xeb\xd2\x84w\xbar\xd0\xff\xac\xb0\xb7\x0d\
\x8f9:J\x1a\xf4*\xed\xae\x8f\xe5\xf8\xecpg\xe2\
\x05\xe9\xb8\xa8\xea@\xee\xd0`q\x9f\xa4XP\x1a\x0f\
R\x1b\xd2\xd9\x81O\xba\xc7\xc9\xc4O\x88\xb4$\x19\xf4\
(O\xc3D\xab\x1a{\x97\xe6r=<)\x82Y\x93\
\xf7\xba\x962\xf6\x1e\x8f3\xf6P\xb3+P\xe76\xb3\
\x91\xb0ZE\x03B\x82z#i\xf2M\x9d\x9a\xa7?\
\xb8\xb5\xd6\xc0\xcc\xa9\x1a\x82\xb56\xa9\xfb\x89\xf7e\xa8\
\xea\xd9_9\xcb0\x8e\xa2\xc5\xfb\xe5\xbe^\xd7\x84D\
\xd7\xe5\xab\xe1\x19\xba\xfe\xb0|\xcac\xd3\xd9\xbe]f\
\xc1\x87\xf9\x13\xe8\xfe\xa4\xfc\x9f$\xad\xa7\xc7\xe4\xeaq\
\x22\xf2>\x0e6\xf1\x89V5\xf6\x1c\xbb\x22c\xef3\
\x89\xbd\x7f\x8d+\xf6\x90\xec\x0a&W\xf4\xcd\xbcs\xa0\
\xf5\x90\x13\x9e]4\xf0\x09\x82\xc5\xc6\xf6Q\xf0\xe9\x82\
\xec\xfb\x9a\x988\xa1qk\xe5\xca{I\x95\xe8\xba4\
\xe1\xf5&1\xde\xbb\xdb\x18<\xa5\xb8\x1e\x9b\x84\xb6\xbb\
B\xdd\xd5\x9e\xf1\x14s\xc7\xa5\xc6T\x9et\xc7\x92&\
\xf1Y\x8d=$\xbb\x82\xc5S\x02\xb1[\x19*\xe1\xc9\
\x09o\xb8\xf5 t\xc2'>N\x95a_Y\x92\xcb\
(\xc4\x13x\xb6'\xaf\xcc\xf5^n\xf2\xcd\x07\xed\x89\
/yo1\xf0-_\xdd\xc4:\xce\x84\xa7\xc98M\
\x0d/\x91|>-\x9biyv\xb9\xa0\xd8{#q\
\xfe\xe7q\xc5\x1e\x92\xdd\x08t\x13^\xca\xdb\xa6\x1e\xf1\
\xec\xb5\x12\xd4i\xbe\x874\x0e:\x9d\x116gb\xd5\
\x93\xf0W\xbe\xf9\xd54|\xe1\x8e\xbc\x87\xdab}k\
)\xcf{\xd1\xdb+\xd9\xf6\xab\xc5\x84\xaf3Tq\xb9\
jM2m\x99i\x0d{\xc8IM\xb5\x86\xd7N\xe6\
\xd9\x8fSw\xf6d\xfd|\xd3p\x9c\xba:\xb1\xb7\x94\
;\xf6\xa4L%\xf6\xfe\x98\xf6\xfbv\x8b0\x91\x0e\x8a\
\xac\x93i\x8ec\xf2\xcd\xa2u\x02sE\xda#6J\
&\x9e%\xb7\x1aZ_\x93\x5cd\xf9\xc9\x17\xc7\xdbG\
\x94Ag\xc6\x94\xa5\xb4\xfb\xd1\x93\xe7\x9d\xf4\xaa\x19\x8e\
\xf6\xa7\xe9\xe4\x19'=\xc1\xb4\xb1_\xae\xf5\xbbV'\
-\x1d\xd0\xe0\xae\xe5\xa4=\xdf\x17|k\x7f\xc1\x5c>\
4y\xbf^\xacC\x93\xb9\xfcSK{\x9ct\x98\x87\
\xf6~\xea\xfe\x97\xea\xd3\xf9\x0cp'~\xf2\xc4\xdeQ\
\xea\x99~\x0a4\xdc\x11\x0c\xc0\x97d\xf7\xd2\xb6\x96\x05\
\xbaX\x96\xbat\xe5\xfdt9\xaei\x9d=\xff\x95n\
5\x8a\xbc=\x89\xc7e\xc9~$\x00oG\xcc\x9f\x95\
\x8c\xf9E\xda\xab\xfe\xa7\x03C\xe75\xc1\x85t\xcbK\
\x8f\x8b\x96\x15G\xfc\xd3\xd5c\x22\xb1\xac\x1d\x03\xdeG\
\xd7$)mf}\x22\x22\x14\x0f\x8e\xdc\xc9\xac\x1e'\
-KK\xb6lJ\xe6\xfe\xb4\xc5\x1e\x92\x9dG\xd1\xc9\
\x0e\xa6\x8b~\xabY\xda[(\x1d\xd8\x1b\xea\x99\xd5\xa6\
\x84I\xd4T \x1b\xb4\xd9\xc1\xdc\xd1D\xc7\xf6\xe6\xab\
4\xa3\xf8\x93\x06}\x8bs$\xba\xd9\x80\x89\x00`\xae\
\xb4\xbf`\xdc\xc6\xe3\xd5tP\xef\xd9\xea\xe1Z\xcb\xd8\
\xbd\xcb\xa8\xd4\xd3\x94\xa0\xdf\xb9\xd0\xfe&7\x0e\x7fm\
%Q\xa66V\x98\x1c$;\x98+\xfd_\xf2,\xc9\
j\xf9\x06\xbb\xe77\xac\xd3\xdb\xd5\xa6\xbe&\x9d\x11\xe5\
\xf6\x93-\xc9-\x19\xbe\xb1\x900}\x90\xec`nt\
\xbe\xb8:PS3\x15\xfdo\xfa\xc6f\xdeC\x87\xcf\
\xec\x18Y\xb2C\x03?L\x1bv\xb6\x5c\x5c+ux\
\x003L\x1ftP\xc0\xdc\xf8}c{\x7f\xd8\x91\xff\
:VLnq\xbfG\xa2\x9b=\xa8}\xc1\xdc\x89\x1f\
<\x97\xb6;kL\xea)\x860\x18{\xf6!\xd9\xc1\
\xdc\xea\xcc\xb6Q\xb5\xd6~\xa3S9\xc9\xc9Py?\
u\x167\xe5\xb5\xf3\x88\xe8Gc\xa2\x93\x0b\xb7p4\
\xbd\xdff\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x01\xff\x07\xb4<2M\x93'\x06\
\xcb\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x03X\
<\
?xml version=\x221.\
0\x22 encoding=\x22utf\
-8\x22?>\x0a<!-- Gener\
ator: Adobe Illu\
strator 23.0.1, \
SVG Export Plug-\
In . SVG Version\
: 6.00 Build 0) \
-->\x0a<svg versio\
n=\x221.1\x22 id=\x22Laye\
r_1\x22 xmlns=\x22http\
://www.w3.org/20\
00/svg\x22 xmlns:xl\
ink=\x22http://www.\
w3.org/1999/xlin\
k\x22 x=\x220px\x22 y=\x220p\
x\x22\x0a\x09 viewBox=\x220 \
0 100 100\x22 style\
=\x22enable-backgro\
und:new 0 0 100 \
100\x22 xml:space=\x22\
preserve\x22>\x0a<styl\
e type=\x22text/css\
\x22>\x0a path{fill\
:rgb(150, 146, 1\
44)}\x0a polygon\
{fill:rgb(150, 1\
46, 144)}\x0a ci\
rcle{fill:rgb(15\
0, 146, 144)}\x0a \
rect{fill:rgb(\
150, 146, 144)}\x0a\
</style><path d=\
\x22M51.3,75.9c-1.9\
,0-3.8-0.8-5-2.4\
L18.1,38.4c-2.2-\
2.8-1.8-6.9,1-9.\
1c2.8-2.2,6.9-1.\
8,9.1,1l28.2,35.\
1c2.2,2.8,1.8,6.\
9-1,9.1\x0a\x09C54.2,7\
5.5,52.7,75.9,51\
.3,75.9z\x22/>\x0a<pat\
h d=\x22M51.3,75.9c\
-1.4,0-2.9-0.5-4\
-1.4c-2.8-2.2-3.\
2-6.3-1-9.1l28.2\
-35.1c2.2-2.8,6.\
3-3.2,9.1-1c2.8,\
2.2,3.2,6.3,1,9.\
1L56.4,73.5\x0a\x09C55\
.1,75.1,53.2,75.\
9,51.3,75.9z\x22/>\x0a\
</svg>\x0a\
\x00\x00\x03Y\
<\
?xml version=\x221.\
0\x22 encoding=\x22utf\
-8\x22?>\x0a<!-- Gener\
ator: Adobe Illu\
strator 23.0.1, \
SVG Export Plug-\
In . SVG Version\
: 6.00 Build 0) \
-->\x0a<svg versio\
n=\x221.1\x22 id=\x22Laye\
r_1\x22 xmlns=\x22http\
://www.w3.org/20\
00/svg\x22 xmlns:xl\
ink=\x22http://www.\
w3.org/1999/xlin\
k\x22 x=\x220px\x22 y=\x220p\
x\x22\x0a\x09 viewBox=\x220 \
0 100 100\x22 style\
=\x22enable-backgro\
und:new 0 0 100 \
100;\x22 xml:space=\
\x22preserve\x22>\x0a<sty\
le type=\x22text/cs\
s\x22>\x0a path{fil\
l:rgb(150, 146, \
144)}\x0a polygo\
n{fill:rgb(150, \
146, 144)}\x0a c\
ircle{fill:rgb(1\
50, 146, 144)}\x0a \
rect{fill:rgb\
(150, 146, 144)}\
\x0a</style><path d\
=\x22M31.8,56.4c-1.\
9,0-3.8-0.8-5-2.\
4c-2.2-2.8-1.8-6\
.9,1-9.1l35.1-28\
.2c2.8-2.2,6.9-1\
.8,9.1,1c2.2,2.8\
,1.8,6.9-1,9.1L3\
5.8,54.9\x0a\x09C34.6,\
55.9,33.2,56.4,3\
1.8,56.4z\x22/>\x0a<pa\
th d=\x22M66.9,84.6\
c-1.4,0-2.9-0.5-\
4-1.4L27.7,54.9c\
-2.8-2.2-3.2-6.3\
-1-9.1c2.2-2.8,6\
.3-3.2,9.1-1l35.\
1,28.2c2.8,2.2,3\
.2,6.3,1,9.1\x0a\x09C7\
0.6,83.8,68.8,84\
.6,66.9,84.6z\x22/>\
\x0a</svg>\x0a\
\x00\x00\x03X\
<\
?xml version=\x221.\
0\x22 encoding=\x22utf\
-8\x22?>\x0a<!-- Gener\
ator: Adobe Illu\
strator 23.0.1, \
SVG Export Plug-\
In . SVG Version\
: 6.00 Build 0) \
-->\x0a<svg versio\
n=\x221.1\x22 id=\x22Laye\
r_1\x22 xmlns=\x22http\
://www.w3.org/20\
00/svg\x22 xmlns:xl\
ink=\x22http://www.\
w3.org/1999/xlin\
k\x22 x=\x220px\x22 y=\x220p\
x\x22\x0a\x09 viewBox=\x220 \
0 100 100\x22 style\
=\x22enable-backgro\
und:new 0 0 100 \
100\x22 xml:space=\x22\
preserve\x22>\x0a<styl\
e type=\x22text/css\
\x22>\x0a path{fill\
:rgb(150, 146, 1\
44)}\x0a polygon\
{fill:rgb(150, 1\
46, 144)}\x0a ci\
rcle{fill:rgb(15\
0, 146, 144)}\x0a \
rect{fill:rgb(\
150, 146, 144)}\x0a\
</style><path d=\
\x22M51.3,75.9c-1.9\
,0-3.8-0.8-5-2.4\
L18.1,38.4c-2.2-\
2.8-1.8-6.9,1-9.\
1c2.8-2.2,6.9-1.\
8,9.1,1l28.2,35.\
1c2.2,2.8,1.8,6.\
9-1,9.1\x0a\x09C54.2,7\
5.5,52.7,75.9,51\
.3,75.9z\x22/>\x0a<pat\
h d=\x22M51.3,75.9c\
-1.4,0-2.9-0.5-4\
-1.4c-2.8-2.2-3.\
2-6.3-1-9.1l28.2\
-35.1c2.2-2.8,6.\
3-3.2,9.1-1c2.8,\
2.2,3.2,6.3,1,9.\
1L56.4,73.5\x0a\x09C55\
.1,75.1,53.2,75.\
9,51.3,75.9z\x22/>\x0a\
</svg>\x0a\
"
qt_resource_name = b"\
\x00\x08\
\x05\xe2Y'\
\x00l\
\x00o\x00g\x00o\x00.\x00p\x00n\x00g\
\x00\x08\
\x0aaZ\xa7\
\x00i\
\x00c\x00o\x00n\x00.\x00p\x00n\x00g\
\x00\x0d\
\x0dq\x0b\x87\
\x00c\
\x00o\x00l\x00l\x00a\x00p\x00s\x00e\x00d\x00.\x00s\x00v\x00g\
\x00\x0c\
\x07)\x8aG\
\x00e\
\x00x\x00p\x00a\x00n\x00d\x00e\x00d\x00.\x00s\x00v\x00g\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00L\x00\x00\x00\x00\x00\x01\x00\x00\x14\x9b\
\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xe2\
\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x00\x00\x11>\
"
def qInitResources():
QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
def qCleanupResources():
QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
qInitResources()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
path{fill:rgb(150, 146, 144)}
polygon{fill:rgb(150, 146, 144)}
circle{fill:rgb(150, 146, 144)}
rect{fill:rgb(150, 146, 144)}
</style><path d="M31.8,56.4c-1.9,0-3.8-0.8-5-2.4c-2.2-2.8-1.8-6.9,1-9.1l35.1-28.2c2.8-2.2,6.9-1.8,9.1,1c2.2,2.8,1.8,6.9-1,9.1L35.8,54.9
C34.6,55.9,33.2,56.4,31.8,56.4z"/>
<path d="M66.9,84.6c-1.4,0-2.9-0.5-4-1.4L27.7,54.9c-2.8-2.2-3.2-6.3-1-9.1c2.2-2.8,6.3-3.2,9.1-1l35.1,28.2c2.8,2.2,3.2,6.3,1,9.1
C70.6,83.8,68.8,84.6,66.9,84.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 857 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100" xml:space="preserve">
<style type="text/css">
path{fill:rgb(150, 146, 144)}
polygon{fill:rgb(150, 146, 144)}
circle{fill:rgb(150, 146, 144)}
rect{fill:rgb(150, 146, 144)}
</style><path d="M51.3,75.9c-1.9,0-3.8-0.8-5-2.4L18.1,38.4c-2.2-2.8-1.8-6.9,1-9.1c2.8-2.2,6.9-1.8,9.1,1l28.2,35.1c2.2,2.8,1.8,6.9-1,9.1
C54.2,75.5,52.7,75.9,51.3,75.9z"/>
<path d="M51.3,75.9c-1.4,0-2.9-0.5-4-1.4c-2.8-2.2-3.2-6.3-1-9.1l28.2-35.1c2.2-2.8,6.3-3.2,9.1-1c2.8,2.2,3.2,6.3,1,9.1L56.4,73.5
C55.1,75.1,53.2,75.9,51.3,75.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 856 B

+191 -37
View File
@@ -8,13 +8,17 @@ import threading
from cefpython3 import cefpython as cef
import PySide2
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *
import server.gui.cellxgene_rc
from server.gui.browser import CefWidget, CefApplication
from server.gui.options_parser import parse_opt_string
from server.cli.launch import parse_engine_args
from server.gui.workers import Worker, SiteReadyWorker
from server.gui.utils import WINDOWS, LINUX, MAC, FileLoadSignals, Emitter, WorkerSignals
from server.utils.constants import MODES
from server.gui.utils import WINDOWS, LINUX, MAC, FileLoadSignals, Emitter, WorkerSignals, FileChanged
from server.utils.errors import OptionsError
from server.utils.utils import find_available_port
if WINDOWS or LINUX:
@@ -24,17 +28,19 @@ if WINDOWS or LINUX:
# Configuration
# TODO remember this or calculate it?
WIDTH = 1024
HEIGHT = 768
WIDTH = 1300
HEIGHT = 800
GUI_PORT = find_available_port("localhost")
BROWSER_INDEX = 0
LOAD_INDEX = 1
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__(None)
self.cef_widget = None
self.data_widget = None
self.stacked_layout = None
self.parent_conn, self.child_conn = None, None
self.load_emitter = None
self.emitter_thread = None
@@ -49,6 +55,7 @@ class MainWindow(QMainWindow):
def showBrowser(self):
self.stacked_layout.setCurrentIndex(BROWSER_INDEX)
def restartOnError(self):
self.window().shutdownServer()
# close emitter on error/finished
@@ -120,8 +127,12 @@ class MainWindow(QMainWindow):
file_menu.addAction(load_action)
def showLoad(self):
self.clearMessages()
self.stacked_layout.setCurrentIndex(LOAD_INDEX)
def clearMessages(self):
self.data_widget.reset()
def closeEvent(self, event):
# Close browser (force=True) and free CEF reference
if self.cef_widget.browser:
@@ -147,57 +158,105 @@ class LoadWidget(QFrame):
logo_layout = QHBoxLayout()
logo_layout.setContentsMargins(0, 0, 0, 20)
load_layout = QGridLayout()
load_layout.setContentsMargins(0, 0, 0, 0)
load_layout.setSpacing(0)
file_layout = QVBoxLayout()
message_layout = QHBoxLayout()
message_layout.setContentsMargins(0, 0, 0, 0)
self.serverError = False
self.title = ""
self.label = QLabel("cellxgene")
self.file_name = FilePath()
self.label = QLabel()
logo = QPixmap(":/logo.png")
self.label.setPixmap(logo)
self.label.setContentsMargins(100, 0, 100, 0)
logo_layout.addWidget(self.label)
# UI section
# TODO add load spinner
# TODO add cancel button to send back to browser (if available)
self.embedding_label = QLabel("embedding: ")
load_layout.addWidget(self.embedding_label, 0, 0)
self.file_label = QLabel("file: ")
load_layout.addWidget(self.file_label, 0, 1)
self.embeddings = QComboBox(self)
self.embeddings.currentIndexChanged.connect(self.updateEmbedding)
self.embeddings.addItems(MODES)
self.embedding_selection = [MODES[0]]
load_layout.addWidget(self.embeddings, 1, 0)
self.load = QPushButton("Open...")
self.load.clicked.connect(self.onLoad)
load_layout.addWidget(self.load, 1, 1)
self.file_area = FileArea()
self.file_name.signals.changed.connect(self.updatePath)
self.launch_widget = QPushButton("Launch cellxgene")
self.launch_widget.setEnabled(False)
self.launch_widget.clicked.connect(self.onLoad)
self.progress = QProgressBar()
self.progress.setTextVisible(False)
self.advanced_options = CLIOptionsArea(self)
file_layout.addWidget(self.file_area)
file_layout.addWidget(self.advanced_options)
self.loading_layout = QStackedLayout()
self.loading_layout.addWidget(self.launch_widget)
self.loading_layout.addWidget(self.progress)
file_layout.addLayout(self.loading_layout)
file_layout.setStretch(0, 10)
# Error section
self.error_label = QLabel("")
self.error_label.setWordWrap(True)
self.error_label.setFixedWidth(self.MAX_CONTENT_WIDTH)
message_layout.addWidget(self.error_label, alignment=Qt.AlignTop)
message_layout.addWidget(self.error_label)
# Options Form
# Layout
for l in [logo_layout, load_layout, message_layout ]:
for l in [logo_layout, file_layout, message_layout]:
load_ui_layout.addLayout(l)
load_ui_layout.setStretch(2, 10)
#TODO remove magic number
load_ui_layout.setStretch(1, 10)
self.setLayout(load_ui_layout)
self.timer = QTimer()
self.timer.setInterval(100)
self.timer.timeout.connect(self.updateProgress)
self.signals = FileLoadSignals()
self.signals.selectedFile.connect(self.createScanpyEngine)
self.signals.error.connect(self.onError)
def updateEmbedding(self, idx):
self.embedding_selection = [MODES[idx]]
def updatePath(self):
file_name = self.file_name.value
if file_name:
self.file_area.label.setText("File: " + file_name)
else:
self.file_area.label.setText("")
self.launch_widget.setEnabled(bool(file_name))
def reset(self):
self.loading_layout.setCurrentIndex(0)
self.timer.stop()
self.error_label.setText("")
self.file_name.updateValue(None)
self.advanced_options.reset()
def updateProgress(self):
curr_val = self.progress.value()
next_val = (curr_val + 1) % 100
self.progress.setValue(next_val)
def resetProgress(self):
print("here i am")
self.progress.setValue(0)
self.loading_layout.setCurrentIndex(0)
self.timer.stop()
def createScanpyEngine(self, file_name):
try:
self.advanced_options.parse()
except OptionsError as e:
self.signals.error.emit(f"Options Error: {e}")
return
title = self.advanced_options.title
if not title:
title = splitext(basename(file_name))[0]
self.window().setupServer()
worker = Worker(self.window().parent_conn, self.window().child_conn, file_name, self.title, host="127.0.0.1", port=GUI_PORT,
layout=self.embedding_selection)
worker = Worker(self.window().parent_conn, self.window().child_conn, file_name, host="127.0.0.1",
port=GUI_PORT, title=title, engine_options=self.advanced_options.engine_options)
self.window().load_emitter.signals.ready.connect(self.onDataReady)
self.window().load_emitter.signals.engine_error.connect(self.onServerError)
self.window().load_emitter.signals.server_error.connect(self.onServerError)
@@ -208,15 +267,14 @@ class LoadWidget(QFrame):
self.window().child_conn.close()
def onLoad(self):
options = QFileDialog.Options()
# options |= QFileDialog.DontUseNativeDialog
file_name, _ = QFileDialog.getOpenFileName(self,
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
self.title = splitext(basename(file_name))[0]
if file_name:
self.signals.selectedFile.emit(file_name)
if self.file_name.value:
# Reset error on reload
self.serverError = False
self.loading_layout.setCurrentIndex(1)
self.timer.start()
self.signals.selectedFile.emit(self.file_name.value)
else:
self.signals.error.emit("Please select a file before launching.")
def onDataReady(self):
self.site_ready_worker = SiteReadyWorker(self.window().url)
@@ -228,6 +286,7 @@ class LoadWidget(QFrame):
def onServerReady(self):
if not self.serverError:
self.resetProgress()
self.window().cef_widget.browser.Navigate(self.window().url)
self.window().showBrowser()
@@ -236,13 +295,105 @@ class LoadWidget(QFrame):
if server_error:
self.serverError = True
# Report error and switch to load screen
self.window().shutdownServer()
self.window().shutdownServer()
self.resetProgress()
self.window().stacked_layout.setCurrentIndex(LOAD_INDEX)
self.error_label.setText(f"Error: {err}")
self.error_label.resize(self.MAX_CONTENT_WIDTH, self.error_label.height())
self.window().repaint()
onServerError = partialmethod(onError, server_error=True)
class CLIOptionsArea(QFrame):
def __init__(self, parent):
super(CLIOptionsArea, self).__init__(parent)
self.engine_options = {}
self.setFixedWidth(500)
self.title = None
self.form_layout = QFormLayout()
self.form_layout.setContentsMargins(0,0,0,0)
self.cli_label = QLabel("CLI Options:")
self.cli_label.setToolTip("Additional options can be passed as cli parameters. See cellxgene --help for more info")
self.cli_widget = QLineEdit()
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
self.form_layout.addRow(self.cli_label, self.cli_widget)
self.setLayout(self.form_layout)
def reset(self):
self.cli_widget.setText("")
self.engine_options = {}
self.title = None
def parse(self):
opts = parse_opt_string(self.cli_widget.text())
self.title = opts.pop("title", None)
self.engine_options = parse_engine_args(**opts)
class FilePath(QObject):
def __init__(self):
super(FilePath, self).__init__()
self.value = ""
self.signals = FileChanged()
def updateValue(self, path=None):
self.value = path
self.signals.changed.emit(self.value != path)
class FileArea(QFrame):
def __init__(self):
super(FileArea, self).__init__()
self.setFrameShape(QFrame.Box)
self.setMinimumHeight(100)
self.setFixedWidth(500)
self.setAcceptDrops(True)
self.instructions = QLabel(self)
self.instructions.setText("Drag & Drop a h5ad file to load or open")
self.instructions.setGeometry(10, 10, 500, self.instructions.height())
self.loadButton = QPushButton("Open...", parent=self)
x_pos = (500 - self.loadButton.width()) / 2
self.loadButton.setGeometry(x_pos, 50, self.loadButton.width(), self.loadButton.height())
self.loadButton.clicked.connect(self.fileBrowse)
self.label = QLabel(self)
self.label.setGeometry(10, 75, 500, self.label.height())
def fileBrowse(self):
options = QFileDialog.Options()
# options |= QFileDialog.DontUseNativeDialog
file_name, _ = QFileDialog.getOpenFileName(self,
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
if file_name:
self.parent().file_name.updateValue(file_name)
def dragEnterEvent(self, e):
if e.mimeData().hasUrls:
e.accept()
else:
e.ignore()
def dragMoveEvent(self, e):
if e.mimeData().hasUrls:
e.accept()
else:
e.ignore()
def dropEvent(self, e):
"""
Drop files directly onto the widget
File locations are stored in fname
:param e:
:return:
"""
if e.mimeData().hasUrls:
e.setDropAction(Qt.CopyAction)
e.accept()
for url in e.mimeData().urls():
file_name = str(url.toLocalFile())
self.parent().file_name.updateValue(file_name)
else:
e.ignore()
def main():
# This generates an error.log file on error
@@ -256,6 +407,9 @@ def main():
cef.Initialize(settings)
app = CefApplication(sys.argv)
main_window = MainWindow()
main_window.setWindowTitle("cellxgene")
main_window.setUnifiedTitleAndToolBarOnMac(True)
main_window.setWindowIcon(QIcon(":icon.png"))
main_window.show()
main_window.activateWindow()
main_window.raise_()
+27
View File
@@ -0,0 +1,27 @@
import shlex
import click
from server.cli.launch import common_args
from server.utils.errors import OptionsError
@click.command()
@common_args
def cli(**kwargs):
pass
def parse_opt_string(opts):
context = click.Context(cli)
parser = click.OptionParser(context)
for command in context.command.params:
command.add_to_parser(parser, context)
try:
opts, args, param_order = parser.parse_args(shlex.split(opts))
for param in cli.params:
value, args = param.handle_parse_result(context, opts, args)
except click.ClickException as ce:
raise OptionsError(ce.message) from ce
return dict(context.params)
+5
View File
@@ -41,6 +41,11 @@ class SiteReadySignals(QObject):
class FileLoadSignals(QObject):
selectedFile = Signal(str)
error = Signal(str)
class FileChanged(QObject):
changed = Signal(bool)
class Emitter:
+4 -4
View File
@@ -21,13 +21,13 @@ class EmittingProcess(Process):
class Worker(EmittingProcess):
def __init__(self, parent_conn, child_conn, data_file, title, host, port, layout=["umap"], *args, **kwargs):
def __init__(self, parent_conn, child_conn, data_file, host, port, title, engine_options, *args, **kwargs):
super(Worker, self).__init__(parent_conn, child_conn)
self.data_file = data_file
self.layout = layout
self.title = title
self.host = host
self.port = port
self.title = title
self.engine_options = engine_options
def run(self):
super(Worker, self).run()
@@ -47,12 +47,12 @@ class Worker(EmittingProcess):
# load data
try:
args = {
"layout": self.layout,
"max_category_items": 100,
"diffexp_lfc_cutoff": 0.01,
"obs_names": None,
"var_names": None,
}
args.update(self.engine_options)
data = ScanpyEngine(self.data_file, args)
server.attach_data(data, self.title)
self.emit("ready")
+32
View File
@@ -0,0 +1,32 @@
import unittest
from server.gui.options_parser import parse_opt_string
from server.utils.errors import OptionsError
default_opts = {'title': None, 'layout': (), 'obs_names': None, 'var_names': None, 'max_category_items': 1000,
'diffexp_lfc_cutoff': 0.01}
class UtilsTest(unittest.TestCase):
def test_empty_opts(self):
result = parse_opt_string("")
self.assertEqual(result, default_opts)
def test_one_opts(self):
result = parse_opt_string("--title abcde")
expected_opts = dict(default_opts)
expected_opts["title"] = "abcde"
self.assertEqual(result, expected_opts)
def test_malformed_opts(self):
with self.assertRaises(OptionsError):
parse_opt_string("--ewwwww lkafsjkl")
def test_complex_opts(self):
result = parse_opt_string(
"--title abcde --obs-names zzzzz --var-names 'xxx yyy' --max-category-items 234907 "
"--diffexp-lfc-cutoff 0.00003847382")
expected_opts = {'title': 'abcde', 'layout': (), 'obs_names': 'zzzzz', 'var_names': 'xxx yyy',
'max_category_items': 234907, 'diffexp_lfc_cutoff': 3.847382e-05}
self.assertEqual(result, expected_opts)
+7
View File
@@ -0,0 +1,7 @@
class OptionsError(Exception):
"""
Raised when user specified cli options specified fail parsing
"""
def __init__(self, message):
self.message = message
+3 -1
View File
@@ -1,4 +1,6 @@
[flake8]
max-line-length = 120
ignore = E203, W503
exclude = server/app/util/fbs/NetEncoding/
exclude =
server/app/util/fbs/NetEncoding/,
server/gui/cellxgene_rc.py