UNPKG

seqviz-plus

Version:
998 lines (966 loc) 365 kB
/*! * seqviz-plus - 2.0.26 * provided and maintained by Lattice Automation (https://latticeautomation.com/) * LICENSE MIT */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("seqviz-plus", [], factory); else if(typeof exports === 'object') exports["seqviz-plus"] = factory(); else root["seqviz-plus"] = factory(); })(this, () => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ([ /* 0 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Viewer = exports.Enzymes = exports.SeqViz = void 0; var React = __webpack_require__(1); var ReactDOM = __webpack_require__(2); var server_1 = __webpack_require__(3); var SeqViz_1 = __webpack_require__(4); exports.SeqViz = SeqViz_1.default; __webpack_require__(44); var enzymes_1 = __webpack_require__(42); exports.Enzymes = enzymes_1.default; exports["default"] = SeqViz_1.default; /** * Return a Viewer object with three properties: * - `render` to an HTML element * - `setState(options)` to update the viewer's internal state * - `renderToString` to return an HTML representation of the Viewer */ var Viewer = function (element, options) { if (element === void 0) { element = "root"; } // used to keep track of whether to re-render after a "set" call var rendered = false; // get the HTML element by ID or use as is if passed directly var domElement; if (!document) return; if (typeof element === "string") { if (document.getElementById(element)) { domElement = document.getElementById(element); } else { throw new Error("Failed to find an element with ID: ".concat(element)); } } else { domElement = element; } var viewer = React.createElement(SeqViz_1.default, options, null); /** * Render the Viewer to the element passed */ var render = function () { rendered = true; ReactDOM.render(viewer, domElement); return viewer; }; /** * Return an HTML string representation of the viewer */ var renderToString = function () { return (0, server_1.renderToString)(viewer); }; /** * Update the viewer with new settings. Re-renders if render was already called. */ var setState = function (state) { options = __assign(__assign({}, options), state); viewer = React.createElement(SeqViz_1.default, options, null); if (rendered) { ReactDOM.render(viewer, domElement); } return viewer; }; return { render: render, renderToString: renderToString, setState: setState, }; }; exports.Viewer = Viewer; /***/ }), /* 1 */ /***/ ((module) => { module.exports = require("react"); /***/ }), /* 2 */ /***/ ((module) => { module.exports = require("react-dom"); /***/ }), /* 3 */ /***/ ((module) => { module.exports = require("react-dom/server"); /***/ }), /* 4 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", ({ value: true })); var React = __webpack_require__(1); var seqparse_1 = __webpack_require__(5); var SeqViewerContainer_1 = __webpack_require__(6); var colors_1 = __webpack_require__(13); var digest_1 = __webpack_require__(41); var isEqual_1 = __webpack_require__(11); var search_1 = __webpack_require__(43); var sequence_1 = __webpack_require__(24); /** * SeqViz is a viewer for rendering sequences in a linear and/or circular viewer. */ var SeqViz = /** @class */ (function (_super) { __extends(SeqViz, _super); function SeqViz(props) { var _this = _super.call(this, props) || this; /** * Re-parse props to state if there are changes to: * - seq/accession/file (this probably means we need to update the rest) * - search input changes * - enzymes change * - annotations * * This is needed for the parse(accession) call that makes an async fetch to a remote repository * https://reactjs.org/docs/react-component.html#componentdidupdate */ _this.componentDidUpdate = function ( // previous props _a, // previous state _b) { var _c = _a.accession, accession = _c === void 0 ? "" : _c, annotations = _a.annotations, enzymes = _a.enzymes, enzymesCustom = _a.enzymesCustom, file = _a.file, search = _a.search; var seq = _b.seq, seqType = _b.seqType; // New accession or file provided, fetch and/or parse. if (accession !== _this.props.accession || file !== _this.props.file || (_this.props.seq && _this.props.seq !== seq)) { var input = _this.parseInput(); _this.setState(__assign(__assign({ annotations: input.annotations, compSeq: input.compSeq, name: input.name, seq: input.seq, seqType: input.seqType }, _this.search(_this.props, input.seq)), _this.cut(input.seq, input.seqType))); return; } // New search parameters provided. if (search && (!_this.props.search || search.query !== _this.props.search.query || search.mismatch !== _this.props.search.mismatch)) { _this.setState(_this.search(_this.props, seq)); // new search parameters } // New digest parameters. if (!(0, isEqual_1.isEqual)(enzymes, _this.props.enzymes) || !(0, isEqual_1.isEqual)(enzymesCustom, _this.props.enzymesCustom)) { _this.setState(_this.cut(seq, seqType)); } // New annotations provided. if (!(0, isEqual_1.isEqual)(annotations, _this.props.annotations)) { _this.setState({ annotations: _this.parseAnnotations(_this.props.annotations, _this.props.seq), }); } }; /** * If a file is provided or a sequence is provided, parse it and its annotations. * If an accession is provided, query a remote repository and parse the sequence and annotations. */ _this.parseInput = function (props) { var _a = props || _this.props, annotations = _a.annotations, compSeq = _a.compSeq, file = _a.file, _b = _a.name, name = _b === void 0 ? "" : _b, seq = _a.seq; if (file) { // Parse a sequence file var parseOptions = {}; if (file && file instanceof File) { parseOptions.fileName = file.name; } var parsed = (0, seqparse_1.parseFile)(file.toString(), parseOptions); if (parsed.length) { var seqType = (0, sequence_1.guessType)(parsed[0].seq); return { annotations: _this.parseAnnotations(parsed[0].annotations, parsed[0].seq), compSeq: (0, sequence_1.complement)(parsed[0].seq, seqType).compSeq, name: parsed[0].name, seq: parsed[0].seq, seqType: seqType, }; } } else if (seq) { // Fill in default props just using the seq var seqType = (0, sequence_1.guessType)(seq); return { annotations: _this.parseAnnotations(annotations, seq), compSeq: compSeq || (0, sequence_1.complement)(seq, seqType).compSeq, name: name, seq: seq, seqType: seqType, }; } return { annotations: [], compSeq: "", name: "", seq: "", seqType: "dna", }; }; /** * Search for the query sequence in the part sequence, set in state. */ _this.search = function (props, seq) { var onSearch = props.onSearch, searchProp = props.search, seqType = props.seqType; if (!searchProp || !seq || !seq.length) { return { search: [] }; } var results = (0, search_1.default)(searchProp.query, searchProp.mismatch, seq, seqType || (0, sequence_1.guessType)(seq)); if (_this.state && (0, isEqual_1.isEqual)(results, _this.state.search)) { return { search: _this.state.search }; } onSearch && onSearch(results); return { search: results }; }; /** * Find and save enzymes' cut-site locations. */ _this.cut = function (seq, seqType) { return ({ cutSites: (0, digest_1.default)(seq || "", seqType, _this.props.enzymes || [], _this.props.enzymesCustom || {}), }); }; /** * Fix annotations to add unique ids, fix directionality, and modulo the start and end of each. */ _this.parseAnnotations = function (annotations, seq) { if (annotations === void 0) { annotations = null; } if (seq === void 0) { seq = ""; } return (annotations || []).map(function (a, i) { return (__assign(__assign({ id: (0, sequence_1.randomID)() }, a), { color: a.color || (0, colors_1.colorByIndex)(i, colors_1.COLORS), direction: (0, sequence_1.directionality)(a.direction), end: a.end % (seq.length + 1), start: a.start % (seq.length + 1) })); }); }; var seq = _this.parseInput(props); _this.state = __assign(__assign(__assign({}, seq), _this.search(props, seq.seq)), _this.cut(seq.seq, seq.seqType)); return _this; } /** * If an accession was provided, query it here. */ SeqViz.prototype.componentDidMount = function () { var _this = this; var accession = this.props.accession; if (!accession || !accession.length) { return; } // Query an accession to a sequence (0, seqparse_1.default)(accession, { cors: true }).then(function (parsed) { var seqType = (0, sequence_1.guessType)(parsed.seq); _this.setState(__assign(__assign({ annotations: _this.parseAnnotations(parsed.annotations, parsed.seq), compSeq: (0, sequence_1.complement)(parsed.seq, seqType).compSeq, name: parsed.name, seq: parsed.seq, seqType: seqType }, _this.search(_this.props, parsed.seq)), _this.cut(parsed.seq, seqType))); }); }; /** Log caught errors. */ SeqViz.prototype.componentDidCatch = function (error, errorInfo) { console.error("Error caught in SeqViz: %v %v", error, errorInfo); }; SeqViz.prototype.render = function () { var _this = this; var _a = this.props, highlightedRegions = _a.highlightedRegions, highlights = _a.highlights, showTranslations = _a.showTranslations, showComplement = _a.showComplement, showIndex = _a.showIndex, style = _a.style, zoom = _a.zoom; var translations = this.props.translations; var _b = this.state, compSeq = _b.compSeq, seq = _b.seq, seqType = _b.seqType; // This is an unfortunate bit of seq checking. We could get a seq directly or from a file parsed to a part. if (!seq) return React.createElement("div", { className: "la-vz-seqviz" }); // If the seqType is aa, make the entire sequence the "translation" if (seqType === "aa" || (showTranslations && (translations === null || translations === void 0 ? void 0 : translations.length) === 0)) { // TODO: during some grand future refactor, make this cleaner and more transparent to the user translations = [{ direction: 1, end: seq.length, start: 0 }]; } // Since all the props are optional, we need to parse them to defaults. var props = { // translations: (colorized && translations?.length === 0) ? [{start:0, end: seq?.length, direction: 1}] : translations, bpColors: this.props.bpColors || {}, copyEvent: this.props.copyEvent || (function () { return false; }), cutSites: this.state.cutSites, highlights: (highlights || []).concat(highlightedRegions || []).map(function (h, i) { return (__assign(__assign({}, h), { direction: 1, end: h.end % (seq.length + 1), id: "highlight-".concat(i, "-").concat(h.start, "-").concat(h.end), name: "", start: h.start % (seq.length + 1) })); }), onSelection: (function (selection) { // @ts-ignore _this.setState({ selection: selection }); }), rotateOnScroll: !!this.props.rotateOnScroll, showComplement: (!!compSeq && (typeof showComplement !== "undefined" ? showComplement : true)) || false, showIndex: !!showIndex, translations: (translations || []).map(function (t) { return ({ direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, // end: viewer !== 'alignment' ? t.start + Math.floor((t.end - t.start) / 3) * 3 : t.end, end: t.end, start: t.start % seq.length, }); }), viewer: this.props.viewer || "both", zoom: { circular: typeof (zoom === null || zoom === void 0 ? void 0 : zoom.circular) == "number" ? Math.min(Math.max(zoom.circular, 0), 100) : 0, linear: typeof (zoom === null || zoom === void 0 ? void 0 : zoom.linear) == "number" ? Math.min(Math.max(zoom.linear, 0), 100) : 50, }, }; return (React.createElement("div", { className: "la-vz-seqviz", "data-testid": "la-vz-seqviz", style: style }, React.createElement(SeqViewerContainer_1.default, __assign({}, this.props, props, this.state)))); }; SeqViz.defaultProps = { accession: "", annotations: [], backbone: "", bpColors: {}, colors: [], compSeq: "", copyEvent: function (e) { return e.key === "c" && (e.metaKey || e.ctrlKey); }, enzymes: [], enzymesCustom: {}, name: "", nameToCompare: "", onSearch: function (_) { return null; }, onSelection: function (_) { return null; }, rotateOnScroll: true, search: { mismatch: 0, query: "" }, seq: "", seqToCompare: "", colorized: true, aagrouping: true, showComplement: true, showTranslations: false, showDetails: true, showIndex: true, style: {}, translations: [], viewer: "both", zoom: { circular: 0, linear: 50 }, }; return SeqViz; }(React.Component)); exports["default"] = SeqViz; /***/ }), /* 5 */ /***/ ((module) => { module.exports = require("seqparse"); /***/ }), /* 6 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.CHAR_WIDTH = void 0; var React = __webpack_require__(1); var react_resize_detector_1 = __webpack_require__(7); var Circular_1 = __webpack_require__(8); var EventHandler_1 = __webpack_require__(22); var MultipleEventHandler_1 = __webpack_require__(26); var Linear_1 = __webpack_require__(28); var SelectionHandler_1 = __webpack_require__(38); var MultipleSequenceSelectionHandler_1 = __webpack_require__(39); var centralIndexContext_1 = __webpack_require__(9); var isEqual_1 = __webpack_require__(11); var selectionContext_1 = __webpack_require__(21); var Alignment_1 = __webpack_require__(40); /** * This is the width in pixels of a character that's 12px * This will need to change whenever the css of the plasmid viewer text changes * just divide the width of some rectangular text by it's number of characters */ exports.CHAR_WIDTH = 7.2; /** * a parent sequence viewer component that holds whatever is common between * the linear and circular sequence viewers. The Header is an example */ var SeqViewerContainer = /** @class */ (function (_super) { __extends(SeqViewerContainer, _super); function SeqViewerContainer(props) { var _this = _super.call(this, props) || this; /** this is here because the size listener is returning a new "size" prop every time */ _this.shouldComponentUpdate = function (nextProps, nextState) { return !(0, isEqual_1.isEqual)(nextProps, _this.props) || !(0, isEqual_1.isEqual)(nextState, _this.state); }; /** * Update the central index of the linear or circular viewer. */ _this.setCentralIndex = function (type, value) { var _a; if (type !== "LINEAR" && type !== "CIRCULAR") { throw new Error("Unknown central index type: ".concat(type)); } if (_this.state.centralIndex[type.toLowerCase()] === value) { return; // nothing changed } _this.setState({ centralIndex: __assign(__assign({}, _this.state.centralIndex), (_a = {}, _a[type.toLowerCase()] = value, _a)) }); }; /** * Update selection in state. Should only be performed from handlers/selection.jsx */ _this.setSelection = function (selection) { // If the user passed a selection, do not update our state here var parent = selection.parent, ref = selection.ref, rest = __rest(selection, ["parent", "ref"]); if (!_this.props.selection) _this.setState({ selection: selection }); if (_this.props.onSelection) _this.props.onSelection(rest); }; /** * Returns the selection that was either a prop (optional) or the selection maintained in state. */ _this.getSelection = function (state, prop) { if (prop) { return __assign(__assign({}, prop), { clockwise: typeof prop.clockwise === "undefined" || !!prop.clockwise, type: "" }); } return state; }; /** * given the width of the screen, and the current zoom, how many basepairs should be displayed * on the screen at a given time and what should their size be */ _this.linearProps = function () { var _a = _this.props, seq = _a.seq, seqType = _a.seqType, viewer = _a.viewer, colorized = _a.colorized, aagrouping = _a.aagrouping; var size = _this.props.testSize || { height: _this.props.height, width: _this.props.width }; var zoom = _this.props.zoom.linear; // hack if (viewer.includes("both")) { size.width /= 2; } var seqFontSize = Math.min(Math.round(zoom * 0.1 + 9.5), 18); // max 18px // otherwise the sequence needs to be cut into smaller subsequences // a sliding scale in width related to the degree of zoom currently active var bpsPerBlock = Math.round((size.width / seqFontSize) * 1.4) || 1; // width / 1 * seqFontSize if (seqType === "aa" && (colorized) && !aagrouping) { bpsPerBlock = Math.round(bpsPerBlock / 3); // more space for each amino acid } if (zoom <= 5) { bpsPerBlock *= 3; } else if (zoom <= 10) { // really ramp up the range, since at this zoom it'll just be a line bpsPerBlock *= 2; } else if (zoom > 70) { // keep font height the same but scale number of bps in one row bpsPerBlock = Math.round(bpsPerBlock * (70 / zoom)); } bpsPerBlock = Math.max(1, bpsPerBlock); if (size.width && bpsPerBlock < seq.length) { size.width -= 28; // -28 px for the padding (10px) + scroll bar (18px) } var charWidth = size.width / bpsPerBlock; // width of each basepair var lineHeight = 1.4 * seqFontSize; // aspect ratio is 1.4 for roboto mono var elementHeight = 16; // the height, in pixels, of annotations, ORFs, etc return __assign(__assign({}, _this.props), { bpsPerBlock: bpsPerBlock, charWidth: charWidth, elementHeight: elementHeight, lineHeight: lineHeight, seqFontSize: seqFontSize, viewer: viewer, size: size, zoom: { linear: zoom } }); }; _this.alignmentProps = function () { var _a = _this.props, seq = _a.seq, seqType = _a.seqType, colorized = _a.colorized, aagrouping = _a.aagrouping, viewer = _a.viewer; var size = _this.props.testSize || { height: _this.props.height, width: _this.props.width }; var zoom = _this.props.zoom.linear; var seqFontSize = Math.min(Math.round(zoom * 0.1 + 9.5), 18); // max 18px // otherwise the sequence needs to be cut into smaller subsequences // a sliding scale in width related to the degree of zoom currently active var bpsPerBlock = Math.round((size.width / seqFontSize) * 1.4) || 1; // width / 1 * seqFontSize if (seqType === "aa" && colorized && !aagrouping) { bpsPerBlock = Math.round(bpsPerBlock / 3); // more space for each amino acid } if (zoom <= 5) { bpsPerBlock *= 3; } else if (zoom <= 10) { // really ramp up the range, since at this zoom it'll just be a line bpsPerBlock *= 2; } else if (zoom > 70) { // keep font height the same but scale number of bps in one row bpsPerBlock = Math.round(bpsPerBlock * (70 / zoom)); } bpsPerBlock = Math.max(1, bpsPerBlock); if (size.width && bpsPerBlock < seq.length) { size.width -= 28; // -28 px for the padding (10px) + scroll bar (18px) } var charWidth = size.width / bpsPerBlock; // width of each basepair var lineHeight = 1.4 * seqFontSize; // aspect ratio is 1.4 for roboto mono var elementHeight = 16; // the height, in pixels, of annotations, ORFs, etc return __assign(__assign({}, _this.props), { bpsPerBlock: bpsPerBlock, charWidth: charWidth, viewer: viewer, elementHeight: elementHeight, lineHeight: lineHeight, seqFontSize: seqFontSize, size: size, zoom: { linear: zoom } }); }; /** * given the length of the sequence and the dimensions of the viewbox, how should * zoom of the plasmid viewer affect the radius of the circular viewer and its vertical shift * * minPixelPerBP = s / 50 where * s = theta * radius where * radius = h / 2 + c ^ 2 / 8 h (https://en.wikipedia.org/wiki/Circular_segment) * and theta = 50 / seqLength */ _this.circularProps = function () { var _a = _this.props, seqLength = _a.seq.length, viewer = _a.viewer; var size = _this.props.testSize || { height: _this.props.height, width: _this.props.width }; var zoom = _this.props.zoom.circular; // hack if (viewer.includes("both")) { size.width /= 2; } var center = { x: size.width / 2, y: size.height / 2, }; var limitingDim = Math.min(size.height, size.width); var exp = 0.83; // exponent... greater exp leads to flatter curve (c in fig) var beta = Math.exp(Math.log(50 / seqLength) / -(Math.pow(100, exp))); // beta coefficient (b in fig) var bpsOnArc = seqLength * beta; // calc using the full expression // scale the radius so only (bpsOnArc) many bps are shown var radius = limitingDim * 0.34; return __assign(__assign({}, _this.props), { bpsOnArc: bpsOnArc, center: center, radius: radius === 0 ? 1 : radius, size: size, yDiff: 0, zoom: { circular: zoom } }); }; _this.state = { centralIndex: { circular: 0, linear: 0, setCentralIndex: _this.setCentralIndex, }, selection: _this.getSelection(selectionContext_1.defaultSelection, props.selection), }; return _this; } SeqViewerContainer.prototype.render = function () { var _this = this; var _a = this.props, selectionProp = _a.selection, seq = _a.seq, viewer = _a.viewer, seqToCompare = _a.seqToCompare, showDetails = _a.showDetails; var _b = this.state, centralIndex = _b.centralIndex, selection = _b.selection; var linearProps = this.linearProps(); var circularProps = this.circularProps(); var alignmentProps = this.alignmentProps(); return (React.createElement("div", { ref: this.props.targetRef, className: "la-vz-viewer-container", "data-testid": "la-vz-viewer-container" }, React.createElement(centralIndexContext_1.default.Provider, { value: centralIndex }, React.createElement(selectionContext_1.default.Provider, { value: this.getSelection(selection, selectionProp) }, viewer !== 'alignment' && React.createElement(SelectionHandler_1.default, { bpsPerBlock: linearProps.bpsPerBlock, center: circularProps.center, centralIndex: centralIndex.circular, seq: seq, setCentralIndex: this.setCentralIndex, setSelection: this.setSelection, yDiff: circularProps.yDiff }, function (inputRef, handleMouseEvent, onUnmount) { return (React.createElement(EventHandler_1.EventHandler, { bpsPerBlock: linearProps.bpsPerBlock, copyEvent: _this.props.copyEvent, handleMouseEvent: handleMouseEvent, selection: selection, seq: seq, setSelection: _this.setSelection }, viewer === "linear" && (React.createElement(Linear_1.default, __assign({}, linearProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount }))), viewer === "circular" && (React.createElement(Circular_1.default, __assign({}, circularProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount }))), viewer === "both" && (React.createElement(React.Fragment, null, React.createElement(Circular_1.default, __assign({}, circularProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount })), React.createElement(Linear_1.default, __assign({}, linearProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount })))), viewer === "both_flip" && (React.createElement(React.Fragment, null, React.createElement(Linear_1.default, __assign({}, linearProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount })), React.createElement(Circular_1.default, __assign({}, circularProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount })))))); }), viewer === 'alignment' && React.createElement(React.Fragment, null, React.createElement(MultipleSequenceSelectionHandler_1.default, { bpsPerBlock: linearProps.bpsPerBlock, center: circularProps.center, centralIndex: centralIndex.circular, seq: [seq, seqToCompare], setCentralIndex: this.setCentralIndex, setSelection: this.setSelection, yDiff: circularProps.yDiff }, function (inputRef, handleMouseEvent, onUnmount) { return (React.createElement(MultipleEventHandler_1.MultipleEventHandler, { bpsPerBlock: linearProps.bpsPerBlock, copyEvent: _this.props.copyEvent, aagrouping: _this.props.aagrouping, name: _this.props.name, nameToCompare: _this.props.nameToCompare, handleMouseEvent: handleMouseEvent, selection: selection, seq: [seq, seqToCompare], showDetails: showDetails, setSelection: _this.setSelection }, React.createElement(Alignment_1.default, __assign({}, alignmentProps, { handleMouseEvent: handleMouseEvent, inputRef: inputRef, onUnmount: onUnmount })))); })))))); }; return SeqViewerContainer; }(React.Component)); exports["default"] = (0, react_resize_detector_1.withResizeDetector)(SeqViewerContainer); /***/ }), /* 7 */ /***/ ((module) => { module.exports = require("react-resize-detector"); /***/ }), /* 8 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Arc = exports.RENDER_SEQ_LENGTH_CUTOFF = void 0; var React = __webpack_require__(1); var SeqViewerContainer_1 = __webpack_require__(6); var centralIndexContext_1 = __webpack_require__(9); var elementsToRows_1 = __webpack_require__(10); var isEqual_1 = __webpack_require__(11); var Annotations_1 = __webpack_require__(12); var CutSites_1 = __webpack_require__(15); var Find_1 = __webpack_require__(16); var Index_1 = __webpack_require__(17); var Labels_1 = __webpack_require__(18); var Selection_1 = __webpack_require__(20); /** Sequence length cutoff below which the circular viewer's sequence won't be rendered. */ exports.RENDER_SEQ_LENGTH_CUTOFF = 250; /** Circular is a circular viewer that contains a bunch of arcs. */ var Circular = /** @class */ (function (_super) { __extends(Circular, _super); function Circular(props) { var _this = _super.call(this, props) || this; /** * Deep equality comparison */ _this.shouldComponentUpdate = function (nextProps) { return !(0, isEqual_1.isEqual)(nextProps, _this.props); }; /** * Return the SVG rotation transformation needed to put a child element in the * correct location around the plasmid. This func makes use of the centralIndex field in parent state * to rotate the plasmid viewer. */ _this.getRotation = function (index) { var center = _this.props.center; var seqLength = _this.state.seqLength; var centralIndex = _this.context.circular; // how many degrees should it be rotated? var adjustedIndex = index - centralIndex; var startPerc = adjustedIndex / seqLength; var degrees = startPerc * 360; return "rotate(".concat(degrees || 0, ", ").concat(center.x, ", ").concat(center.y, ")"); }; /** * Given an index along the plasmid and its radius, find the coordinate * will be used in many of the child components * * In general, this is for lines and labels */ _this.findCoor = function (index, radius, rotate) { var center = _this.props.center; var seqLength = _this.state.seqLength; var rotatedIndex = rotate ? index - _this.context.circular : index; var lengthPerc = rotatedIndex / seqLength; var lengthPercCentered = lengthPerc - 0.25; var radians = lengthPercCentered * Math.PI * 2; var xAdjust = Math.cos(radians) * radius; var yAdjust = Math.sin(radians) * radius; return { x: center.x + xAdjust, y: center.y + yAdjust, }; }; /** * Given a coordinate, and the degrees to rotate it, find the new coordinate * (assuming that the rotation is around the center) * * in general this is for text and arcs */ _this.rotateCoor = function (coor, degrees) { var center = _this.props.center; // find coordinate's current angle var angle = degrees * (Math.PI / 180); // degrees to radians var cos = Math.cos(angle); var sin = Math.sin(angle); // find the new coordinate var xDiff = coor.x - center.x; var yDiff = coor.y - center.y; var cosX = cos * xDiff; var cosY = cos * yDiff; var sinX = sin * xDiff; var sinY = sin * yDiff; var xAdjust = cosX - sinY; var yAdjust = sinX + cosY; return { x: center.x + xAdjust, y: center.y + yAdjust, }; }; /** * Given an inner and outer radius, and the length of the element, return the * path for an arc that circles the plasmid. The optional paramters sweepFWD and sweepREV * are needed for selection arcs (where the direction of the arc isn't known beforehand) * and arrowFWD and arrowREV are needed for annotations, where there may be directionality */ _this.genArc = function (args) { var arrowFWD = args.arrowFWD, arrowREV = args.arrowREV, innerRadius = args.innerRadius, largeArc = args.largeArc, length = args.length, outerRadius = args.outerRadius, sweepFWD = args.sweepFWD; var radius = _this.props.radius; var _a = _this.state, lineHeight = _a.lineHeight, seqLength = _a.seqLength; var offset = args.offset === undefined ? 0 : args.offset; // build up the six default coordinates var leftBottom = _this.findCoor(offset, innerRadius); var leftTop = _this.findCoor(offset, outerRadius); var rightBottom = _this.findCoor(length + offset, innerRadius); var rightTop = _this.findCoor(length + offset, outerRadius); var leftArrow = ""; var rightArrow = ""; // create arrows by making a midpoint along edge and shifting corners inwards if (arrowREV || arrowFWD) { // one quarter of lineHeight in px is the shift inward for arrows var inwardShift = lineHeight / 4; // given the arc length (inwardShift) and the radius (from SeqViewer), // we can find the degrees to rotate the corners var centralAngle = inwardShift / radius; // Math.min here is to make sure the arrow it's larger than the element var centralAnglePerc = Math.min(centralAngle / 2, length / seqLength); var centralAngleDeg = centralAnglePerc * 360; if (arrowREV) { leftBottom = _this.rotateCoor(leftBottom, centralAngleDeg); leftTop = _this.rotateCoor(leftTop, centralAngleDeg); var lArrowC = _this.findCoor(0, (innerRadius + outerRadius) / 2); leftArrow = "L ".concat(lArrowC.x, " ").concat(lArrowC.y); } else { rightBottom = _this.rotateCoor(rightBottom, -centralAngleDeg); rightTop = _this.rotateCoor(rightTop, -centralAngleDeg); var rArrowC = _this.findCoor(length, (innerRadius + outerRadius) / 2); rightArrow = "L ".concat(rArrowC.x, " ").concat(rArrowC.y); } } var lArc = largeArc ? 1 : 0; var sFlagF = sweepFWD ? 1 : 0; var sFlagR = sweepFWD ? 0 : 1; return "M ".concat(rightBottom.x, " ").concat(rightBottom.y, "\n A ").concat(innerRadius, " ").concat(innerRadius, ", 0, ").concat(lArc, ", ").concat(sFlagR, ", ").concat(leftBottom.x, " ").concat(leftBottom.y, "\n L ").concat(leftBottom.x, " ").concat(leftBottom.y, "\n ").concat(leftArrow, "\n L ").concat(leftTop.x, " ").concat(leftTop.y, "\n A ").concat(outerRadius, " ").concat(outerRadius, ", 0, ").concat(lArc, ", ").concat(sFlagF, ", ").concat(rightTop.x, " ").concat(rightTop.y, "\n ").concat(rightArrow, "\n Z"); }; /** * handle a scroll event and, if it's a CIRCULAR viewer, update the * current central index */ _this.handleScrollEvent = function (e) { var _a = _this.props, rotateOnScroll = _a.rotateOnScroll, seq = _a.seq; if (!rotateOnScroll) return; // a "large scroll" (1000) should rotate through 20% of the plasmid var delta = seq.length * (e.deltaY / 5000); delta = Math.floor(delta); // must scroll by *some* amount (only matters for very small plasmids) if (delta === 0) { if (e.deltaY > 0) delta = 1; else delta = -1; } var newCentralIndex = _this.context.circular + delta; newCentralIndex = (newCentralIndex + seq.length) % seq.length; _this.context.setCentralIndex("CIRCULAR", newCentralIndex); }; _this.state = { annotationsInRows: [], inlinedLabels: [], lineHeight: 0, outerLabels: [], seqLength: 0, }; return _this; } Circular.prototype.render = function () { var _a = this.props, center = _a.center, compSeq = _a.compSeq, cutSites = _a.cutSites, handleMouseEvent = _a.handleMouseEvent, inputRef = _a.inputRef, name = _a.name, radius = _a.radius, search = _a.search, seq = _a.seq, showIndex = _a.showIndex, size = _a.size, yDiff = _a.yDiff; var _b = this.state, annotationsInRows = _b.annotationsInRows, inlinedLabels = _b.inlinedLabels, lineHeight = _b.lineHeight, outerLabels = _b.outerLabels, seqLength = _b.seqLength; var _c = this, findCoor = _c.findCoor, genArc = _c.genArc, getRotation = _c.getRotation, rotateCoor = _c.rotateCoor; // props contains props used in many/all children var props = { center: center, findCoor: findCoor, genArc: genArc, getRotation: getRotation, inputRef: inputRef, lineHeight: lineHeight, radius: radius, rotateCoor: rotateCoor, seqLength: seqLength, }; // calculate the selection row height based on number of annotation var totalRows = 4 + annotationsInRows.length; var plasmidId = "la-vz-".concat(name, "-viewer-circular"); if (!size.height) return null; return (React.createElement("svg", { ref: inputRef(plasmidId, { type: "SEQ", viewer: "CIRCULAR" }), className: "la-vz-viewer-circular", "data-testid": "la-vz-viewer-circular", height: size.height, id: plasmidId, width: size.width >= 0 ? size.width : 0, onMouseDown: handleMouseEvent, onMouseMove: handleMouseEvent, onMouseUp: handleMouseEvent, onWheel: this.handleScrollEvent }, React.createElement("g", { className: "la-vz-circular-root", transform: "translate(0, ".concat(yDiff, ")") }, React.createElement(Selection_1.Selection, __assign({}, props, { seq: seq, totalRows: totalRows })), React.createElement(CutSites_1.CutSites, __assign({}, props, { cutSites: cutSites, selectionRows: 4 })), React.createElement(Index_1.Index, __assign({}, props, { compSeq: compSeq, name: name, seq: seq, showIndex: showIndex, size: size, totalRows: totalRows, yDiff: yDiff })), React.createElement(Find_1.Find, { genArc: props.genArc, getRotation: props.getRotation, highlights: this.props.highlights, inputRef: props.inputRef, lineHeight: props.lineHeight, radius: props.radius, search: search, seqLength: props.seqLength }), React.createElement(Annotations_1.Annotations, __assign({}, props, { annotations: annotationsInRows, inlinedAnnotations: inlinedLabels, rowsToSkip: 0 })), React.createElement(Labels_1.Labels, __assign({}, props, { labels: outerLabels, size: size, yDiff: yDiff }))))); }; Circular.contextType = centralIndexContext_1.default; Circular.getDerivedStateFromProps = function (nextProps) { var lineHeight = 14; var annotationsInRows = (0, elementsToRows_1.stackElements)(nextProps.annotations, nextProps.seq.length); /** * find the element labels that need to be rendered outside the plasmid. This is done for * annotation names/etc for element titles that don't fit within the width of the element * they represent. For example, an annotation might be named "Transcription Factor XYZ" * but be only 20bps long on a plasmid that's 20k bps. Obviously that name doesn't fit. * But, a gene that's 15k on the same plasmid shouldn't have it's label outside the plasmid * when it can easily fit on top of the annotation itself */ var seqLength = nextProps.seq.length; var cutSiteLabels = nextProps.cutSites; var radius = nextProps.radius; var innerRadius = radius - 3 * lineHeight; var inlinedLabels = []; var outerLabels = []; annotationsInRows.forEach(function (r) { var circumf = innerRadius * Math.PI; r.forEach(function (ann) { // how large is the name of the annotation horizontally (with two char padding) var annNameLengthPixels = (ann.name.length + 2) * SeqViewerContainer_1.CHAR_WIDTH; // how large would part be if it were wrapped around the plasmid var annLengthBases = ann.end - ann.start; if (ann.start >= ann.end) annLengthBases += seqLength; // crosses zero-index var annLengthPixels = 2 * circumf * (annLengthBases / seqLength); if (annNameLengthPixels < annLengthPixels) { inlinedLabels.push(ann.id); } else { var end = ann.end, id = ann.id, name_1 = ann.name, start = ann.start; var type = "annotation"; outerLabels.push({ end: end, id: id, name: name_1, start: start, type: type }); } }); innerRadius -= lineHeight; }); cutSiteLabels.forEach(function (c) { return outerLabels.push(__assign(__assign(__assign({}, c.enzyme), c), { start: c.fcut, type: "enzyme" })); }); // sort all the labels so they're in ascending order outerLabels.sort(function (a, b) { return Math.min(a.start, a.end) - Math.min(b.start, b.end); }); return { annotationsInRows: annotationsInRows, inlinedLabels: inlinedLabels, lineHeight: lineHeight, outerLabels: outerLabels, seqLength: nextProps.seq.length, }; }; return Circular; }(React.Component)); exports["default"] = Circular; /** * Create an SVG arc around a single element in the Circular Viewer. */ var Arc = function (props) { var className = props.className, color = props.color, direction = props.direction, genArc = props.genArc, getRotation = props.getRotation, inputRef = props.inputRef, lineHeight = props.lineHeight, radius = props.radius, seqLength = props.seqLength, start = props.start; var end = props.end; // crosses the zero index if (end < start) { end += seqLength; } var resultLength = Math.abs(end - start); var findPath = genArc({ innerRadius: radius - lineHeight / 2, largeArc: resultLength > seqLength / 2, length: resultLength, outerRadius: radius + lineHeight / 2, sweepFWD: true, }); var id = "".concat(className, "-circular-").concat(start, "-").concat(end, "-").concat(direction); return (React.createElement("path", { key: id, ref: inputRef(id, { end: end, ref: id, start: start, type: "FIND", viewer: "CIRCULAR", }), className: className, cursor: "pointer", d: findPath, fill: color, id: id, shapeRendering: "auto", stroke: "rgba(0, 0, 0, 0.5)", strokeWidth: 1, transform: getRotation(start) })); }; exports.Arc = Arc; /***/ }), /* 9 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); var React = __webpack_require__(1); /** Default central index context object */ var defaultCentralIndex = { circular: 0, linear: 0, setCentralIndex: function (_, __) { // do nothing }, }; /** The "central index" is used to scroll the linear or circular viewer when you click on an annotation */ var CentralIndexContext = React.createContext(defaultCentralIndex); CentralIndexContext.displayName = "CentralIndexContext"; exports["default"] = CentralIndexContext; /***/ }), /* 10 */ /***/ (function(__unused_webpack_module, exports) { var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createSingleRows = exports.createMultiRows = exports.stackElements = void 0; // utility funcs for stackElements var last = function (arr) { return arr[arr.length - 1]; }; var first = function (arr) { return arr[0]; }; /** * Take an array of elements and create a 2D array where non-overlapping elements are in * the same row. Example: * * input (`T[]`): * ``` * [ ---Ann--- ---Ann3--- * ---Ann2--- ] * ``` * * output (`T[][]`): * ``` * [ ---Ann--- ---Ann3---] * [ -