← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/msa/matrix-renderer.tsx

31.1 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import clsx from "clsx";
import { memo, useMemo, type KeyboardEvent } from "react";
import { FormattedMessage, useIntl } from "react-intl";

import { getSequenceResidueStyle } from "../sequence/sequence-palette";

import type { MsaDerivedAnalysis, MsaRowProjection } from "./analysis";
import { ALIGNMENT_ROW_GROUP_METADATA_KEY } from "./alignment-editing";
import type { FocusedMsaCell, MsaHoverCellDetails } from "./cell-hover-overlay";
import {
  isNonsynonymousCodonDifference,
  translateStandardCodon,
} from "./codon";
import {
  CODING_NONSYNONYMOUS_COLOR,
  CODING_SYNONYMOUS_COLOR,
  DIFFERENCE_MATCH_COLOR,
  DIFFERENCE_MISMATCH_COLOR,
  getMsaCellBackground,
  getMsaCellTextColor,
  isThemeAwareMsaPalette,
  type MsaColorMode,
  type MsaResiduePalette,
} from "./colors";
import type { MsaColumnSummary } from "./conservation";
import {
  getUngappedPosition,
  getUngappedPositionFromProjection,
} from "./coordinate-map";
import { buildHoverCellDetails } from "./hover-details";
import { isGapSymbol } from "./residue-alphabet";
import type { MsaMotifSearchHit } from "./search";
import type {
  MsaAnnotationTrack,
  MsaColumnRange,
  MsaDocument,
  MsaInsertionRun,
  MsaSequenceRow,
} from "./types";
import {
  MSA_ROW_HEIGHT_PX,
  MSA_ROW_LABEL_WIDTH_PX,
  type MsaViewportSlice,
} from "./virtualization";

const MATCH_PLACEHOLDER = "·";
const EMPTY_ANNOTATION_VALUE = " ";

function makeRovingTabStop(target: HTMLElement): void {
  const grid = target.closest('[role="grid"]');
  grid
    ?.querySelectorAll<HTMLElement>('[data-msa-cell="true"]')
    .forEach((cell) => {
      cell.tabIndex = cell === target ? 0 : -1;
    });
}

function moveGridFocus(
  event: KeyboardEvent<HTMLElement>,
  alignedLength: number,
): number | null {
  const row = Number(event.currentTarget.dataset.msaRowIndex);
  const column = Number(event.currentTarget.dataset.msaColumn);
  if (!Number.isInteger(row) || !Number.isInteger(column)) return null;
  let nextRow = row;
  let nextColumn = column;
  if (event.key === "ArrowLeft") nextColumn -= 1;
  else if (event.key === "ArrowRight") nextColumn += 1;
  else if (event.key === "ArrowUp") nextRow -= 1;
  else if (event.key === "ArrowDown") nextRow += 1;
  else if (event.key === "Home") nextColumn = 0;
  else if (event.key === "End") nextColumn = Math.max(0, alignedLength - 1);
  else if (event.key === "PageUp") nextRow -= 10;
  else if (event.key === "PageDown") nextRow += 10;
  else return null;
  const target = event.currentTarget
    .closest('[role="grid"]')
    ?.querySelector<HTMLElement>(
      `[data-msa-row-index="${Math.max(0, nextRow)}"][data-msa-column="${Math.max(0, Math.min(alignedLength - 1, nextColumn))}"]`,
    );
  if (target == null) return null;
  target.focus();
  return nextColumn;
}

type HitRange = {
  end: number;
  selected: boolean;
  start: number;
};

export const MsaMatrixRenderer = memo(function MsaMatrixRenderer({
  analysis,
  cellWidth,
  colorMode,
  columns,
  document,
  motifHits,
  onFocusedCellChange,
  onHoverCellDetailsChange,
  onKeyboardRangeSelection,
  onPinnedCellChange,
  onRangeSelectionExtend,
  onRangeSelectionStart,
  referenceSequence,
  residuePalette,
  rows,
  selectedColumnRange,
  selectedHit,
  showAnnotationTracks,
  showIdenticalAsDots,
  showRnaStructureOverlays,
  slice,
  summaries,
  totalVisibleRows,
}: {
  analysis: MsaDerivedAnalysis | null;
  cellWidth: number;
  colorMode: MsaColorMode;
  columns: Array<number>;
  document: MsaDocument;
  motifHits: Array<MsaMotifSearchHit>;
  onFocusedCellChange: (cell: FocusedMsaCell) => void;
  onHoverCellDetailsChange: (details: MsaHoverCellDetails) => void;
  onKeyboardRangeSelection: (
    anchorColumn: number,
    nextColumn: number,
    extend: boolean,
  ) => void;
  onPinnedCellChange: (cell: FocusedMsaCell) => void;
  onRangeSelectionExtend: (column: number) => void;
  onRangeSelectionStart: (column: number) => void;
  referenceSequence: string | null;
  residuePalette: MsaResiduePalette | null;
  rows: Array<MsaSequenceRow>;
  selectedColumnRange: MsaColumnRange | null;
  selectedHit: MsaMotifSearchHit | null;
  showAnnotationTracks: boolean;
  showIdenticalAsDots: boolean;
  showRnaStructureOverlays: boolean;
  slice: MsaViewportSlice;
  summaries: Array<MsaColumnSummary>;
  totalVisibleRows: number;
}): React.ReactElement {
  const intl = useIntl();
  const referenceLabel = intl.formatMessage({
    id: "codex.filePreview.msa.referenceRow",
    defaultMessage: "Reference",
    description: "Row label for the rendered MSA reference sequence.",
  });
  const projectionByRowId = useMemo(
    () =>
      new Map(
        (analysis?.projections ?? []).map((projection) => [
          projection.rowId,
          projection,
        ]),
      ),
    [analysis?.projections],
  );
  const hitRangesByRow = useMemo(
    () => indexHitRanges(motifHits, selectedHit),
    [motifHits, selectedHit],
  );
  const insertionByRowColumn = useMemo(
    () =>
      analysis?.insertionByRowColumn ?? indexInsertions(document.insertions),
    [analysis?.insertionByRowColumn, document.insertions],
  );
  const topSpacerHeight = slice.rowStart * MSA_ROW_HEIGHT_PX;
  const bottomSpacerHeight =
    Math.max(0, totalVisibleRows - slice.rowEnd) * MSA_ROW_HEIGHT_PX;
  return (
    <div
      aria-label={intl.formatMessage({
        id: "codex.filePreview.msa.viewerAria",
        defaultMessage: "Interactive multiple sequence alignment viewer",
        description:
          "Accessible label for the interactive MSA alignment matrix.",
      })}
      aria-colcount={document.alignedLength}
      aria-rowcount={totalVisibleRows + (referenceSequence == null ? 0 : 1)}
      className="p-3 font-mono text-[11px] leading-5"
      role="grid"
      style={{
        minWidth:
          MSA_ROW_LABEL_WIDTH_PX +
          Math.max(1, document.alignedLength) * cellWidth,
      }}
    >
      <MsaColumnHeader
        cellWidth={cellWidth}
        columns={columns}
        document={document}
        onRangeSelectionExtend={onRangeSelectionExtend}
        onRangeSelectionStart={onRangeSelectionStart}
        selectedColumnRange={selectedColumnRange}
        slice={slice}
      />
      {referenceSequence == null ? null : (
        <MsaGridRow
          analysis={analysis}
          cellWidth={cellWidth}
          colorMode={colorMode}
          columns={columns}
          document={document}
          hitRanges={[]}
          isFirstRenderedRow={false}
          label={referenceLabel}
          insertionByColumn={undefined}
          onFocusedCellChange={onFocusedCellChange}
          onHoverCellDetailsChange={onHoverCellDetailsChange}
          onKeyboardRangeSelection={onKeyboardRangeSelection}
          onPinnedCellChange={onPinnedCellChange}
          onRangeSelectionExtend={onRangeSelectionExtend}
          onRangeSelectionStart={onRangeSelectionStart}
          projection={undefined}
          referenceSequence={referenceSequence}
          residuePalette={residuePalette}
          row={null}
          rowIndex={0}
          selectedColumnRange={selectedColumnRange}
          showIdenticalAsDots={false}
          showRnaStructureOverlays={showRnaStructureOverlays}
          slice={slice}
          summaries={summaries}
          values={referenceSequence}
        />
      )}
      {showAnnotationTracks ? (
        <MsaAnnotationRows
          alignedLength={document.alignedLength}
          cellWidth={cellWidth}
          columns={columns}
          slice={slice}
          tracks={document.annotations}
        />
      ) : null}
      {document.cdsContext.applicability === "eligible" ? (
        <>
          <MsaCodonFrameRow
            alignedLength={document.alignedLength}
            cellWidth={cellWidth}
            columns={columns}
            slice={slice}
          />
          <MsaTranslationRow
            alignedLength={document.alignedLength}
            cellWidth={cellWidth}
            columns={columns}
            residuePalette={residuePalette}
            sequence={
              referenceSequence ?? document.rows[0]?.alignedSequence ?? ""
            }
            slice={slice}
          />
        </>
      ) : null}
      {topSpacerHeight > 0 ? (
        <div aria-hidden="true" style={{ height: topSpacerHeight }} />
      ) : null}
      {rows.map((row, renderedRowIndex) => (
        <MsaGridRow
          analysis={analysis}
          cellWidth={cellWidth}
          colorMode={colorMode}
          columns={columns}
          document={document}
          hitRanges={hitRangesByRow.get(row.id) ?? []}
          isFirstRenderedRow={renderedRowIndex === 0}
          insertionByColumn={insertionByRowColumn[row.id]}
          key={row.id}
          label={formatRowLabel(row)}
          onFocusedCellChange={onFocusedCellChange}
          onHoverCellDetailsChange={onHoverCellDetailsChange}
          onKeyboardRangeSelection={onKeyboardRangeSelection}
          onPinnedCellChange={onPinnedCellChange}
          onRangeSelectionExtend={onRangeSelectionExtend}
          onRangeSelectionStart={onRangeSelectionStart}
          projection={projectionByRowId.get(row.id)}
          referenceSequence={referenceSequence}
          residuePalette={residuePalette}
          row={row}
          rowIndex={
            slice.rowStart +
            renderedRowIndex +
            (referenceSequence == null ? 0 : 1)
          }
          selectedColumnRange={selectedColumnRange}
          showIdenticalAsDots={showIdenticalAsDots}
          showRnaStructureOverlays={showRnaStructureOverlays}
          slice={slice}
          summaries={summaries}
          values={row.alignedSequence}
        />
      ))}
      {bottomSpacerHeight > 0 ? (
        <div aria-hidden="true" style={{ height: bottomSpacerHeight }} />
      ) : null}
    </div>
  );
});

function formatRowLabel(row: MsaSequenceRow): string {
  const group = row.metadata?.[ALIGNMENT_ROW_GROUP_METADATA_KEY]?.trim();
  return group == null || group.length === 0
    ? row.label
    : `${group} · ${row.label}`;
}

function MsaColumnHeader({
  cellWidth,
  columns,
  document,
  onRangeSelectionExtend,
  onRangeSelectionStart,
  selectedColumnRange,
  slice,
}: {
  cellWidth: number;
  columns: Array<number>;
  document: MsaDocument;
  onRangeSelectionExtend: (column: number) => void;
  onRangeSelectionStart: (column: number) => void;
  selectedColumnRange: MsaColumnRange | null;
  slice: MsaViewportSlice;
}): React.ReactElement {
  const intl = useIntl();
  return (
    <div
      className="sticky top-0 z-20 flex bg-token-main-surface-primary pb-1"
      role="row"
    >
      <div
        className="sticky left-0 z-30 shrink-0 bg-token-main-surface-primary pr-2 text-right text-token-text-tertiary"
        role="columnheader"
        style={{ width: MSA_ROW_LABEL_WIDTH_PX }}
      >
        <FormattedMessage
          id="codex.filePreview.msa.columnHeader"
          defaultMessage="Column"
          description="Header label for MSA alignment columns."
        />
      </div>
      <HorizontalSpacer width={slice.columnStart * cellWidth} />
      {columns.map((column) => (
        <button
          aria-colindex={column + 1}
          aria-label={intl.formatMessage(
            {
              id: "codex.filePreview.msa.selectColumnRange",
              defaultMessage:
                "Start or extend an MSA column selection at column {column}",
              description:
                "Accessible label for a draggable MSA column ruler cell.",
            },
            { column: column + 1 },
          )}
          className={clsx(
            "inline-flex shrink-0 cursor-col-resize justify-center text-token-text-tertiary",
            isColumnWithinRange(selectedColumnRange, column) &&
              "bg-sky-400/20 text-token-text-primary",
          )}
          key={column}
          onPointerDown={() => onRangeSelectionStart(column)}
          onPointerEnter={() => onRangeSelectionExtend(column)}
          role="columnheader"
          style={{ width: cellWidth }}
          type="button"
        >
          {(column + 1) % 10 === 0 ? column + 1 : MATCH_PLACEHOLDER}
        </button>
      ))}
      <HorizontalSpacer
        width={
          Math.max(0, document.alignedLength - slice.columnEnd) * cellWidth
        }
      />
    </div>
  );
}

function MsaAnnotationRows({
  alignedLength,
  cellWidth,
  columns,
  slice,
  tracks,
}: {
  alignedLength: number;
  cellWidth: number;
  columns: Array<number>;
  slice: MsaViewportSlice;
  tracks: Array<MsaAnnotationTrack>;
}): React.ReactElement | null {
  if (tracks.length === 0) {
    return null;
  }
  return (
    <>
      {tracks.map((track) => (
        <div className="flex text-token-text-tertiary" key={track.id}>
          <div
            className="sticky left-0 z-10 shrink-0 truncate bg-token-main-surface-primary pr-2 text-right"
            style={{ width: MSA_ROW_LABEL_WIDTH_PX }}
            title={track.label}
          >
            {track.label}
          </div>
          <HorizontalSpacer width={slice.columnStart * cellWidth} />
          {columns.map((column) => (
            <span
              className={clsx(
                "inline-flex shrink-0 justify-center",
                track.kind === "rna-secondary-structure" &&
                  "font-semibold text-purple-700",
              )}
              key={`${track.id}:${column}`}
              style={{ width: cellWidth }}
            >
              {track.values[column] ?? EMPTY_ANNOTATION_VALUE}
            </span>
          ))}
          <HorizontalSpacer
            width={Math.max(0, alignedLength - slice.columnEnd) * cellWidth}
          />
        </div>
      ))}
    </>
  );
}

function MsaCodonFrameRow({
  alignedLength,
  cellWidth,
  columns,
  slice,
}: {
  alignedLength: number;
  cellWidth: number;
  columns: Array<number>;
  slice: MsaViewportSlice;
}): React.ReactElement {
  return (
    <div className="flex text-emerald-700">
      <div
        className="sticky left-0 z-10 shrink-0 bg-token-main-surface-primary pr-2 text-right"
        style={{ width: MSA_ROW_LABEL_WIDTH_PX }}
      >
        <FormattedMessage
          id="codex.filePreview.msa.codonFrame"
          defaultMessage="Codon frame"
          description="Label for the codon-reading-frame helper row in eligible nucleotide MSAs."
        />
      </div>
      <HorizontalSpacer width={slice.columnStart * cellWidth} />
      {columns.map((column) => (
        <span
          className="inline-flex shrink-0 justify-center"
          key={column}
          style={{ width: cellWidth }}
        >
          {String((column % 3) + 1)}
        </span>
      ))}
      <HorizontalSpacer
        width={Math.max(0, alignedLength - slice.columnEnd) * cellWidth}
      />
    </div>
  );
}

