seqviz-plus
Version:
An extension for Seqviz
998 lines (966 loc) • 365 kB
JavaScript
/*!
* 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---]
* [ -