Skip to content

Allow configs to mix top-level tracks with groups (drop the "every track must live in a group" requirement) #160

@dlrice

Description

@dlrice

Context
Today every track in a config must live inside a GroupConfig. The default UniProt viewer has 15 groups, each with one or more tracks, so the rule fits the canonical layout — but it forces awkward modelling for embedders whose data shapes don't naturally cluster. A small lab pointing the viewer at a single CRISPR-guide track has to wrap that one track in a one-track group with a synthetic id and label. Likewise, configs that want a couple of standalone "headline" tracks alongside the canonical UniProt grouping have no way to express it: every track gets a group wrapper or none does.

The schema's authoring ergonomics don't need a one-or-the-other choice — the renderer treats a one-track group the same as a standalone track for layout purposes. We can let authors mix freely: top-level tracks for the simple cases, groups for clusters, both in the same config when that's the right model.

Task
Extend the config schema to accept top-level tracks alongside top-level groups, and thread the change through validate / normalize / render. Authors should be able to write:

groups:
  - id: signal_peptide        # standalone track, no group wrapper
    kind: features
    filter: SIGNAL
    data: features
  - id: DOMAINS               # group with child tracks
    tracks:
      - id: domain
        kind: features
        filter: DOMAIN
        data: features
  - id: confidence            # another standalone track
    kind: confidence-score
    data: { source: alphafold-prediction }

…and have the viewer render two single rows (Signal peptide, Confidence) bracketing one collapsible group (Domains).

Scope:

  • Schema discrimination. GroupConfig is currently distinguished by the presence of tracks: TrackConfig[]. Extend the JSON Schema and types.ts so each entry under the top-level groups: is a discriminated union of GroupConfig and TrackConfig. Use tracks: as the discriminator (presence → group; absence → track). Keep groups: as the field name for back-compat — renaming to a more generic items: is a separate breaking-change conversation.
  • Validator. Extend the unique-id check to span the entire top-level array (groups and standalone tracks share an id namespace at the top level — collisions across the two shapes are still a config bug). Update the "Track has no rendering path" check so standalone tracks (no parent group component) still validate when they have a kind or explicit component.
  • Normalizer. Standalone tracks need their own NormalizedTrack shape at the top level, OR they can be wrapped in a synthetic single-track NormalizedGroup with label === track.label and a CSS hint so the renderer omits the collapsible header. Pick one — wrapping is simpler for downstream code; the bare-track shape is more honest. Document the decision in the architecture doc.
  • Renderer. A standalone-track entry should render as a single row with no group-collapse affordance. A one-track group should keep its collapse affordance — the difference is author-controlled, not auto-collapsed.
  • Inheritance. Standalone tracks inherit from defaults.rendering directly, with no group-rendering layer. The cascade for a standalone track is defaults → kind preset → track. Update normalize.ts's cascade and the JSDoc that documents it.
  • Extends merger. Mixed-shape merging needs care: a child config replacing a base group with a standalone track (or vice versa) should be permitted but explicit. Either it's fine and child-wins, or it's a structural config bug that should fail validation. Pick one and document the choice.
  • Tests. Add coverage for: a config with one standalone track and zero groups; a config with mixed groups + standalone tracks; an extends merge replacing a group with a standalone track; a unique-id collision between a group id and a standalone track id; a standalone track with no kind and no component (validation should catch it the same way it catches the group-track equivalent).
  • Migration. Scan src/default-config.yaml for groups whose only purpose is wrapping a single track. If any are clear candidates for standalone-track expression, update the default and confirm the rendered output is unchanged. (Likely zero candidates given the canonical layout, but worth a pass.)
  • Architecture doc. Update docs/architecture.md's "How a config becomes pixels" section to mention that standalone tracks bypass the group-rendering layer, and add a bullet under "Conventions and gotchas" about the shared id namespace.

Notes:

  • The cleanest implementation models a standalone track as GroupConfig | TrackConfig at the top level rather than introducing a separate topLevelTracks: array. Two arrays at the same level encourages authors to think about ordering twice; one array preserves declaration order naturally.
  • The discriminator-by-presence-of-tracks approach has a sharp edge: a track that happens to have a property called tracks (it shouldn't, but YAML is forgiving) would be misclassified. The structural validator should explicitly forbid tracks on a track-shaped entry to keep the discrimination unambiguous.
  • Don't add a "type": "group" / "type": "track" field — that's a worse authoring surface than presence-based discrimination, and the schema can express the discrimination cleanly via oneOf.
  • Keep groups: as the field name even though it now accepts non-groups. Renaming to items: would be a breaking change for every existing config in the world, including the default, with marginal authoring benefit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    nextIssue which pertains to the next version of ProtVista.

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions