import { html } from "lit"; import { property } from "lit/decorators.js"; import { axisBottom, select, Selection } from "d3"; import NightingaleElement, { withDimensions, withPosition, withMargin, withResizable, withHighlight, withManager, withZoom, bindEvents, customElementOnce, } from "@nightingale-elements/nightingale-new-core"; const DEFAULT_NUMBER_OF_TICKS = 3; export type SequenceBaseType = { position: number; aa: string }; @customElementOnce("nightingale-sequence") class NightingaleSequence extends withManager( withZoom( withResizable( withMargin( withPosition(withDimensions(withHighlight(NightingaleElement))), ), ), ), ) { @property({ type: String }) sequence?: string | null; #seq_bg?: Selection< SVGGElement, SequenceBaseType | unknown, HTMLElement | SVGElement | null, unknown >; #axis?: Selection< SVGGElement, unknown, HTMLElement | SVGElement | null, unknown >; protected seq_g?: Selection< SVGGElement, unknown, HTMLElement | SVGElement | null, unknown >; protected highlighted?: Selection< SVGGElement, unknown, HTMLElement | SVGElement | null, unknown >; margins?: Selection< SVGGElement, unknown, HTMLElement | SVGElement | null, unknown >; #bases?: Selection< SVGTextElement, SequenceBaseType, SVGElement | null, unknown >; numberOfTicks?: number; chWidth?: number; chHeight?: number; override connectedCallback() { super.connectedCallback(); const ticks = parseInt(this.getAttribute("numberofticks") || "", 10); this.numberOfTicks = Number.isInteger(ticks) ? ticks : DEFAULT_NUMBER_OF_TICKS; this.addEventListener("load", (e: Event) => { this.data = (e as CustomEvent).detail.payload; }); } get data() { return this.sequence || ""; } set data(data: string | Record) { if (typeof data === "string") this.sequence = data; else if (typeof data?.sequence === "string") this.sequence = data.sequence; if (this.svg) { this.updateScaleDomain(); this.applyZoomTranslation(); } } protected getCharSize() { if (!this.seq_g) return; const xratio = 0.8; const yratio = 1.6; const node = this.seq_g.select("text.base").node(); if (node) { this.chWidth = node.getBBox().width * xratio; this.chHeight = node.getBBox().height * yratio; } else { // Add a dummy node to measure the width const tempNode = this.seq_g .append("text") .attr("class", "base") .text("T"); this.chWidth = (tempNode.node()?.getBBox().width || 0) * xratio; this.chHeight = (tempNode.node()?.getBBox().height || 0) * yratio; tempNode.remove(); } } protected createSequence() { this.svg = select(this as unknown as NightingaleElement) .selectAll("svg") .attr("id", "") .attr("width", this.width) .attr("height", this.height); this.#seq_bg = this.svg?.append("g").attr("class", "background"); this.#axis = this.svg?.append("g").attr("class", "x axis"); this.seq_g = this.svg ?.append("g") .attr("class", "sequence") .attr( "transform", `translate(0,${ this["margin-top"] + 0.75 * this.getHeightWithMargins() })`, ); this.highlighted = this.svg.append("g").attr("class", "highlighted"); this.margins = this.svg.append("g").attr("class", "margin"); if (this.sequence) { this.updateScaleDomain(); this.applyZoomTranslation(); } } override firstUpdated() { this.createSequence(); } override zoomRefreshed() { this.renderD3(); } renderD3() { this.getCharSize(); this.svg?.attr("width", this.width).attr("height", this.height); if (this.#axis) { const ftWidth = this.getSingleBaseWidth(); const space = ftWidth - (this.chWidth || 0); const half = ftWidth / 2; const first = Math.floor(Math.max(0, this.getStart() - 1)); const last = Math.ceil( Math.min(this.sequence?.length || 0, this.getEnd()), ); const bases: Array = space < 0 ? [] : this.sequence ?.slice(first, last) .split("") .map((aa, i) => ({ position: 1 + first + i, aa, })) || []; // only add axis if there is room if (this.height > (this.chWidth || 0) && this.xScale) { const xAxis = axisBottom(this.xScale) .tickFormat((d) => `${Number.isInteger(d) ? d : ""}`) .ticks(this.numberOfTicks, "s"); this.#axis.call(xAxis); } this.#axis.attr( "transform", `translate(${half},${this["margin-top"]})`, ); this.#axis.select(".domain").remove(); this.#axis.selectAll(".tick line").remove(); this.#axis.selectAll(".tick text").attr("y", 2); const size = Math.max( 10, this.chWidth || 10, Math.min( this["margin-top"] + 0.25 * this.getHeightWithMargins(), ftWidth - 2, ), ); this.#axis.selectAll(".tick text").attr("font-size", size); if (this.seq_g) { this.seq_g.attr( "transform", `translate(0,${ this["margin-top"] + 0.75 * this.getHeightWithMargins() })`, ); this.#bases = this.seq_g.selectAll("text.base"); const textElements = this.#bases.data( bases, (d) => (d as SequenceBaseType).position, ); textElements .enter() .append("text") .attr("class", "base") .attr("text-anchor", "middle") .attr("x", (d) => this.getXFromSeqPosition(d.position) + half) .text((d) => d.aa) .attr("font-size", size) .style("pointer-events", "none") .style("font-family", "monospace"); textElements.exit().remove(); textElements .attr("font-size", size) .attr("x", (d) => this.getXFromSeqPosition(d.position) + half); if (this.#seq_bg) { const background = this.#seq_bg .selectAll("rect.base_bg") .data(bases, (d) => (d as SequenceBaseType).position); background .enter() .append("rect") .attr("class", "base_bg feature") .attr("height", this.getHeightWithMargins()) .attr("width", ftWidth) .attr("fill", (d) => (Math.round(d.position) % 2 ? "#ccc" : "#eee")) .attr("x", (d) => this.getXFromSeqPosition(d.position)) .attr("y", this["margin-top"]) .style("opacity", Math.min(1, space)) .call(bindEvents, this); background .attr("width", ftWidth) .attr("fill", (d) => (Math.round(d.position) % 2 ? "#ccc" : "#eee")) .attr("height", this.getHeightWithMargins()) .attr("x", (d) => this.getXFromSeqPosition(d.position)) .attr("y", this["margin-top"]); background.exit().remove(); this.seq_g.style("opacity", Math.min(1, space)); background.style("opacity", Math.min(1, space)); } } this.updateHighlight(); this.renderMarginOnGroup(this.margins); } } protected getStart(): number { return this["display-start"] || 1; } protected getEnd(): number { return ( ((this["display-end"] || 0) > 0 ? this["display-end"] : this.length) || 0 ); } protected updateHighlight() { if (!this.highlighted) return; const highlighs = this.highlighted .selectAll< SVGRectElement, { start: number; end: number; }[] >("rect") .data(this.highlightedRegion.segments); highlighs .enter() .append("rect") .style("pointer-events", "none") .merge(highlighs) .attr("fill", this["highlight-color"]) .attr("height", this.height) .attr("x", (d) => this.getXFromSeqPosition(d.start)) .attr("width", (d) => Math.max(0, this.getSingleBaseWidth() * (d.end - d.start + 1)), ); highlighs.exit().remove(); } override render() { return html``; } } export default NightingaleSequence;