{% extends layout|default('base.html', true) %} {% block style %} {% endblock %} {% block content %} Plexora - Edit {{ data.datasetName }} {# Generated from the project record: every section below is gated on `project.has`, so a project sees only the fields it actually has. The old edit page rendered the CSV column-matching screen for every project regardless of format, which is what made editing an AnnData project destroy it. #} {% set project = data.project %}
{# Single quotes, and they matter. `tojson` escapes < > & and ' but NOT the double quotes JSON is built out of, so in a double-quoted attribute the value ends at the record's first key and the rest spills into the tag as garbage attributes. `id` still parsed, so the page looked completely normal -- and projectEdit.js threw on JSON.parse("{"), which is where the Save button lost its listener. #}
Edit Project

{{ data.datasetName }}

{# Image: read-only. Everything the project derives is keyed to it. #}
Image

{# A sample with no image is not a broken one: transcripts, a table and a mask, or spots all register against a blank reference frame, and what is drawn is the layers on top of it. Printing an empty path here read as a fault. #} {% if project.image.blank %} No image — blank reference frame
{% else %} {{ project.image.src }}
{% endif %} {# "3 channel(s)" is true of a brightfield image's storage and wrong about what it is: three samples of one colour picture are not three markers. #} {% if project.image.blank %}Layers are registered against it {% elif project.imageType == 'brightfield' %}Colour (RGB) {% else %}{{ project.image.channelCount or 0 }} channel(s) {% endif %} {% if project.image.size[0] %} · {{ project.image.size[0] }} × {{ project.image.size[1] }} px {% endif %}

The image cannot be changed — import a new project to use a different one.
Segmentation Mask
{# "any" rather than "file": a .zarr mask is a directory, which the server has read since LocalSegmentationProvider was written and which this button could not pick at all. #}
{% if project.segmentation.pending %} Converting — the mask will be ready shortly. {% else %} Clear the box to remove the mask. {% endif %}
{# Mounted by projectEdit.js from the shared control (dataSourceField.js), not written out here: swapping in a .zarr store with several tables needs a table picker, and the two static Browse buttons this used to carry were never wired to anything -- only the upload page's script attaches [data-browse-target], and it does not load on this page. #}
Data
{# The path itself comes from the project record the page is already carrying; only the note needs saying here. #} {# Three notes, because there are three states. An unresolved source is the one worth wording carefully: the file is recorded and has never been read, so "Detected as spatialdata" would be claiming something nobody has checked, and a user looking at a project that silently opens as an image deserves to be told which question is still open. #}
{# What the numbers in this project actually are. The select is shown only when the file carries more than one matrix -- raw counts and a log-transformed copy live side by side in one .h5ad, and which was read decides what every marker histogram is a histogram of. The log1p switch is offered either way: a file may carry raw counts and nothing else, and thresholds on those are unreadable untransformed. #} {% if project.has.features %}
Expression Values
{% if project.data.layers %}
Which matrix the marker intensities are read from. Thresholds set against one do not carry to another.
{% endif %}
{% endif %} {# The read spec is how the adapter gets from file to table. Shown so the user can see what was auto-detected; changing it means replacing the data file above, which re-detects it. #} {% if project.has.readSpec %}
How this file is read
{# Coordinates are no longer summarised here: they are a real control in the Columns section below, because which array holds a cell's position is a choice and not a detail. This line read "auto-detected" for a project whose source was picked by matching an obsm key against a hardcoded list of names -- accurate, and no help to anyone it was wrong for. #}

Features: {{ project.data.features.get('layer') or project.data.features.get('source') or 'X' }} {% if project.data.subset.get('column') %}
Image: {{ project.data.subset['column'] }} = {{ project.data.subset['value'] }} {% endif %}

{% endif %} {# CSV only: the marker/metadata split, which also owns the roles. #} {% if project.has.columns %}
Columns
{% endif %} {# Everything else with a table: only the roles backed by real columns (the adapter synthesizes the rest). #} {% if project.has.data and not project.has.columns %}
Columns
{% endif %} {# Whether this image's pixels are one colour picture or a stack of marker channels. Detected from the file at import and shown here with the reason, because the one case that needs a human is a file whose metadata says nothing -- and the person who ran the scan is the only one who knows. Changing it re-reads the image. It never rewrites it: the two modes are two readings of the same bytes, and switching back and forth is free. Hidden for a flat PNG/JPEG, which has no second reading, and for an image on a data node, where the file is not here to re-read. #} {% if project.image.kind != 'rgb' and project.image.src %}
Image Type
is clipped at the control's width with no way to read the rest -- so it goes in the hint below, where the Cell Layer section also puts what its choice currently resolves to. #} Currently reading it as {%- if project.imageType == 'brightfield' %} one true-colour image {%- else %} {{ project.image.channelCount }} marker channel{{ '' if project.image.channelCount == 1 else 's' }} {%- endif %}{% if project.imageTypeReason %}, because {{ project.imageTypeReason }}{% endif %}. Changing this re-reads the image; the file on disk is never modified.
{% endif %} {# What one pixel is worth. The same control the viewer's channel panel carries, in its `alwaysShow` mode: there it hides itself when the file states its own size, because a scale bar that contradicts its own source is the one failure a scale bar must not have -- but a stated size CAN be wrong, and this is where it is meant to be fixable without re-importing. Saves on its own, immediately, through /set_pixel_size, rather than with the rest of this page: it is the one field here that changes nothing about how the file is READ, so making it wait for a Save that also re-runs the image would be a slow way to correct a typo. #} {# Shown for a blank frame too, and deliberately: its micron-per- pixel is not decoration there, it is what every layer registered against it was scaled by, so it is the one field on this page that a sample with no image most needs. #} {% if project.image.src or project.image.blank %}
Image Scale
{% include "_scale_calibration.html" %}
{% endif %} {# What else is registered against this image. Read-only apart from the two verbs that make sense here -- add one, remove one -- for the same reason the Image section is read-only: a layer's alignment, its channels and its colour belong to the Layers panel in the viewer, where they can be seen while being changed. The reference layer is listed and cannot be removed: it is the coordinate system, and the way to change it is to import again. #}
Layers
{% if project.layers %}
    {% for layer in project.layers %}
  • {{ layer.label }} {{ layer.modality or layer.kind }} {%- if layer.status != 'ready' %} · {{ layer.status }}{% endif %} {%- if layer.unresolved %} · needs {{ layer.unresolved|join(', ') }}{% endif %} {% if layer.id not in ('__image__', '__mask__', '__centroids__') %} {% endif %}
  • {% endfor %}
{% else %}

Nothing but the image yet.

{% endif %}
{# How tools draw per-cell results. Defaulted rather than asked -- the mask if there is one, centroids otherwise -- so this is an override. "Automatic" is a real option and the one an untouched project is on, because the alternative is that saving this page for any other reason records a choice nobody made. That mattered: a project whose mask was still converting could only offer centroids here, and the save froze centroids in place for good -- the mask landed minutes later and every tool went on drawing points. #} {% if project.has.cellLayer %}
Cell Layer
Used by tools that colour cells, such as Cell Explorer and Thresholding. Currently drawing: {% if project.cellLayer == 'segmentation' %}the segmentation mask{% else %}centroids{% endif %}.
{% endif %} {# Where each piece of this project's data is read from. Rendered by projectEdit.js from /project//resources rather than here, because the choices depend on which nodes are answering right now -- and that is a question the page has to be able to re-ask after an attach without a reload. Applied immediately rather than on Save, unlike everything above. Attaching is not an edit to a field: it re-reads the resource, and a form that queued that up alongside a role change would make one button mean two very different sizes of thing. #}
{# No blocking wait here. A mask arriving mid-session is adopted in place by the viewer (views/segmentationWait.js and main.js's watchLayers), and the import pages that used to overlay a progress card over a form are gone -- the import dialog reports progress inline and opens the sample as soon as its record exists. #} {# `alwaysShow`, unlike the viewer's: this page is where a physical size the FILE states is meant to be correctable. No viewer to tell -- the scale bar is on the other page, and it re-reads the endpoint when it loads. Deferred so the class (loaded in base.html, not deferred) is already defined. #} {% endblock %}