function MsaTranslationRow({
  alignedLength,
  cellWidth,
  columns,
  residuePalette,
  sequence,
  slice,
}: {
  alignedLength: number;
  cellWidth: number;
  columns: Array<number>;
  residuePalette: MsaResiduePalette | null;
  sequence: string;
  slice: MsaViewportSlice;
}): React.ReactElement {
  const intl = useIntl();
  const useThemePalette = isThemeAwareMsaPalette(residuePalette);
  return (
    <div
      className={clsx(
        "flex",
        useThemePalette ? "text-token-text-secondary" : "text-sky-700",
      )}
      title={
        useThemePalette && residuePalette !== "neutral"
          ? intl.formatMessage({
              id: "codex.filePreview.msa.translationPaletteDescription",
              defaultMessage:
                "Translation colors show amino-acid groups, not substitution impact or conservation.",
              description:
                "Clarifies the residue-class colors used by the translation helper row.",
            })
          : undefined
      }
    >
      <div
        className="sticky left-0 z-10 shrink-0 bg-token-main-surface-primary pr-2 text-right"
        style={{ width: MSA_ROW_LABEL_WIDTH_PX }}
      >
        <FormattedMessage
          id="codex.filePreview.msa.translationTrack"
          defaultMessage="AA translation"
          description="Label for the amino-acid translation track in eligible coding DNA MSAs."
        />
      </div>
      <HorizontalSpacer width={slice.columnStart * cellWidth} />
      {columns.map((column) => {
        const codonStart = column - (column % 3);
        const residue =
          column % 3 === 1
            ? (translateStandardCodon(
                sequence.slice(codonStart, codonStart + 3),
              ) ?? "·")
            : " ";
        return (
          <span
            className="inline-flex shrink-0 justify-center font-semibold"
            key={column}
            style={{
              ...(useThemePalette && residue.trim() !== ""
                ? getSequenceResidueStyle({
                    molecule: "protein",
                    paletteId:
                      residuePalette === "neutral"
                        ? "neutral"
                        : "muted-amino-acid",
                    residue,
                  })
                : {}),
              width: cellWidth,
            }}
          >
            {residue}
          </span>
        );
      })}
      <HorizontalSpacer
        width={Math.max(0, alignedLength - slice.columnEnd) * cellWidth}
      />
    </div>
  );
}

