Edit on GitHub

starplast.guided_panel

The Start-here tab: one question at a time, ending at the strategies worth running.

Beside the Strategies tab, which is left exactly as it was. The Strategies tab answers "what is there"; this one answers "where do I begin", for someone who arrives with a gene list from a screen, their own measurement, or nothing but curiosity.

question    one per screen, in a few plain words, with large choices and nothing else on the
            screen to read; a gene step searches, a gene-list step pastes, loads, takes the
            genes gated on the map or offers a real example, and a column step shows each
            column's coverage so an almost-empty one is never picked by accident
trail       every answer so far along the top, each one a button back to the question that
            asked it, so nothing is a one-way door
the end     three to six recommended strategies, each as the card summary the Strategies tab
            already shows -- name, method, calibration grade and the same four headline bars
            (`strategy_panel.headline`, `strategy_card`, `scorecard.HEADLINE`) -- with one line
            on why it is recommended, and Run it here / Open in Strategies

The questions and the ranking are :mod:starplast.guided, which holds no Qt: this module draws them and nothing more. It also holds no window: it emits what the user asked for (run_strategy, open_strategy, open_view) and :func:install wires those to the application's strategy panel and docks, which is what lets the whole panel be built and walked in a test with no window at all.

TIPS = {'back': 'Back to the previous question. Your later answers are dropped, because they were answers to questions that followed from this one.', 'restart': 'Start again from the first question. Nothing is kept.', 'next': 'Take what is entered here and go on to the next question.', 'trail': 'Go back to this question and answer it differently. Everything after it is asked again.', 'search': 'Type an accession, a symbol, or a word from the product description; the matches appear below.', 'matches': 'The genes matching what you typed, with their product. Click one to choose it.', 'paste': 'Paste or type identifiers, one per line or separated by commas or spaces. Anything this table does not hold is reported rather than silently dropped.', 'file': 'Read identifiers from a text or CSV file: the first column of each line, or the whole file if it is a plain list.', 'gate': 'Use the genes currently gated on the 3D map, so a group you drew around can be handed straight to the questions.', 'example': 'Fill the list with the members of one known category, so the branch can be tried before you have a list of your own.', 'filter': 'Type to shorten the list to the columns whose name contains the text.', 'run': 'Run this strategy now, with the settings your answers decided. It runs in the background like any job, and its tables appear under Strategies, Results.', 'open': 'Open this strategy in the Strategies tab with the settings your answers decided, where its guide, every setting and its self-test are.', 'test_genes': "Your genes' labels are hidden all at once and the five fast strategies are asked for them back; the answer opens in the evidence panel, gene by gene. Genes with no label cannot be tested and are left out.", 'view': 'Open this panel: it answers the question you asked better than running anything does.', 'option': 'Choose this and go on to the next question.'}
FILTER_ABOVE = 12
MATCH_LIMIT = 40
class Choice(PyQt6.QtWidgets.QAbstractButton):

One answer, as a wide flat row: its label, its coverage or kind on the right, and a border.

Painted rather than styled so the row reads the same on all four themes and so the detail can be dimmer than the label, which a single stylesheet on a QPushButton cannot do.

Choice(option: starplast.guided.Option, parent=None)
option
def sizeHint(self) -> PyQt6.QtCore.QSize:

Two lines of breathing room around one line of text.

def paintEvent(self, _event):

paintEvent(self, e: QPaintEvent|None)

class Card(PyQt6.QtWidgets.QFrame):

A frame whose height follows from its width, which is what wrapped text needs in a scroller.

A QScrollArea sizes its contents to their minimum height, and a word-wrapped QLabel has no minimum height until its width is known: without this the recommendation cards collapsed on top of one another and their buttons vanished. Declaring height-for-width and handing the question to the card's own layout is Qt's answer to that, and GuidedPanel._fit_body asks the same question of the column of cards.

Card(parent=None)
def hasHeightForWidth(self) -> bool:

Yes: the height depends on how wide the card is drawn.

def heightForWidth(self, width: int) -> int:

How tall this card must be at width, as its own layout works it out.

def minimumSizeHint(self) -> PyQt6.QtCore.QSize:

Never shorter than the layout needs at the width it currently has.

class RecommendationCard(Card):

One recommended strategy, as the Strategies tab's card summarises it, plus why and Run.

The four bars are scorecard.headline_bars drawn by strategy_card.Scorecard, filled from strategy_panel.headline -- the same numbers the Strategies tab shows for the same strategy, computed in one place rather than two.

RecommendationCard(rec: starplast.guided.Recommendation, organism: str, parent=None)
def run(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def open_it(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

rec
run_btn
open_btn
def paintEvent(self, _event):

paintEvent(self, a0: QPaintEvent|None)

class GuidedPanel(PyQt6.QtWidgets.QWidget):

The guided tab: the current question, the trail of answers, and the recommendations.

GuidedPanel(ctx, gated=None, parent=None)

Build the panel over one organism's context; other spaces are loaded if they are asked for. gated is a callable returning the gene ids gated on the map, or None.

def status(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def run_strategy(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def open_strategy(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def open_view(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def test_genes(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

answers: dict
cards: list
trail_host
trail
question
note
def scroll(unknown):
body
body_layout
back_btn
next_btn
restart_btn
def context(self):

The table the questions are asked of: the space chosen, or the one this panel was built on until one is.

def answer(self, step: str, value):

Answer one question and move on. Later answers are dropped, since they followed it.

def drop_after(self, step: str):

Forget every answer that came after step, which a changed answer has invalidated.

def back(self):

Ask the last answered question again, or do nothing on the first one.

def goto(self, step: str):

Ask step again, dropping it and everything after it.

def restart(self):

Back to the first question with nothing kept.

def render(self):

Draw the question due now, or the recommendations when there is none left.

def resizeEvent(self, event):

Wrapped text is taller in a narrow panel, so the scrolled column is measured again.

def restyle(self) -> bool:

Apply the card stylesheet of the theme and text size in force; True if it changed.

def set_genes(self, genes) -> int:

Put a gene list in the box (and say how many landed); for the buttons and for tests.

def load_file(self, path: str = '') -> int:

Fill the gene box from a text or CSV file. Returns how many identifiers were read.

def use_gated(self) -> int:

Fill the gene box with the genes gated on the 3D map.

def use_example(self) -> int:

Fill the gene box with the members of one known category of the default label.

def recommendations(self) -> list:

The ranked strategies for the answers as they stand.

def install(window) -> GuidedPanel | None:

Add the Start-here tab to the main window, beside Strategies, and wire what it asks for.

Everything it emits is carried out by the panels that already do those things: a strategy is selected, filled in and run in the Strategies tab, so it appears in the Jobs panel and its tables land where every other strategy's tables land; a view request raises that dock.