const MsaGridRow = memo(function MsaGridRow({
  analysis,
  cellWidth,
  colorMode,
  columns,
  document,
  hitRanges,
  isFirstRenderedRow,
  insertionByColumn,
  label,
  onFocusedCellChange,
  onHoverCellDetailsChange,
  onKeyboardRangeSelection,
  onPinnedCellChange,
  onRangeSelectionExtend,
  onRangeSelectionStart,
  projection,
  referenceSequence,
  residuePalette,
  row,
  rowIndex,
  selectedColumnRange,
  showIdenticalAsDots,
  showRnaStructureOverlays,
  slice,
  summaries,
  values,
}: {
  analysis: MsaDerivedAnalysis | null;
  cellWidth: number;
  colorMode: MsaColorMode;
  columns: Array<number>;
  document: MsaDocument;
  hitRanges: Array<HitRange>;
  isFirstRenderedRow: boolean;
  insertionByColumn: Record<string, MsaInsertionRun | undefined> | undefined;
  label: string;
  onFocusedCellChange: (cell: FocusedMsaCell) => void;
  onHoverCellDetailsChange: (details: MsaHoverCellDetails) => void;
  onKeyboardRangeSelection: (
    anchorColumn: number,
    nextColumn: number,
    extend: boolean,
  ) => void;
  onPinnedCellChange: (cell: FocusedMsaCell) => void;
  onRangeSelectionExtend: (column: number) => void;
  onRangeSelectionStart: (column: number) => void;
  projection: MsaRowProjection | undefined;
  referenceSequence: string | null;
  residuePalette: MsaResiduePalette | null;
  row: MsaSequenceRow | null;
  rowIndex: number | undefined;
  selectedColumnRange: MsaColumnRange | null;
  showIdenticalAsDots: boolean;
  showRnaStructureOverlays: boolean;
  slice: MsaViewportSlice;
  summaries: Array<MsaColumnSummary>;
  values: string;
}): React.ReactElement {
  const intl = useIntl();
  return (
    <div
      aria-rowindex={rowIndex == null ? undefined : rowIndex + 1}
      className="group flex"
      role="row"
    >
      <div
        className="sticky left-0 z-10 shrink-0 truncate bg-token-main-surface-primary pr-2 text-right text-token-text-secondary"
        style={{ width: MSA_ROW_LABEL_WIDTH_PX }}
        title={label}
      >
        {label}
      </div>
      <HorizontalSpacer width={slice.columnStart * cellWidth} />
      {columns.map((column) => {
        const symbol = values[column] ?? "-";
        const referenceSymbol = referenceSequence?.[column];
        const isReferenceMatch =
          showIdenticalAsDots &&
          referenceSymbol != null &&
          symbol.toUpperCase() === referenceSymbol.toUpperCase();
        const hitState = getHitState(hitRanges, column);
        const insertion = insertionByColumn?.[String(column)];
        const rnaPair = analysis?.pairByColumn[String(column)];
        const isSelectedColumn = isColumnWithinRange(
          selectedColumnRange,
          column,
        );
        const backgroundColor =
          colorMode === "coding-impact"
            ? getCodingImpactBackground({
                column,
                referenceSequence,
                rowSequence: values,
                symbol,
              })
            : getMatrixCellBackground({
                colorMode,
                column,
                document,
                referenceSymbol,
                residuePalette,
                summaries,
                symbol,
              });
        const textColor = getMsaCellTextColor({
          backgroundColor,
          mode: colorMode,
          moleculeType: document.displayInterpretation.moleculeType,
          palette: residuePalette,
          symbol,
        });
        return (
          <button
            aria-label={intl.formatMessage(
              {
                id: "codex.filePreview.msa.cellAria",
                defaultMessage: "{label} column {column} {symbol}",
                description:
                  "Accessible label for one interactive MSA matrix cell.",
              },
              {
                column: column + 1,
                label,
                symbol,
              },
            )}
            aria-colindex={column + 1}
            aria-selected={isSelectedColumn}
            className={clsx(
              "relative inline-flex h-5 shrink-0 items-center justify-center border border-transparent text-token-text-primary",
              "focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-token-text-primary",
              showRnaStructureOverlays && getRnaPairBorderClass(rnaPair),
              isSelectedColumn && "outline outline-1 outline-sky-400",
              hitState.any && "ring-1 ring-amber-300",
              hitState.selected && "z-[1] ring-2 ring-orange-500",
            )}
            key={`${label}:${column}`}
            data-msa-cell="true"
            data-msa-column={column}
            data-msa-row-index={rowIndex}
            onBlur={() => onHoverCellDetailsChange(null)}
            onClick={() => {
              if (row != null) {
                onPinnedCellChange({ column, row, symbol });
              }
            }}
            onFocus={(event) => {
              makeRovingTabStop(event.currentTarget);
              publishActiveCellDetails({
                analysis,
                column,
                document,
                insertion,
                onFocusedCellChange,
                onHoverCellDetailsChange,
                publishFocus: true,
                projection,
                referenceSymbol,
                row,
                showRnaStructureOverlays,
                symbol,
                target: event.currentTarget,
              });
            }}
            onKeyDown={(event) => {
              const nextColumn = moveGridFocus(event, document.alignedLength);
              if (nextColumn == null) return;
              event.preventDefault();
              onKeyboardRangeSelection(column, nextColumn, event.shiftKey);
            }}
            onMouseEnter={(event) =>
              publishActiveCellDetails({
                analysis,
                column,
                document,
                insertion,
                onFocusedCellChange,
                onHoverCellDetailsChange,
                publishFocus: false,
                projection,
                referenceSymbol,
                row,
                showRnaStructureOverlays,
                symbol,
                target: event.currentTarget,
              })
            }
            onMouseLeave={() => onHoverCellDetailsChange(null)}
            onPointerDown={() => onRangeSelectionStart(column)}
            onPointerEnter={() => onRangeSelectionExtend(column)}
            style={{
              backgroundColor,
              ...(textColor == null ? {} : { color: textColor }),
              width: cellWidth,
            }}
            type="button"
            role="gridcell"
            tabIndex={
              row != null && isFirstRenderedRow && column === columns[0]
                ? 0
                : -1
            }
          >
            {isReferenceMatch ? MATCH_PLACEHOLDER : symbol}
            {insertion == null ? null : (
              <sup className="pointer-events-none absolute top-0 right-0 z-[2] translate-x-1/2 -translate-y-1/2 rounded-full bg-purple-600 px-1 text-[7px] leading-3 text-white shadow-sm">
                <FormattedMessage
                  id="codex.filePreview.msa.insertionBadge"
                  defaultMessage="+{count, number}"
                  description="Badge that reports how many insertion residues are hidden after an MSA alignment column."
                  values={{ count: insertion.residues.length }}
                />
              </sup>
            )}
          </button>
        );
      })}
      <HorizontalSpacer
        width={
          Math.max(0, document.alignedLength - slice.columnEnd) * cellWidth
        }
      />
    </div>
  );
});

function HorizontalSpacer({
  width,
}: {
  width: number;
}): React.ReactElement | null {
  if (width <= 0) {
    return null;
  }
  return <span aria-hidden="true" className="shrink-0" style={{ width }} />;
}

function isColumnWithinRange(
  range: MsaColumnRange | null,
  column: number,
): boolean {
  return range != null && column >= range.start && column < range.end;
}

function getRnaPairBorderClass(
  pair: MsaDerivedAnalysis["pairByColumn"][string],
): string | false {
  if (pair == null) {
    return false;
  }
  if ((pair.pseudoknotLevel ?? 0) > 0) {
    return "border-fuchsia-400";
  }
  if (pair.pairClass === "wobble") {
    return "border-cyan-300";
  }
  return "border-purple-300";
}

function getMatrixCellBackground({
  colorMode,
  column,
  document,
  referenceSymbol,
  residuePalette,
  summaries,
  symbol,
}: {
  colorMode: MsaColorMode;
  column: number;
  document: MsaDocument;
  referenceSymbol?: string;
  residuePalette: MsaResiduePalette | null;
  summaries: Array<MsaColumnSummary>;
  symbol: string;
}): string {
  if (
    (colorMode === "identity" || colorMode === "protein-conservation") &&
    summaries.length === 0
  ) {
    return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
  }
  return getMsaCellBackground({
    columnSummary: summaries[column] ?? {
      conservationModel: null,
      conservationNormalized: null,
      gapFraction: 0,
      identity: 0,
      nucleotideInformationContentBits: null,
      nongapCount: 0,
      proteinRelativeEntropyBits: null,
      symbolCounts: {},
      weightedSupport: 0,
      weightedSymbolFractions: {},
    },
    document,
    mode: colorMode,
    palette: residuePalette,
    ...(referenceSymbol == null ? {} : { referenceSymbol }),
    symbol,
  });
}

function getCodingImpactBackground({
  column,
  referenceSequence,
  rowSequence,
  symbol,
}: {
  column: number;
  referenceSequence: string | null;
  rowSequence: string;
  symbol: string;
}): string {
  if (referenceSequence == null) {
    return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
  }
  const codonStart = column - (column % 3);
  const anchorCodon = referenceSequence.slice(codonStart, codonStart + 3);
  const candidateCodon = rowSequence.slice(codonStart, codonStart + 3);
  if (candidateCodon.toUpperCase() === anchorCodon.toUpperCase()) {
    return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
  }
  const nonsynonymous = isNonsynonymousCodonDifference({
    anchorCodon,
    candidateCodon,
  });
  if (nonsynonymous == null) {
    return DIFFERENCE_MISMATCH_COLOR;
  }
  return nonsynonymous ? CODING_NONSYNONYMOUS_COLOR : CODING_SYNONYMOUS_COLOR;
}

function publishActiveCellDetails({
  analysis,
  column,
  document,
  insertion,
  onFocusedCellChange,
  onHoverCellDetailsChange,
  publishFocus,
  projection,
  referenceSymbol,
  row,
  showRnaStructureOverlays,
  symbol,
  target,
}: {
  analysis: MsaDerivedAnalysis | null;
  column: number;
  document: MsaDocument;
  insertion: MsaInsertionRun | undefined;
  onFocusedCellChange: (cell: FocusedMsaCell) => void;
  onHoverCellDetailsChange: (details: MsaHoverCellDetails) => void;
  publishFocus: boolean;
  projection: MsaRowProjection | undefined;
  referenceSymbol?: string;
  row: MsaSequenceRow | null;
  showRnaStructureOverlays: boolean;
  symbol: string;
  target: HTMLElement;
}): void {
  if (row == null) {
    return;
  }
  if (publishFocus) onFocusedCellChange({ column, row, symbol });
  onHoverCellDetailsChange(
    buildHoverCellDetails(
      formatCellTitle({
        analysis,
        column,
        document,
        insertionResidues: insertion?.residues,
        projection,
        referenceSymbol,
        row,
        showRnaStructureOverlays,
        symbol,
      }),
      target,
    ),
  );
}

function formatCellTitle({
  analysis,
  column,
  document,
  insertionResidues,
  projection,
  referenceSymbol,
  row,
  showRnaStructureOverlays,
  symbol,
}: {
  analysis: MsaDerivedAnalysis | null;
  column: number;
  document: MsaDocument;
  insertionResidues?: string;
  projection: MsaRowProjection | undefined;
  referenceSymbol?: string;
  row: MsaSequenceRow;
  showRnaStructureOverlays: boolean;
  symbol: string;
}): string {
  const parts = [row.label, `Column ${column + 1}`, `Symbol ${symbol}`];
  const ungapped =
    getUngappedPositionFromProjection(projection, column) ??
    getUngappedPosition(row.alignedSequence, column);
  if (ungapped != null) {
    parts.push(`Ungapped position ${ungapped}`);
  }
  if (referenceSymbol != null) {
    parts.push(`Reference ${referenceSymbol}`);
  }
  if (insertionResidues != null) {
    parts.push(`Insertion ${insertionResidues}`);
  }
  if (showRnaStructureOverlays) {
    const pair = analysis?.pairByColumn[String(column)];
    if (pair != null) {
      const partner =
        pair.leftColumn === column ? pair.rightColumn : pair.leftColumn;
      parts.push(`RNA pair partner column ${partner + 1}`);
    } else if (document.rnaStructure != null && analysis == null) {
      parts.push("RNA pair details computing");
    }
  }
  return parts.join(" · ");
}

function indexInsertions(
  insertions: Array<MsaInsertionRun>,
): Record<string, Record<string, MsaInsertionRun | undefined> | undefined> {
  const byRow: Record<
    string,
    Record<string, MsaInsertionRun | undefined> | undefined
  > = {};
  for (const insertion of insertions) {
    const rowInsertions = byRow[insertion.rowId] ?? {};
    rowInsertions[String(insertion.afterAlignmentColumn)] = insertion;
    byRow[insertion.rowId] = rowInsertions;
  }
  return byRow;
}

function indexHitRanges(
  hits: Array<MsaMotifSearchHit>,
  selectedHit: MsaMotifSearchHit | null,
): Map<string, Array<HitRange>> {
  const ranges = new Map<string, Array<HitRange>>();
  for (const hit of hits) {
    const rowRanges = ranges.get(hit.rowId) ?? [];
    rowRanges.push({
      end: hit.alignmentEndColumn,
      selected:
        selectedHit != null &&
        selectedHit.rowId === hit.rowId &&
        selectedHit.alignmentStartColumn === hit.alignmentStartColumn &&
        selectedHit.alignmentEndColumn === hit.alignmentEndColumn &&
        selectedHit.orientation === hit.orientation,
      start: hit.alignmentStartColumn,
    });
    ranges.set(hit.rowId, rowRanges);
  }
  for (const rowRanges of ranges.values()) {
    rowRanges.sort((left, right) => left.start - right.start);
  }
  return ranges;
}

function getHitState(
  ranges: Array<HitRange>,
  column: number,
): { any: boolean; selected: boolean } {
  for (const range of ranges) {
    if (range.start > column) {
      break;
    }
    if (column <= range.end) {
      return { any: true, selected: range.selected };
    }
  }
  return { any: false, selected: false };
}

SHA-256: e146a8882d9f0347cd9ebdfc24830a7a9569b9410875ff8b9ab8acdd0b84f74d