=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.8.5",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Sw,i=Ew,o=Pw,a=kw,u=Cw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",Nw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new xw(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new xw(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new xw(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Aw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Aw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Aw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Tw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Aw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Tw(e),a=0;aStart: {start}
Stop: {stop}",
+ opacity: 0,
+ position: "absolute",
+ padding: "8px",
+ borderRadius: "4px",
+ border: "1px solid rgba(0,0,0,0.1)",
+ boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
+ pointerEvents: "none",
+ fontFamily: "Arial, sans-serif",
+ fontSize: "12px",
+ zIndex: 1000,
+ color: "#333",
+ lineHeight: "1.5"
+ }
+ },
+ genome: {
+ labelsOptions: {
+ fontSize: "10px",
+ show: true
+ }
+ }
+};
+
+function getMarker(markerName, xPos, yPos, size, height = null) {
+
+ height = height === null ? size : height;
+
+ switch (markerName) {
+ case "arrow":
+ return `M ${xPos} ${yPos - height/2} L ${xPos + size/2} ${yPos + height/2} L ${xPos - size/2} ${yPos + height/2} Z`;
+ default:
+ return ""; // Default empty path
+ }
+}
diff --git a/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/geneviewer-0.1.4/geneviewer.js b/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/geneviewer-0.1.4/geneviewer.js
new file mode 100644
index 0000000..6677302
--- /dev/null
+++ b/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/geneviewer-0.1.4/geneviewer.js
@@ -0,0 +1,2976 @@
+function DivContainer(div) {
+ this.div = div;
+}
+
+function createDivContainer(targetElement) {
+ var baseIdDiv = "div-container";
+
+ var div = d3.select(targetElement)
+ .attr("id", getUniqueId(baseIdDiv))
+ .classed("div-content", true);
+
+ return new DivContainer(div);
+}
+
+//Utils
+
+function getUniqueId(baseId) {
+ var i = 1;
+ while (document.getElementById(baseId + "-" + i)) {
+ i++;
+ }
+ return baseId + "-" + i;
+}
+
+function sanitizeId(str) {
+ // Replace any character that is not a letter, number, underscore, or hyphen with an underscore
+ return str.replace(/[^a-zA-Z0-9_-]/g, '_');
+}
+
+function wrap(text, width, options = {}) {
+ // Default options
+ const defaultOptions = {
+ dyAdjust: 0,
+ lineHeightEms: 1.05,
+ lineHeightSquishFactor: 1,
+ splitOnHyphen: true,
+ centreVertically: true,
+ };
+
+ // Merge default options and user-specified options
+ const {
+ dyAdjust,
+ lineHeightEms,
+ lineHeightSquishFactor,
+ splitOnHyphen,
+ centreVertically
+ } = { ...defaultOptions, ...options };
+
+ text.each(function () {
+ var text = d3.select(this),
+ x = text.attr("x"),
+ y = text.attr("y");
+
+ var words = [];
+ text
+ .text()
+ .split(/\s+/)
+ .forEach(function (w) {
+ if (splitOnHyphen) {
+ var subWords = w.split("-");
+ for (var i = 0; i < subWords.length - 1; i++)
+ words.push(subWords[i] + "-");
+ words.push(subWords[subWords.length - 1] + " ");
+ } else {
+ words.push(w + " ");
+ }
+ });
+
+ text.text(null); // Empty the text element
+
+ var tspan = text.append("tspan");
+ var line = "";
+ var prevLine = "";
+ var nWordsInLine = 0;
+
+ for (var i = 0; i < words.length; i++) {
+ var word = words[i];
+ prevLine = line;
+ line = line + word;
+ ++nWordsInLine;
+ tspan.text(line.trim());
+
+ if (tspan.node().getComputedTextLength() > width && nWordsInLine > 1) {
+ tspan.text(prevLine.trim());
+ prevLine = "";
+ line = word;
+ nWordsInLine = 1;
+ tspan = text.append("tspan").text(word.trim());
+ }
+ }
+
+ var tspans = text.selectAll("tspan");
+ var h = lineHeightEms;
+
+ if (tspans.size() > 2)
+ for (var i = 0; i < tspans.size(); i++) h *= lineHeightSquishFactor;
+
+ tspans.each(function (d, i) {
+ var dy = i * h + dyAdjust;
+ if (centreVertically) dy -= ((tspans.size() - 1) * h) / 2;
+ d3.select(this)
+ .attr("y", y)
+ .attr("x", x)
+ .attr("dy", dy + "em");
+ });
+ });
+}
+
+function adjustViewBox(svg, options = {}) {
+ const defaultOptions = {
+ padding: {
+ left: 10,
+ right: 10,
+ top: 10,
+ bottom: 10
+ }
+ };
+
+ const { padding } = { ...defaultOptions, ...options };
+
+ // Get Container Dimensions
+ var width = svg.node().getBoundingClientRect().width;
+ var height = svg.node().getBoundingClientRect().height;
+
+ // Adjust viewBox
+ var bbox = svg.node().getBBox();
+ svg.attr("viewBox", [
+ bbox.x - padding.left,
+ bbox.y - padding.top,
+ bbox.width + padding.left + padding.right,
+ bbox.height + padding.top + padding.bottom,
+ ]);
+
+ return svg;
+};
+
+function computeSize(inputSize, containerSize) {
+
+ // If inputSize is undefined or null, return 0
+ if (typeof inputSize === "undefined" || inputSize === null) {
+ return 0;
+ }
+
+ // If inputSize is a number, return it directly
+ if (typeof inputSize === "number") {
+ return inputSize;
+ }
+
+ // Initialize resultSize
+ var resultSize;
+
+ // Check if the size is given as a percentage
+ if (inputSize.includes("%")) {
+ var percentageValue = parseFloat(inputSize);
+ var fraction = percentageValue / 100;
+ resultSize = Math.round(fraction * containerSize);
+ }
+ // Check if the size is given in pixels
+ else if (inputSize.includes("px")) {
+ resultSize = parseFloat(inputSize);
+ }
+ // Assume it's a plain number otherwise
+ else {
+ resultSize = parseFloat(inputSize);
+ }
+
+ return Math.floor(resultSize);
+}
+
+function adjustGeneLabels(container, labelSelector, options = {}) {
+ // Default options
+ const defaultOptions = {
+ rotation: 65, // Rotation angle (in degrees)
+ dx: "-0.8em", // Horizontal adjustment
+ dy: "0.15em", // Vertical adjustment
+ };
+
+ // Merge default options with the provided options
+ const { rotation, dx, dy } = { ...defaultOptions, ...options };
+
+ // Select all the labels based on the provided selector
+ var labels = container.svg.selectAll(".label").nodes();
+
+ // Iterate over each label
+ for (var i = 0; i < labels.length - 1; i++) {
+ var label1 = labels[i].getBoundingClientRect();
+
+ // Compare it with all the labels that come after it
+ for (var j = i + 1; j < labels.length; j++) {
+ var label2 = labels[j].getBoundingClientRect();
+
+ // If the labels overlap
+ if (!(label1.right < label2.left ||
+ label1.left > label2.right ||
+ label1.bottom < label2.top ||
+ label1.top > label2.bottom)) {
+
+ // Get the current x and y attributes of the labels
+ var x1 = parseFloat(d3.select(labels[i]).attr('x'));
+ var y1 = parseFloat(d3.select(labels[i]).attr('y'));
+ var x2 = parseFloat(d3.select(labels[j]).attr('x'));
+ var y2 = parseFloat(d3.select(labels[j]).attr('y'));
+
+ // Rotate both labels
+ d3.select(labels[i])
+ .style("text-anchor", "end")
+ .attr("dx", dx)
+ .attr("dy", dy)
+ .attr("transform", `rotate(${rotation}, ${x1}, ${y1})`);
+
+ d3.select(labels[j])
+ .style("text-anchor", "end")
+ .attr("dx", dx)
+ .attr("dy", dy)
+ .attr("transform", `rotate(${rotation}, ${x2}, ${y2})`);
+ }
+ }
+ }
+ return container;
+}
+
+function adjustSpecificLabel(container, labelSelector, elementId, options = {}) {
+ // Default options
+ const defaultOptions = {
+ rotation: -65, // Rotation angle (in degrees)
+ offsetX: 0,
+ offsetY: 0,
+ dx: "0em", // Horizontal adjustment
+ dy: "0em", // Vertical adjustment
+ };
+
+ // Merge default options with the provided options
+ const { rotation, offsetX, offsetY, dx, dy } = { ...defaultOptions, ...options };
+
+ const overlapPercentage = (rect1, rect2) => {
+ const x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
+ const y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
+ const overlapArea = x_overlap * y_overlap;
+ const rect1Area = (rect1.right - rect1.left) * (rect1.bottom - rect1.top);
+ return (overlapArea / rect1Area) * 100;
+ };
+
+ // Select all the labels based on the provided selector
+ var labels = container.svg.selectAll(labelSelector).nodes();
+
+ // Select the specific label using the provided elementId
+ var specificLabel = container.svg.select(`#${elementId}`).node();
+
+ // Calculate the label's original center position
+ const bbox = specificLabel.getBBox();
+ var centerX = bbox.x + bbox.width / 2;
+ var centerY = bbox.y;
+
+ centerX += offsetX
+ centerY += (rotation < 0) ? bbox.height : bbox.height / 2;
+ centerY += offsetY;
+
+ // Check for overlap with other labels
+ for (var i = 0; i < labels.length; i++) {
+ if (labels[i] !== specificLabel) { // Ensure we're not comparing the label with itself
+ var labelRect = labels[i].getBoundingClientRect();
+ var specificLabelRect = specificLabel.getBoundingClientRect();
+
+ // If the specific label overlaps with another label
+ if (overlapPercentage(specificLabelRect, labelRect) > 0) {
+ // Adjust the label rotation and position
+ d3.select(specificLabel)
+ .style("text-anchor", "start")
+ .attr("dx", dx)
+ .attr("dy", dy)
+ .attr("x", centerX + offsetX)
+ .attr("y", centerY)
+ .attr("transform", `rotate(${rotation}, ${centerX}, ${centerY})`);
+
+ // Break out of the loop once we've adjusted the specific label
+ break;
+ }
+ }
+ }
+ return container;
+}
+
+function camelToKebab(string) {
+ return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
+}
+
+function extractAdditionalOptions(combinedOptions, defaultOptions) {
+ // Extract additional options that are not in defaultOptions
+ const additionalOptions = Object.keys(combinedOptions).reduce((acc, key) => {
+ if (!(key in defaultOptions)) {
+ acc[key] = combinedOptions[key];
+ }
+ return acc;
+ }, {});
+
+ return additionalOptions;
+}
+
+function setStyleFromOptions(currentElement, additionalOptions) {
+ for (const [key, value] of Object.entries(additionalOptions)) {
+ currentElement.style(camelToKebab(key), value);
+ }
+}
+
+function applyStyleToElement(currentElement, itemStyle, i) {
+ const style = itemStyle.find(s => s.index === i);
+ if (style) {
+ for (const [key, value] of Object.entries(style)) {
+ if (key !== 'index' && key !== 'labelAdjustmentOptions') {
+ currentElement.style(camelToKebab(key), value);
+ }
+ }
+ }
+}
+
+function removeNullKeys(obj) {
+ const cleanObj = { ...obj };
+ for (let key in cleanObj) {
+ if (cleanObj[key] === null) {
+ delete cleanObj[key];
+ }
+ }
+ return cleanObj;
+}
+
+function mergeOptions(defaultOptions, themeOptionsKey, userOptions) {
+ // Start with default options
+ let combinedOptions = { ...defaultOptions };
+ let themeOpts = removeNullKeys(this.themeOptions?.[themeOptionsKey] ?? {});
+
+ userOptions = removeNullKeys(userOptions);
+
+ // Iterate over the keys in defaultOptions
+ for (let key in defaultOptions) {
+ if (typeof defaultOptions[key] === 'object' && !Array.isArray(defaultOptions[key]) && defaultOptions[key] !== null) {
+ combinedOptions[key] = {
+ ...defaultOptions[key],
+ ...(themeOpts[key] || {}), // Safeguard against undefined values
+ ...(userOptions[key] || {}) // Safeguard against undefined values
+ };
+ } else {
+ // Direct merge for non-object or null properties
+ combinedOptions[key] = userOptions[key] !== undefined ? userOptions[key] : (themeOpts[key] !== undefined ? themeOpts[key] : defaultOptions[key]);
+ }
+ }
+
+ return combinedOptions;
+}
+
+function getColorScale(colorScheme, customColors, uniqueGroups) {
+ let colorScale;
+
+ // Check if customColors is an object and not an array
+ if (customColors && typeof customColors === 'object' && !Array.isArray(customColors)) {
+ // Find groups without a corresponding color in customColors
+ const unmappedGroups = uniqueGroups.filter(group => !(group in customColors));
+ // Issue a warning if there are unmapped groups
+ if (unmappedGroups.length > 0) {
+ console.warn(`Warning: No color mapping found for the following groups, defaulting to black: ${unmappedGroups.join(', ')}`);
+ }
+
+
+ // Create a color scale based on the customColors object
+ colorScale = d3.scaleOrdinal()
+ .domain(uniqueGroups)
+ .range(uniqueGroups.map(group => customColors[group] || "black"));
+ } else if (colorScheme) {
+ if (!d3[colorScheme]) {
+ console.warn(`Warning: The color scheme "${colorScheme}" does not exist. Defaulting to black.`);
+ colorScale = d3.scaleOrdinal()
+ .domain(uniqueGroups)
+ .range(uniqueGroups.map(() => "black")); // Set all colors to black
+ } else {
+ colorScale = d3.scaleOrdinal(d3[colorScheme])
+ .domain(uniqueGroups);
+ // Check if uniqueGroups are more than the colors in the colorScale
+ if (uniqueGroups.length > colorScale.range().length) {
+ console.warn(`Warning: More unique groups than colors. Some colors will repeat.`);
+ }
+ }
+ } else if (customColors && customColors.length > 0) {
+ colorScale = d3.scaleOrdinal()
+ .domain(uniqueGroups)
+ .range(customColors);
+ } else {
+ colorScale = d3.scaleOrdinal(d3.schemeCategory10)
+ .domain(uniqueGroups);
+ }
+
+ return colorScale;
+}
+
+function isInAnyDiscontinuity(value, breaks) {
+ for (let gap of breaks) {
+ if (value >= gap.start && value <= gap.end) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function createDiscontinuousScale(minStart, maxEnd, width, margin, breaks, reverse = false) {
+ let totalGap = 0;
+
+ // Calculate the total gap based on all discontinuities
+ for (let gap of breaks) {
+ if (gap.start >= minStart && gap.end <= maxEnd) {
+ totalGap += (gap.end - gap.start);
+ }
+ }
+
+ // Define the linear scale. Adjust the scale based on the reverse option.
+ let domainStart = reverse ? maxEnd - totalGap : minStart;
+ let domainEnd = reverse ? minStart : maxEnd - totalGap;
+
+ const linearScale = d3.scaleLinear()
+ .domain([domainStart, domainEnd])
+ .range([0, width - margin.left - margin.right]);
+
+ // Proxy object for discontinuous scale
+ const scaleProxy = function (value) {
+ if (isInAnyDiscontinuity(value, breaks)) {
+ return null;
+ }
+
+ let cumulativeAdjustment = 0;
+
+ // Adjust the value by all previous discontinuities
+ for (let gap of breaks) {
+ if (value > gap.end) {
+ cumulativeAdjustment += (gap.end - gap.start);
+ } else {
+ break;
+ }
+ }
+
+ // Apply reverse logic to the value adjustment
+ value = value - cumulativeAdjustment;
+
+ return linearScale(value);
+ };
+
+ // Dynamically copy all methods and properties from linearScale to scaleProxy
+ for (let prop in linearScale) {
+ if (typeof linearScale[prop] === 'function') {
+ scaleProxy[prop] = (...args) => {
+ const result = linearScale[prop](...args);
+ return result === linearScale ? scaleProxy : result;
+ };
+ } else {
+ scaleProxy[prop] = linearScale[prop];
+ }
+ }
+
+ return scaleProxy; // Return the discontinuous scale
+}
+
+function parseAndStyleText(text, parentElement, fontOptions) {
+ const tagRegex = /<([biu])>(.*?)<\/\1>/g;
+
+ let lastIndex = 0;
+
+ // Helper function to append text with specific styles
+ const appendText = (content, isBold = false, isItalic = false, isUnderline = false) => {
+ const tspan = parentElement.append("tspan")
+ .style("font-weight", isBold ? "bold" : fontOptions.weight)
+ .style("font-style", isItalic ? "italic" : fontOptions.style)
+ .text(content);
+
+ if (isUnderline) {
+ tspan.style("text-decoration", "underline");
+ }
+ };
+
+ // Iterate through the string and apply styles
+ text.replace(tagRegex, function (match, tag, content, offset) {
+ // Append text before the tag
+ if (offset > lastIndex) {
+ appendText(text.substring(lastIndex, offset), false, false, false);
+ }
+
+ // Apply style based on the tag
+ appendText(content, tag === 'b', tag === 'i', tag === 'u');
+
+ lastIndex = offset + match.length;
+ return match; // This return is not used, but is necessary for the replace function
+ });
+
+ // Append any remaining text after the last tag
+ if (lastIndex < text.length) {
+ appendText(text.substring(lastIndex), false, false, false);
+ }
+}
+
+function container(svg, margin, width, height) {
+ this.svg = svg;
+ this.margin = margin;
+ this.width = width;
+ this.height = height;
+}
+
+function createContainer(targetElementId, id, themeOptionsKey, options = {}) {
+
+ const defaultOptions = {
+ id: id || "svg-container",
+ margin: { top: 5, right: 50, bottom: 5, left: 50 },
+ style: {
+ backgroundColor: "#0000"
+ },
+ width: null,
+ height: null
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, themeOptionsKey, options);
+ const { id: containerId, margin: originalMargin, style, width, height } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ // Compute margins without modifying the original margin object
+ const computedMargin = {
+ top: computeSize(originalMargin?.top ?? 0, height),
+ right: computeSize(originalMargin?.right ?? 0, width),
+ bottom: computeSize(originalMargin?.bottom ?? 0, height),
+ left: computeSize(originalMargin?.left ?? 0, width)
+ };
+
+ var svg = d3.select(targetElementId)
+ .append("svg")
+ .attr("id", getUniqueId(containerId))
+ .attr("width", width || "100%")
+ .attr("height", height || "100%")
+ .attr("preserveAspectRatio", "xMinYMin meet")
+ .attr("viewBox", `0 0 ${width} ${height}`)
+ .classed("geneviewer-svg-content", true)
+ .style("box-sizing", "border-box")
+ .style("background-color", style.backgroundColor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+
+ // Apply styles from the combined options
+ Object.entries(style).forEach(([key, value]) => {
+ svg.style(key, value);
+ });
+
+ return new container(svg, computedMargin, width, height);
+}
+
+function addScalePadding(startValue, endValue, padding, to) {
+ let paddingValue;
+
+ // Check if padding is a percentage string
+ if (typeof padding === 'string' && padding.endsWith('%')) {
+ paddingValue = parseFloat(padding) / 100;
+ }
+ // Check if padding is a number between 0 and 100
+ else if (typeof padding === 'number' && padding >= 0 && padding <= 100) {
+ paddingValue = padding / 100;
+ }
+ // If padding is not in a valid format or range, log a warning and return the original value
+ else {
+ console.warn('Invalid padding format. Padding should be a percentage (as a string or a number between 0 and 100). No padding applied.');
+ return to === 'start' ? startValue : endValue;
+ }
+
+ // Calculate the total range
+ const totalRange = Math.abs(endValue - startValue);
+
+ // Calculate the actual padding value based on the total range
+ paddingValue = totalRange * paddingValue;
+
+ // Adjust the value based on whether we're padding the start or end
+ if (to === 'start') {
+ return startValue - paddingValue; // Subtract padding from the start
+ } else if (to === 'end') {
+ return endValue + paddingValue; // Add padding to the end
+ } else {
+ console.error('Invalid "to" parameter in addScalePadding. Must be "start" or "end".');
+ return to === 'start' ? startValue : endValue; // Return the original value if 'to' is neither
+ }
+}
+
+// Make links function
+
+function getLinkCoordinates(graphContainer, data) {
+
+ const links = data.map(item => {
+ // Construct the selectors from the data
+ const selector1 = `.link-marker[cluster='${item.cluster1}'][linkID='${item.linkID}'][position='${item.start1}']`;
+ const selector2 = `.link-marker[cluster='${item.cluster2}'][linkID='${item.linkID}'][position='${item.start2}']`;
+ const selector3 = `.link-marker[cluster='${item.cluster1}'][linkID='${item.linkID}'][position='${item.end1}']`;
+ const selector4 = `.link-marker[cluster='${item.cluster2}'][linkID='${item.linkID}'][position='${item.end2}']`;
+
+ // Get the elements within graphContainer using D3
+ const element1 = graphContainer.select(selector1).node();
+ const element2 = graphContainer.select(selector2).node();
+ const element3 = graphContainer.select(selector3).node();
+ const element4 = graphContainer.select(selector4).node();
+
+ // Check if elements exist
+ if (!element1 || !element2 || !element3 || !element4) {
+ console.error('Elements not found for selectors:', selector1, selector2, selector3, selector4);
+ return null;
+ }
+
+ // Get bounding rectangles
+ const rect1 = element1.getBoundingClientRect();
+ const rect2 = element2.getBoundingClientRect();
+ const rect3 = element3.getBoundingClientRect();
+ const rect4 = element4.getBoundingClientRect();
+
+ // Adjust the coordinates relative to the graphContainer's position
+ const containerRect = graphContainer.node().getBoundingClientRect();
+ const adjustRect = rect => ({
+ x: rect.left - containerRect.left,
+ y: rect.top - containerRect.top
+ });
+
+ // Determine the strand based on the x-coordinate positions
+ const strand1 = item.start1 <= item.end1 ? "forward" : "reverse";
+ const strand2 = item.start2 <= item.end2 ? "forward" : "reverse";
+
+ return [
+ { startPoint: adjustRect(rect1), endPoint: adjustRect(rect2), strand: strand1 },
+ { startPoint: adjustRect(rect3), endPoint: adjustRect(rect4), strand: strand2 }
+ ];
+ });
+
+ return links.filter(link => link !== null);
+};
+
+function makeLinks(graphContainer, links, clusters) {
+
+ if (!links || links.length === 0) {
+ return;
+ }
+ // Default options for title and subtitle
+ const defaultOptions = {
+ curve: true,
+ invertedColor: "red",
+ normalColor: "blue",
+ color: "lightgrey",
+ style: {
+ stroke: "none",
+ fillOpacity: 0.8
+ }
+ };
+
+
+ const graphRect = graphContainer.node().getBoundingClientRect();
+ // Create a container div for the SVG
+ const svgContainer = graphContainer.insert("div", ":first-child")
+ .style("position", "relative")
+ .style("width", "100%")
+ .style("height", "100%");
+
+
+ // Create an SVG element inside graphContainer
+ var lineSvg = svgContainer.insert("svg", ":first-child")
+ .attr("width", graphRect.width)
+ .attr("height", graphRect.height)
+ .classed("GeneLink", true)
+ .style("position", "absolute")
+ .style("z-index", 1)
+ .style("left", `${graphContainer.left}px`)
+ .style("top", `${graphContainer.top}px`);
+
+
+ links.forEach(function(link) {
+
+ const combinedOptions = mergeOptions(defaultOptions, "linkOptions", link.options);
+ const { curve, invertedColor, normalColor, color, style } = combinedOptions;
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+ const coordinates = getLinkCoordinates(graphContainer, HTMLWidgets.dataframeToD3(link.data));
+
+ coordinates.forEach(function(coordinate, index) {
+
+ const baseColor = coordinate[0].strand == coordinate[1].strand ? d3.rgb(normalColor) : d3.rgb(invertedColor)
+ var colorScale = d3.scaleSequential(t => d3.interpolate("#FFFFFF", baseColor)(t))
+ .domain([0, 100]);
+ const identity = link.data?.identity?.[index] ?? 100
+
+ lineSvg.append("path")
+ .attr("d", createLinkerPath(coordinate[0], coordinate[1], curve))
+ .style("fill", colorScale(identity))
+ .style("stroke", style.stroke)
+ .style("fill-opacity", style.fillOpacity)
+ .classed("GeneLink", true)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ });
+ });
+}
+
+function createLinkerPath(link1, link2, curve = true) {
+ var path = d3.path();
+
+ if(curve){
+ var midY1 = (link1.startPoint.y + link1.endPoint.y) / 2;
+ path.moveTo(link1.startPoint.x, link1.startPoint.y);
+ path.bezierCurveTo(link1.startPoint.x, midY1, link1.endPoint.x, midY1, link1.endPoint.x, link1.endPoint.y);
+
+ // Line to second curve's end point
+ path.lineTo(link2.endPoint.x, link2.endPoint.y);
+
+ // Second Bezier curve in reverse
+ var midY2 = (link2.startPoint.y + link2.endPoint.y) / 2;
+ path.bezierCurveTo(link2.endPoint.x, midY2, link2.startPoint.x, midY2, link2.startPoint.x, link2.startPoint.y);
+
+
+ } else {
+ path.moveTo(link1.startPoint.x, link1.startPoint.y);
+ path.lineTo(link1.endPoint.x, link1.endPoint.y);
+ path.lineTo(link2.endPoint.x, link2.endPoint.y);
+ path.lineTo(link2.startPoint.x, link2.startPoint.y);
+ }
+ // Close the path
+ path.closePath();
+
+ return path.toString();
+};
+
+function getClusterLinks(data, cluster) {
+
+ if (!data || data.length === 0) {
+ return [];
+ }
+
+ const linksCluster1 = data.filter(item => item.cluster1 === cluster);
+ const linksCluster2 = data.filter(item => item.cluster2 === cluster);
+ const clusterLinks = linksCluster1.concat(linksCluster2);
+
+ return clusterLinks;
+}
+
+// Cluster functions
+
+container.prototype.cluster = function (options = {}) {
+
+ // Default options for title and subtitle
+ const defaultOptions = {
+ separateStrands: false,
+ strandSpacing: 1,
+ preventGeneOverlap: false,
+ overlapSpacing: 5
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'clusterOptions', options);
+ const { separateStrands, strandSpacing, preventGeneOverlap, overlapSpacing} = combinedOptions;
+
+ this.separateStrands = separateStrands;
+ this.strandSpacing = strandSpacing;
+ this.preventGeneOverlap = preventGeneOverlap;
+ this.overlapSpacing = overlapSpacing;
+
+ return this;
+};
+
+container.prototype.theme = function (themeName) {
+ // Make sure the theme exists
+ if (!themes.hasOwnProperty(themeName)) {
+ throw new Error(`Theme '${themeName}' does not exist.`);
+ }
+
+ // Retrieve the theme
+ const themeOptions = themes[themeName];
+
+ // Save the theme options to the instance for later use
+ this.themeOptions = themeOptions;
+
+ return this;
+};
+
+container.prototype.geneData = function (data, clusterData) {
+
+ // Needed to set color
+ this.dataAll = data
+
+ this.data = clusterData.map(item => {
+ var newItem = { ...item };
+
+ // Convert cluster to string
+ newItem.cluster = String(newItem.cluster);
+
+ newItem.direction = "forward";
+ if (newItem.start > newItem.end) {
+ newItem.direction = "reverse";
+ }
+
+ return newItem;
+ });
+
+ return this;
+};
+
+container.prototype.scale = function (options = {}) {
+ // Verify that the data exists
+ if (!this.data) {
+ console.error('No data has been added to this cluster container.');
+ return this;
+ }
+
+ // Default options specific for scales and axis
+ const defaultScaleOptions = {
+ start: null,
+ end: null,
+ padding: 2,
+ hidden: true,
+ reverse: false,
+ axisType: "bottom",
+ breaks: [],
+ tickValues: null,
+ ticksCount: 10,
+ ticksFormat: ",.0f",
+ y: null,
+ tickStyle: {
+ stroke: "grey",
+ strokeWidth: 1,
+ lineLength: 6
+ },
+ textStyle: {
+ fill: "black",
+ fontSize: "10px",
+ fontFamily: "Arial",
+ cursor: "default"
+ },
+ lineStyle: {
+ stroke: "grey",
+ strokeWidth: 1
+ }
+ };
+
+ // Merge provided options with the default ones
+ const combinedOptions = mergeOptions.call(this, defaultScaleOptions, 'scaleOptions', options);
+
+ // De-structure the combined options
+ const { start, end, padding, hidden, breaks, tickValues, reverse, axisType, ticksCount, ticksFormat, y: initialY, tickStyle, textStyle, lineStyle } = combinedOptions;
+
+ // Determine y based on axisType and initialY
+ const y = initialY !== null ? initialY : (axisType === 'bottom' ? 30 : 80);
+
+ // Extract additional options that are not in defaultScaleOptions
+ const additionalOptionsTickStyle = extractAdditionalOptions(tickStyle, defaultScaleOptions.tickStyle);
+ const additionalOptionsTextStyle = extractAdditionalOptions(textStyle, defaultScaleOptions.textStyle);
+ const additionalOptionslineStyle = extractAdditionalOptions(lineStyle, defaultScaleOptions.lineStyle);
+
+ // Filter data based on the provided start and end values
+ if (start !== null) {
+ this.data = this.data.filter(d => d.start >= start);
+ }
+ if (end !== null) {
+ this.data = this.data.filter(d => d.end <= end);
+ }
+
+ // Filter out data where start or end falls within any of the breaks
+ this.data = this.data.filter(d => {
+ return !breaks.some(gap =>
+ (d.start >= gap.start && d.start <= gap.end) ||
+ (d.end >= gap.start && d.end <= gap.end)
+ );
+ });
+
+ this.reverse = reverse;
+
+ // Use provided start and end values if they exist, otherwise compute them from data
+ this.minStart = start !== null ? start : d3.min(this.data, d => Math.min(d.start, d.end));
+ this.maxEnd = end !== null ? end : d3.max(this.data, d => Math.max(d.start, d.end));
+
+ if(start == null){
+ this.minStart = addScalePadding(this.minStart, this.maxEnd, padding, to = "start")
+ }
+
+ if(end == null){
+ this.maxEnd = addScalePadding(this.minStart, this.maxEnd, padding, to = "end")
+ }
+
+ // Create scales
+ this.xScale = createDiscontinuousScale(this.minStart, this.maxEnd, this.width, this.margin, breaks, reverse);
+ this.yScale = d3.scaleLinear()
+ .domain([0, 100])
+ .range([this.height - this.margin.bottom - this.margin.top, 0]);
+
+ // Filter breaks within the scale range
+ this.breaks = breaks.filter(gap => gap.start >= this.minStart && gap.end <= this.maxEnd);
+
+ const that = this;
+ if (!hidden) {
+
+ // Create and configure the X-axis
+ const adjustedYOffset = this.yScale ? this.yScale(y) : y;
+ const axisGroup = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left},${this.margin.top + adjustedYOffset})`);
+
+ linearScale = d3.scaleLinear()
+ .domain([this.minStart, this.maxEnd])
+ .range([0, this.width - this.margin.left - this.margin.right]);
+
+ const xAxis = d3.axisBottom(linearScale)
+ .tickFormat(d3.format(ticksFormat));
+
+ if (Array.isArray(tickValues) && tickValues.length > 0) {
+ xAxis.tickValues(tickValues);
+ } else if (typeof tickValues === 'number') {
+ xAxis.tickValues([tickValues]);
+ } else {
+ xAxis.ticks(ticksCount);
+ }
+
+ const axis = axisGroup.append("g").call(xAxis);
+
+ // Style axis lines and text
+ axis.selectAll(".tick line")
+ .style("stroke", tickStyle.stroke)
+ .style("stroke-width", tickStyle.strokeWidth)
+ .attr("y2", tickStyle.lineLength)
+ .each(function () {
+ const currentElement = d3.select(this)
+ setStyleFromOptions(currentElement, additionalOptionsTickStyle);
+ });
+
+ axis.selectAll(".tick text")
+ .style("fill", textStyle.fill)
+ .style("font-size", textStyle.fontSize)
+ .style("font-family", textStyle.fontFamily)
+ .style("cursor", textStyle.cursor)
+ .each(function () {
+ const currentElement = d3.select(this)
+ setStyleFromOptions(currentElement, additionalOptionsTextStyle);
+ });
+
+ axis.selectAll(".tick").each(function (d) {
+ let tickValue = d3.select(this).data()[0];
+ let newX = that.xScale(tickValue);
+
+ if (newX === null) {
+ // If the new X position is null, remove the tick
+ d3.select(this).remove();
+ } else {
+ // Otherwise, update the transform attribute
+ d3.select(this).attr("transform", `translate(${newX},0)`);
+ }
+ });
+
+ axis.select(".domain")
+ .style("stroke", lineStyle.stroke)
+ .style("stroke-width", lineStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionslineStyle);
+ });
+
+ }
+ return this;
+};
+
+container.prototype.title = function (title, subtitle, show = true, options = {}) {
+
+ // Return early if neither title nor subtitle is provided
+ if (!title && !subtitle) {
+ return this;
+ }
+
+ if (!show) {
+ return this;
+ }
+
+ // Default options for title and subtitle
+ const defaultOptions = {
+ x: 0,
+ y: 20,
+ align: "center",
+ spacing: 20, // Default spacing between title and subtitle
+ titleFont: {
+ fontSize: "16px",
+ fontStyle: "normal",
+ fontWeight: "bold",
+ textDecoration: "normal",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ },
+ subtitleFont: {
+ fontSize: "14px",
+ fontStyle: "normal",
+ fontWeight: "normal",
+ textDecoration: "none",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ },
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'titleOptions', options);
+ const { x, y, titleFont, subtitleFont, align, spacing } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsTitleFont = extractAdditionalOptions(titleFont, defaultOptions.titleFont);
+ const additionalOptionsSubtitleFont = extractAdditionalOptions(subtitleFont, defaultOptions.subtitleFont);
+
+ let xPos;
+ let textAnchor;
+
+ // Determine text align and anchor based on the provided align
+ switch (align) {
+ case "left":
+ xPos = x;
+ textAnchor = "start";
+ break;
+ case "right":
+ xPos = this.width - this.margin.left - this.margin.right + x;
+ textAnchor = "end";
+ break;
+ default:
+ const effectiveWidth = this.width - this.margin.left - this.margin.right;
+ xPos = (effectiveWidth / 2) + x;
+ textAnchor = "middle";
+ }
+
+ var g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ if (title) {
+ // Add title to the SVG
+ g.append("text")
+ .attr("x", xPos)
+ .attr("y", y + (this.margin.top / 2))
+ .attr("text-anchor", textAnchor)
+ .style("font-size", titleFont.fontSize)
+ .style("font-style", titleFont.fontStyle)
+ .style("font-weight", titleFont.fontWeight)
+ .style("text-decoration", titleFont.textDecoration)
+ .style("font-family", titleFont.fontFamily)
+ .style("cursor", titleFont.cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(title, currentElement, titleFont);
+ setStyleFromOptions(currentElement, additionalOptionsTitleFont);
+ });
+ }
+
+ if (subtitle) {
+ // Add subtitle to the SVG
+ g.append("text")
+ .attr("x", xPos)
+ .attr("y", y + (this.margin.top / 2) + spacing)
+ .attr("text-anchor", textAnchor)
+ .style("font-size", subtitleFont.fontSize)
+ .style("font-style", subtitleFont.fontStyle)
+ .style("font-weight", subtitleFont.fontWeight)
+ .style("text-decoration", subtitleFont.textDecoration)
+ .style("font-family", subtitleFont.fontFamily)
+ .style("cursor", subtitleFont.cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(subtitle, currentElement, subtitleFont);
+ setStyleFromOptions(currentElement, additionalOptionsSubtitleFont);
+ });
+ }
+
+ return this;
+};
+
+container.prototype.footer = function (title, subtitle, show = true, options = {}) {
+
+ // Return early if neither title nor subtitle is provided
+ if (!title && !subtitle) {
+ return this;
+ }
+
+ if (!show) {
+ return this;
+ }
+
+ // Default options for title and subtitle
+ const defaultOptions = {
+ x: 0,
+ y: 0,
+ align: "left",
+ spacing: 12, // Default spacing between title and subtitle
+ titleFont: {
+ fontSize: "12px",
+ fontWeight: "bold",
+ fontStyle: "normal",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ },
+ subtitleFont: {
+ fontSize: "10px",
+ fontWeight: "normal",
+ fontStyle: "normal",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ },
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'footerOptions', options);
+ const { x, y, titleFont, subtitleFont, align, spacing } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsTitleFont = extractAdditionalOptions(titleFont, defaultOptions.titleFont);
+ const additionalOptionsSubtitleFont = extractAdditionalOptions(subtitleFont, defaultOptions.subtitleFont);
+
+ let xPos;
+ let textAnchor;
+
+ // Determine text align and anchor based on the provided align
+ switch (align) {
+ case "left":
+ xPos = x;
+ textAnchor = "start";
+ break;
+ case "right":
+ xPos = this.width - this.margin.left - this.margin.right + x;
+ textAnchor = "end";
+ break;
+ default:
+ const effectiveWidth = this.width - this.margin.left - this.margin.right;
+ xPos = this.margin.left + (effectiveWidth / 2) + x;
+ textAnchor = "middle";
+ }
+
+
+ // Calculate y align for title and subtitle based on the SVG height and bottom margin
+ const titleYPos = this.height - this.margin.bottom + y - 20;
+ const subtitleYPos = titleYPos + spacing;
+
+ if (title) {
+ this.svg.append("text")
+ .attr("x", xPos)
+ .attr("y", titleYPos)
+ .attr("text-anchor", textAnchor)
+ .style("font-size", titleFont.fontSize)
+ .style("font-weight", titleFont.fontWeight)
+ .style("font-style", titleFont.fontStyle)
+ .style("font-family", titleFont.fontFamily)
+ .style("cursor", titleFont.cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(title, currentElement, titleFont);
+ setStyleFromOptions(currentElement, additionalOptionsTitleFont);
+ });
+
+ }
+
+ // Add subtitle to the SVG if provided
+ if (subtitle) {
+ this.svg.append("text")
+ .attr("x", xPos)
+ .attr("y", subtitleYPos)
+ .attr("text-anchor", textAnchor)
+ .style("font-size", subtitleFont.fontSize)
+ .style("font-weight", subtitleFont.fontWeight)
+ .style("font-style", subtitleFont.fontStyle)
+ .style("font-family", subtitleFont.fontFamily)
+ .style("cursor", subtitleFont.cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(subtitle, currentElement, subtitleFont);
+ setStyleFromOptions(currentElement, subtitleFont);
+ });
+ }
+
+ return this;
+};
+
+container.prototype.clusterLabel = function (title, show = true, options = {}) {
+ if (!show) {
+ return this;
+ }
+
+ // Default options
+ const defaultOptions = {
+ x: 0,
+ y: 0,
+ position: 'left',
+ wrapLabel: true,
+ wrapOptions: {},
+ fontSize: "12px",
+ fontStyle: "normal",
+ fontWeight: "bold",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ };
+
+ // Merge the options using the generic function
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'clusterLabelOptions', options);
+ const {
+ x,
+ y,
+ position,
+ wrapLabel,
+ wrapOptions,
+ fontSize,
+ fontStyle,
+ fontWeight,
+ fontFamily,
+ cursor,
+ } = combinedOptions;
+
+ const additionalwrapOptions = extractAdditionalOptions(wrapOptions, defaultOptions.wrapOptions);
+ // Extract additional options that are not in defaultOptions
+ const additionalOptions = extractAdditionalOptions(combinedOptions, defaultOptions);
+
+ const titleFont = {
+ size: fontSize,
+ style: fontStyle,
+ weight: fontWeight,
+ family: fontFamily,
+ cursor: cursor
+ };
+
+ // calculate middle y position
+ const adjustedHeight = this.height - this.margin.top - this.margin.bottom;
+ const middleY = this.margin.top + adjustedHeight / 2 + y;
+ const titleWidth = position === 'left' ? this.margin.left - x : this.margin.right - x;
+
+ let xposition;
+ if (position === 'left') {
+ xposition = this.margin.left / 2 + x; // title is in the left margin
+ } else { // 'right'
+ xposition = this.width - this.margin.right / 2 - x; // title is in the right margin
+ }
+
+ let clusterTitle = this.svg.append("text")
+ .attr("x", xposition)
+ .attr("y", middleY)
+ .attr("text-anchor", "middle") // text is always centered
+ .attr("dominant-baseline", "central") // Vertically center text
+ .style("font-size", fontSize)
+ .style("font-style", fontStyle)
+ .style("font-weight", fontWeight)
+ .style("font-family", fontFamily)
+ .style("cursor", cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+
+ if (!wrapLabel) {
+ // Set the text and apply styles only if wrapLabel is false
+ parseAndStyleText(title, currentElement, titleFont);
+ } else {
+ currentElement.text(title);
+ // If wrapLabel is true, wrap the text
+ wrap(currentElement, titleWidth, wrapOptions);
+ currentElement.selectAll("tspan").each(function () {
+ const currentTspan = d3.select(this);
+ const tspanText = currentTspan.text();
+ currentTspan.text('');
+ parseAndStyleText(tspanText, currentTspan, titleFont);
+ });
+ }
+ setStyleFromOptions(currentElement, additionalOptions);
+ });
+
+ return this;
+};
+
+container.prototype.sequence = function (show = true, options = {}) {
+
+ if (!show) {
+ return this;
+ }
+
+ if (!this.data) {
+ console.error('No data has been added to this cluster container. Please use the addGeneData() function before attempting to draw a gene line.');
+ return this;
+ }
+
+ const defaultOptions = {
+ y: 50,
+ start: null,
+ end: null,
+ sequenceStyle: { // Adding sequenceStyle
+ stroke: "grey",
+ strokeWidth: 1
+ },
+ markerStyle: {
+ markerHeight: 10,
+ stroke: "grey",
+ strokeWidth: 1,
+ tiltAmount: -5,
+ gap: 0
+ }
+ };
+
+ // Merge the default options with any predefined sequenceOptions and the provided options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'sequenceOptions', options);
+ const { y, start, end, markerStyle, sequenceStyle } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions for sequenceStyle
+ const additionalOptionsSequence = extractAdditionalOptions(sequenceStyle, defaultOptions.sequenceStyle);
+
+ var g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left},${this.margin.top})`);
+
+ // Draw baseline with sequenceStyle
+ g.append("line")
+ .attr("class", "baseline")
+ .attr("x1", this.xScale(start || this.minStart))
+ .attr("y1", this.yScale(y))
+ .attr("x2", this.xScale(end || this.maxEnd))
+ .attr("y2", this.yScale(y))
+ .style("stroke", sequenceStyle.stroke)
+ .style("stroke-width", sequenceStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsSequence);
+ });
+
+ // Draw break markers with tilted lines
+ for (let gap of this.breaks) {
+ const xStart = this.xScale(gap.start - 0.001) * (1 - markerStyle.gap / 100);
+ const xEnd = this.xScale(gap.end + 0.001) * (1 + markerStyle.gap / 100);
+ const yBase = this.yScale(y);
+ const yTop = yBase - markerStyle.markerHeight / 2;
+ const yBottom = yBase + markerStyle.markerHeight / 2;
+
+ if (xStart !== null && xEnd !== null) {
+ g.append("line")
+ .attr("class", "gap-line")
+ .attr("x1", xStart + (markerStyle.tiltAmount / 2))
+ .attr("y1", yBase)
+ .attr("x2", xEnd - (markerStyle.tiltAmount / 2))
+ .attr("y2", yBase)
+ .attr("stroke", "#0000")
+ .style("stroke-width", sequenceStyle.strokeWidth * 1.1);
+ }
+
+ if (xStart !== null) {
+ // Draw the tilted line before the gap
+ g.append("line")
+ .attr("x1", xStart)
+ .attr("y1", yTop)
+ .attr("x2", xStart + markerStyle.tiltAmount)
+ .attr("y2", yBottom)
+ .attr("stroke", markerStyle.stroke)
+ .attr("stroke-width", markerStyle.strokeWidth);
+ }
+
+ if (xEnd !== null) {
+ // Draw the tilted line after the gap
+ g.append("line")
+ .attr("x1", xEnd - markerStyle.tiltAmount)
+ .attr("y1", yTop)
+ .attr("x2", xEnd)
+ .attr("y2", yBottom)
+ .attr("stroke", markerStyle.stroke)
+ .attr("stroke-width", markerStyle.strokeWidth);
+ }
+ }
+
+ return this;
+};
+
+container.prototype.coordinates = function (show = true, options = {}) {
+ if (!show) {
+ return this;
+ }
+
+ const defaultOptions = {
+ rotate: -45,
+ yPositionTop: 53,
+ yPositionBottom: 48,
+ tickValuesTop: null,
+ tickValuesBottom: null,
+ overlapThreshold: 20,
+ tickStyle: {
+ stroke: "black",
+ strokeWidth: 1,
+ lineLength: 6
+ },
+ textStyle: {
+ fill: "black",
+ fontSize: "10px",
+ fontFamily: "Arial",
+ cursor: "default",
+ x: 0,
+ y: 0
+ }
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'coordinatesOptions', options);
+ const { rotate, yPositionTop, yPositionBottom, tickValuesBottom, tickValuesTop, tickStyle, textStyle } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsTickStyle = extractAdditionalOptions(tickStyle, defaultOptions.tickStyle);
+ const additionalOptionsTextStyle = extractAdditionalOptions(textStyle, defaultOptions.textStyle);
+
+
+ const g = this.svg.append("g")
+ .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
+
+ // Convert provided tickValues to the required format
+ let tickValuesTopFinal = Array.isArray(tickValuesTop) ? tickValuesTop.map(value => ({ value, rowID: null }))
+ : (tickValuesTop != null ? [{ value: tickValuesTop, rowID: null }] : []);
+ let tickValuesBottomFinal = Array.isArray(tickValuesBottom) ? tickValuesBottom.map(value => ({ value, rowID: null }))
+ : (tickValuesBottom != null ? [{ value: tickValuesBottom, rowID: null }] : []);
+
+ // If neither tickValuesTop nor tickValuesBottom are provided, calculate them
+ if (!tickValuesTop && !tickValuesBottom) {
+
+ let allTickValues = this.data.reduce((acc, d) => {
+ // Define tickValueStart and tickValueStop
+ let tickValueStart = { value: d.start, rowID: d.rowID };
+ let tickValueStop = { value: d.end, rowID: d.rowID };
+ // Add strand property if it exists
+ tickValueStart.strand = d.strand;
+ tickValueStop.strand = d.strand;
+ // Add geneTrack property if it exists
+ if ('geneTrack' in d) {
+ tickValueStart.geneTrack = d.geneTrack;
+ tickValueStop.geneTrack = d.geneTrack;
+ }
+
+
+ acc.push(tickValueStart);
+ acc.push(tickValueStop);
+
+ return acc;
+ }, []);
+
+ // Remove duplicates based on the 'value' property
+ allTickValues = allTickValues.filter((obj, index, self) =>
+ index === self.findIndex((t) => t.value === obj.value)
+ );
+
+ allTickValues.sort((a, b) => a.value - b.value);
+
+ if (this.separateStrands) {
+ allTickValues.forEach(tickValue => {
+ if (tickValue.strand === "forward") {
+ tickValuesTopFinal.push(tickValue);
+ } else {
+ tickValuesBottomFinal.push(tickValue);
+ }
+ });
+ } else {
+ // Calculate overlap and distribute tick values between top and bottom
+ const totalXValueRange = this.xScale(allTickValues[allTickValues.length - 1].value) - this.xScale(allTickValues[0].value);
+ const tickValueThreshold = combinedOptions.overlapThreshold;
+
+ for (let i = 0; i < allTickValues.length; i++) {
+ if (i === 0) {
+ // First tick always goes to the bottom
+ tickValuesBottomFinal.push(allTickValues[i]);
+ continue;
+ }
+
+ const diff = this.xScale(allTickValues[i].value) - this.xScale(allTickValues[i - 1].value);
+
+ if (diff < tickValueThreshold) {
+ // If the difference exceeds the threshold, place this tick at the top
+ tickValuesTopFinal.push(allTickValues[i]);
+
+ // Place the next tick at the bottom, if it exists
+ if (i + 1 < allTickValues.length) {
+ tickValuesBottomFinal.push(allTickValues[i + 1]);
+ i++; // Skip the next index, as it's already processed
+ }
+ } else {
+ // Otherwise, place this tick at the bottom
+ tickValuesBottomFinal.push(allTickValues[i]);
+ }
+ }
+ }
+ }
+
+ const self = this;
+
+ // Create and configure the top axis
+ const xAxisTop = g.append("g")
+ .attr("transform", "translate(0," + this.yScale(yPositionTop) + ")")
+ .call(d3.axisTop(this.xScale).tickValues(tickValuesTopFinal.map(t => t.value)));
+
+ xAxisTop.selectAll(".tick")
+ .data(tickValuesTopFinal)
+ .attr("rowID", d => d.rowID)
+ .attr("transform", function (d) {
+ const xOffset = self.xScale(d.value);
+ var currentOverlapSpacing = d.geneTrack ? (d.geneTrack - 1) * self.geneOverlapSpacing : 0;
+ return "translate(" + xOffset + "," + -currentOverlapSpacing + ")";
+ });
+
+ xAxisTop.select(".domain").attr("stroke", "none");
+
+ xAxisTop.selectAll("text")
+ .data(tickValuesTopFinal)
+ .attr("class", "coordinate")
+ .style("text-anchor", "end")
+ .attr("dx", `${-0.8 + textStyle.x}em`)
+ .attr("dy", `${0.4 + textStyle.y}em`)
+ .attr("transform", "rotate(" + (-rotate) + ")")
+ .style("fill", textStyle.fill)
+ .style("font-size", textStyle.fontSize)
+ .style("font-family", textStyle.fontFamily)
+ .style("cursor", textStyle.cursor)
+ .each(function() {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsTextStyle);
+ });
+
+ xAxisTop.selectAll(".tick line")
+ .style("stroke", tickStyle.stroke)
+ .style("stroke-width", tickStyle.strokeWidth)
+ .attr("y2", -tickStyle.lineLength)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsTickStyle);
+ });
+
+
+
+ // Create and configure the bottom axis
+ const xAxisBottom = g.append("g")
+ .attr("transform", "translate(0," + this.yScale(yPositionBottom) + ")")
+ .call(d3.axisBottom(this.xScale).tickValues(tickValuesBottomFinal.map(t => t.value)));
+
+ xAxisBottom.selectAll(".tick")
+ .data(tickValuesBottomFinal)
+ .attr("rowID", d => d.rowID)
+ .attr("transform", function (d) {
+ const xOffset = self.xScale(d.value);
+ var currentOverlapSpacing = d.geneTrack ? -(d.geneTrack - 1) * self.geneOverlapSpacing : 0;
+ return "translate(" + xOffset + "," + -currentOverlapSpacing + ")";
+ });
+
+ xAxisBottom.select(".domain").attr("stroke", "none");
+
+ xAxisBottom.selectAll("text")
+ .data(tickValuesBottomFinal)
+ .attr("class", "coordinate")
+ .style("text-anchor", "start")
+ .attr("dx", `${0.8 + textStyle.x}em`)
+ .attr("dy", `${-0.15 + textStyle.y}em`)
+ .attr("transform", "rotate(" + (-rotate) + ")")
+ .style("fill", textStyle.fill)
+ .style("font-size", textStyle.fontSize)
+ .style("font-family", textStyle.fontFamily)
+ .style("cursor", textStyle.cursor)
+ .each(function() {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsTextStyle);
+ });
+
+ xAxisBottom.selectAll(".tick line")
+ .style("stroke", tickStyle.stroke)
+ .style("stroke-width", tickStyle.strokeWidth)
+ .attr("y2", tickStyle.lineLength)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsTickStyle);
+ });
+
+ return this;
+};
+
+container.prototype.scaleBar = function (show = true, options = {}) {
+ if (!show) {
+ return this;
+ }
+
+ const defaultOptions = {
+ title: "1 kb",
+ scaleBarUnit: 1000,
+ x: 0, // default x offset
+ y: 10,
+ labelStyle: { // default styling for the label
+ fontSize: "10px",
+ fontFamily: "sans-serif",
+ cursor: "default",
+ fill: "black", // default text color
+ labelPosition: "left" // moved labelPosition into labelStyle
+ },
+ textPadding: 0, // padding between text and line in x-direction
+ scaleBarLineStyle: { // default styling for the scale bar line
+ stroke: "grey",
+ strokeWidth: 1
+ },
+ scaleBarTickStyle: { // default styling for the scale bar ticks
+ stroke: "grey",
+ strokeWidth: 1
+ }
+ };
+
+ // Merge the default options with any predefined scaleBarOptions and the provided options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'scaleBarOptions', options);
+ const { title, scaleBarUnit, x, y, textPadding, labelStyle, scaleBarLineStyle, scaleBarTickStyle } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsLine = extractAdditionalOptions(scaleBarLineStyle, defaultOptions.scaleBarLineStyle);
+ const additionalOptionsTick = extractAdditionalOptions(scaleBarTickStyle, defaultOptions.scaleBarTickStyle);
+ const additionalOptionsLabel = extractAdditionalOptions(labelStyle, defaultOptions.labelStyle);
+
+ // Calculate the length of the scale bar in pixels
+ const scaleBarLength = Math.abs(this.xScale(scaleBarUnit) - this.xScale(0));
+
+ // Create the group with the x offset applied
+ const g = this.svg.append("g")
+ .attr("transform", `translate(${this.width - this.margin.right - scaleBarLength - parseInt(labelStyle.fontSize) - 5 + x}, ${this.height - this.margin.bottom})`);
+
+ // Create the scale bar line
+ g.append("line")
+ .attr("x1", parseInt(labelStyle.fontSize) + 5 + scaleBarLength)
+ .attr("x2", parseInt(labelStyle.fontSize) + 5)
+ .attr("y1", -y)
+ .attr("y2", -y)
+ .style("stroke", scaleBarLineStyle.stroke)
+ .style("stroke-width", scaleBarLineStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsLine);
+ });
+
+ // Add the ticks
+ [parseInt(labelStyle.fontSize) + 5, parseInt(labelStyle.fontSize) + 5 + scaleBarLength].forEach(d => {
+ g.append("line")
+ .attr("x1", d)
+ .attr("x2", d)
+ .attr("y1", -y - 5)
+ .attr("y2", -y + 5)
+ .style("stroke", scaleBarTickStyle.stroke)
+ .style("stroke-width", scaleBarTickStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsTick);
+ });
+ });
+
+ // Determine the x position of the title based on the labelPosition within labelStyle and adjust with textPadding
+ const titleX = labelStyle.labelPosition === "left" ? (parseInt(labelStyle.fontSize) - textPadding) : (parseInt(labelStyle.fontSize) + 5 + scaleBarLength + textPadding);
+ const textAnchor = labelStyle.labelPosition === "left" ? "end" : "start";
+
+ // Add the title
+ g.append("text")
+ .attr("x", titleX)
+ .attr("y", -y)
+ .style("text-anchor", textAnchor)
+ .style("dominant-baseline", "middle")
+ .style("font-size", labelStyle.fontSize)
+ .style("font-family", labelStyle.fontFamily)
+ .style("cursor", labelStyle.cursor)
+ .style("fill", labelStyle.fill) // Apply text color
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsLabel);
+ })
+ .text(title);
+
+ return this;
+};
+
+container.prototype.labels = function (label, show = true, options = {}) {
+
+ if (!show) {
+ return this;
+ }
+
+ // Verify that the data exists
+ if (!this.data) {
+ console.error('No data has been added to this cluster container. Please use the addGeneData() function before attempting to draw genes.');
+ return this;
+ }
+
+ const defaultOptions = {
+ x: 0,
+ y: 50,
+ dy: "-1.2em",
+ dx: "0em",
+ rotate: 0,
+ start: null,
+ end: null,
+ adjustLabels: true,
+ fontSize: "12px",
+ fontStyle: "italic",
+ fontFamily: "sans-serif",
+ textAnchor: "middle",
+ cursor: "default",
+ labelAdjustmentOptions: {
+ rotation: 65,
+ offsetX: 0,
+ offsetY: 0,
+ dx: "0em",
+ dy: "0em"
+ },
+ itemStyle: [] // [{"index": 3,"y": 20}]
+ };
+
+ // If theme options exist, use them as the default options
+ if (this.themeOptions && this.themeOptions.labelsOptions) {
+ options = { ...this.themeOptions.labelsOptions, ...options };
+ }
+
+ const combinedOptions = { ...defaultOptions, ...options };
+ const { x, y, start, end, adjustLabels, labelAdjustmentOptions, itemStyle, dx, dy, anchor, rotate, fontSize, fontStyle, fontFamily, textAnchor, cursor } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptions = extractAdditionalOptions(combinedOptions, defaultOptions);
+
+ // Placeholder function for getUniqueId
+ const getUniqueId = (label) => label; // Replace with your actual implementation
+
+ // Create the group
+ const g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left},${this.margin.top})`);
+
+ // Sort the data first by the minimum value of start and end.
+ this.data.sort((a, b) => Math.min(a.start, a.end) - Math.min(b.start, b.end));
+
+ // Check for existing labels
+ const existingLabels = g.selectAll("text.label");
+
+ const getAttributesForIndex = (d, i) => {
+ const style = itemStyle.find(s => s.index === i) || {};
+ const currentX = style.x || x;
+ const currentY = style.y || y;
+
+ const currentDx = style.dx || dx;
+ const currentDy = style.dy || dy;
+ const currentRotate = style.rotate || rotate;
+ var currentLabelAdjustmentOptions = style.labelAdjustmentOptions || labelAdjustmentOptions;
+
+ if(!this.separateStrands){
+ currentLabelAdjustmentOptions.rotation = -Math.abs(currentLabelAdjustmentOptions.rotation);
+ }
+ else if (d.strand === "forward") {
+ currentLabelAdjustmentOptions.rotation = -Math.abs(currentLabelAdjustmentOptions.rotation);
+ } else {
+ currentLabelAdjustmentOptions.rotation = Math.abs(currentLabelAdjustmentOptions.rotation);
+ }
+
+ const currentAdjustLabels = style.adjustLabels !== undefined ? style.adjustLabels : adjustLabels;
+
+ const xPos = this.xScale((d.start + d.end) / 2) + currentX;
+
+ const currentGeneStrandSpacing = d.strand == "forward" ? -this.geneStrandSpacing : this.geneStrandSpacing * 4;
+ var currentOverlapSpacing = d.geneTrack ? (d.geneTrack - 1) * this.geneOverlapSpacing : 0;
+ const yPos = this.yScale(currentY) + currentGeneStrandSpacing - currentOverlapSpacing;
+
+ return {
+ xPos,
+ yPos,
+ dx: currentDx,
+ dy: currentDy,
+ rotate: currentRotate,
+ labelAdjustmentOptions: currentLabelAdjustmentOptions,
+ adjustLabels: currentAdjustLabels
+ };
+ };
+
+ const self = this;
+ // Adding the Label
+ g.selectAll("text.label")
+ .data(this.data)
+ .enter()
+ .append("text")
+ .attr("id", (d, i) => `cluster-${sanitizeId(d.cluster)}-label-${i}`)
+ .attr("rowID", (d, i) => `${d["rowID"]}`)
+ .attr("class", "label")
+ .attr("x", (d, i) => getAttributesForIndex(d, i).xPos)
+ .attr("y", (d, i) => getAttributesForIndex(d, i).yPos)
+ .attr("dx", (d, i) => getAttributesForIndex(d, i).dx)
+ .attr("dy", (d, i) => getAttributesForIndex(d, i).dy)
+ .attr("text-anchor", textAnchor)
+ .attr("transform", (d, i) => {
+ const xPos = getAttributesForIndex(d, i).xPos;
+ const yPos = getAttributesForIndex(d, i).yPos;
+ const rotateValue = getAttributesForIndex(d, i).rotate;
+ return `rotate(${rotateValue}, ${xPos}, ${yPos})`;
+ })
+ .style("font-size", fontSize)
+ .style("font-style", fontStyle)
+ .style("font-family", fontFamily)
+ .style("cursor", cursor)
+ .text(d => d[label])
+ .each(function (d, i) {
+
+ const currentElement = d3.select(this);
+ const attributes = getAttributesForIndex(d, i);
+
+ if (attributes.adjustLabels) {
+ adjustSpecificLabel(self, "text.label", currentElement.attr("id"), attributes.labelAdjustmentOptions);
+ }
+ // Set additional options as attributes
+ setStyleFromOptions(currentElement, additionalOptions);
+ // Override with itemStyle based on the index
+ applyStyleToElement(currentElement, itemStyle, i);
+
+ });
+
+ //Make markers available to tooltip
+ this.labels = g.selectAll(".label");
+
+ return this;
+};
+
+container.prototype.tooltip = function (show = true, options = {}) {
+ if (!show) {
+ return this;
+ }
+
+ const defaultOptions = {
+ triggers: ["markers", "genes", "labels"],
+ formatter: "Start: {start}
End: {end}",
+ opacity: 0,
+ position: "absolute",
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
+ padding: "8px",
+ borderRadius: "4px",
+ border: "1px solid rgba(0,0,0,0.1)",
+ boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
+ pointerEvents: "none",
+ fontFamily: "Arial, sans-serif",
+ fontSize: "12px",
+ zIndex: 1000,
+ color: "#333",
+ lineHeight: "1.5"
+ };
+
+ // If theme options exist, use them as the default options
+ if (this.themeOptions && this.themeOptions.tooltipOptions) {
+ options = { ...this.themeOptions.tooltipOptions, ...options };
+ }
+
+ const combinedOptions = { ...defaultOptions, ...options };
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptions = extractAdditionalOptions(combinedOptions, defaultOptions);
+
+
+ // Generate CSS for the tooltip and its pseudo-element
+ const generateTooltipCSS = (opts, additionalOpts) => {
+ let additionalStyles = Object.entries(additionalOpts).map(([key, value]) => `${camelToKebab(key)}: ${value};`).join(' ');
+ return `
+ .cluster-tooltip {
+ ${additionalStyles}
+ opacity: ${opts.opacity};
+ position: ${opts.position};
+ background-color: ${opts.backgroundColor};
+ padding: ${opts.padding};
+ border-radius: ${opts.borderRadius};
+ border: ${opts.border};
+ box-shadow: ${opts.boxShadow};
+ pointer-events: ${opts.pointerEvents};
+ font-family: ${opts.fontFamily};
+ font-size: ${opts.fontSize};
+ z-index: ${opts.zIndex};
+ color: ${opts.color};
+ line-height: ${opts.lineHeight};
+ }
+ .cluster-tooltip::before {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-width: 5px;
+ border-style: solid;
+ border-color: ${opts.backgroundColor} transparent transparent transparent;
+ }
+ `;
+ };
+
+ // Inject the generated CSS into the document
+ const styleTag = document.createElement("style");
+ styleTag.innerHTML = generateTooltipCSS(combinedOptions, additionalOptions);
+ document.head.appendChild(styleTag);
+
+ // Ensure triggers is an array
+ if (typeof combinedOptions.triggers === 'string') {
+ combinedOptions.triggers = [combinedOptions.triggers];
+ }
+
+ // Create the tooltip div if it doesn't exist
+ let tooltip = d3.select("body").select(".cluster-tooltip");
+ if (tooltip.empty()) {
+ tooltip = d3.select("body")
+ .append("div")
+ .attr("class", "cluster-tooltip")
+ .style("opacity", combinedOptions.opacity)
+ .style("position", combinedOptions.position)
+ .style("background-color", combinedOptions.backgroundColor)
+ .style("padding", combinedOptions.padding)
+ .style("border-radius", combinedOptions.borderRadius)
+ .style("border", combinedOptions.border)
+ .style("box-shadow", combinedOptions.boxShadow)
+ .style("pointer-events", combinedOptions.pointerEvents)
+ .style("font-family", combinedOptions.fontFamily)
+ .style("font-size", combinedOptions.fontSize)
+ .style("z-index", combinedOptions.zIndex)
+ .style("color", combinedOptions.color)
+ .style("line-height", combinedOptions.lineHeight);
+ }
+
+ // Function to generate tooltip content
+ const d3Format = d3.format(",");
+
+ const textAccessor = (d) => {
+ return combinedOptions.formatter.replace(/\{(\w+)\}/g, (match, p1) => {
+ if (typeof d[p1] === 'number') {
+ return d3Format(d[p1]);
+ }
+ return d[p1] || '';
+ });
+ };
+
+ combinedOptions.triggers.forEach(trigger => {
+
+ if (!this.hasOwnProperty(trigger)) {
+ return;
+ }
+
+ const selection = this[trigger];
+
+ // Check if the selection exists and is not empty
+ if (!selection || selection.empty()) {
+ return; // Skip this iteration of the loop
+ }
+
+ // Mouseover event to show the tooltip
+ selection.on("mouseover", (event, d) => {
+ const dataPoint = this.data.find(item => item === d);
+ const x = event.pageX;
+ const y = event.pageY;
+
+ const element = d3.select(event.currentTarget);
+ element.classed("hovered", true);
+
+ tooltip.transition()
+ .duration(200)
+ .style("opacity", 1);
+ tooltip.html(textAccessor(dataPoint))
+ .style("left", (x - tooltip.node().offsetWidth / 2) + "px")
+ .style("top", (y - tooltip.node().offsetHeight - 15) + "px");
+ });
+
+ // Mousemove event to reposition the tooltip as the mouse moves
+ selection.on("mousemove", (event, d) => {
+ const x = event.pageX;
+ const y = event.pageY;
+
+ tooltip.style("left", (x - tooltip.node().offsetWidth / 2) + "px")
+ .style("top", (y - tooltip.node().offsetHeight - 15) + "px");
+ });
+
+ // Mouseout event to hide the tooltip
+ selection.on("mouseout", () => {
+
+ const element = d3.select(event.currentTarget);
+ element.classed("hovered", false);
+
+ tooltip.transition()
+ .duration(500)
+ .style("opacity", 0);
+ });
+ });
+
+ return this; // Return the instance for method chaining
+};
+
+container.prototype.genes = function (group, show = true, options = {}) {
+
+ if (!show) {
+ return this;
+ }
+
+ if (!this.data) {
+ console.error('No data has been added to this cluster container. Please use the geneData() function before attempting to draw arrows.');
+ return this;
+ }
+
+ const defaultOptions = {
+ x: 1,
+ y: 50,
+ stroke: "black",
+ strokeWidth: 1,
+ colorScheme: null,
+ customColors: null,
+ cursor: "default",
+ itemStyle: [],
+ arrowheadWidth: 10,
+ arrowheadHeight: 20,
+ arrowHeight: 10
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'geneOptions', options);
+ const { x, y, stroke, strokeWidth, colorScheme, customColors, cursor, itemStyle, arrowheadWidth, arrowheadHeight, arrowHeight } = combinedOptions;
+
+ // Extract additional options that aren't in defaultOptions
+ const additionalOptions = extractAdditionalOptions(combinedOptions, defaultOptions);
+
+ const uniqueGroups = [...new Set(this.dataAll.map(d => d[group]))];
+
+ const colorScale = getColorScale(colorScheme, customColors, uniqueGroups);
+
+ var g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Sort the data first by the minimum value of start and end.
+ this.data.sort((a, b) => Math.min(a.start, a.end) - Math.min(b.start, b.end));
+
+ this.geneStrandSpacing = this.separateStrands ? (arrowHeight + this.strandSpacing) : 0;
+ this.geneOverlapSpacing = (arrowHeight + this.overlapSpacing)
+
+ const getAttributesForIndex = (d, i) => {
+ const style = itemStyle.find(s => s.index === i) || {};
+ // Apply custom values from itemStyle or default if not provided
+ const currentArrowheadWidth = style.arrowheadWidth || arrowheadWidth;
+ const currentArrowheadHeight = style.arrowheadHeight || arrowheadHeight;
+ const currentArrowHeight = style.arrowHeight || arrowHeight;
+ const currentX = style.x || x;
+ const currentY = style.y || y;
+ // Calculate Y position based on geneTrack
+ const currentGeneStrandSpacing = d.strand == "forward" ? -this.geneStrandSpacing : this.geneStrandSpacing;
+ var currentOverlapSpacing = d.geneTrack ? (d.geneTrack - 1) * this.geneOverlapSpacing : 0;
+
+ const yPos = this.yScale(currentY) + currentGeneStrandSpacing - currentOverlapSpacing;
+ const xPos = this.xScale(d.start);
+
+ return { xPos, yPos, currentArrowheadWidth, currentArrowheadHeight, currentArrowHeight };
+ };
+
+ g.selectAll(".gene")
+ .data(this.data)
+ .enter()
+ .append("path")
+ .attr("d", (d, i) => {
+
+ const { currentArrowheadWidth, currentArrowheadHeight, currentArrowHeight } = getAttributesForIndex(d, i);
+ const geneLength = Math.abs(this.xScale(d.end) - this.xScale(d.start));
+ let shaftLength = geneLength - currentArrowheadWidth;
+ shaftLength = Math.max(0, shaftLength);
+
+ const shaftTop = (currentArrowheadHeight - currentArrowHeight) / 2;
+ const shaftBottom = shaftTop + currentArrowHeight;
+
+ const shaftPath =
+ `M0 ${shaftTop}
+ L0 ${shaftBottom}
+ L${shaftLength} ${shaftBottom}
+ L${shaftLength} ${currentArrowheadHeight}
+ L${geneLength} ${(currentArrowheadHeight / 2)}
+ L${shaftLength} 0
+ L${shaftLength} ${shaftTop} Z`;
+
+ return shaftPath;
+ })
+ .attr("transform", (d, i) => {
+ const { xPos, yPos, currentArrowheadHeight } = getAttributesForIndex(d, i);
+ const rotation = this.reverse
+ ? (d.direction === 'forward' ? 180 : 0)
+ : (d.direction === 'forward' ? 0 : 180);
+ return `rotate(${rotation}, ${xPos}, ${yPos}) translate(${xPos}, ${yPos - (currentArrowheadHeight / 2)})`;
+ })
+ .attr("fill", (d) => colorScale(d[group]))
+ .attr("class", "gene")
+ .attr("id", (d, i) => `${sanitizeId(d.cluster)}-gene-${i}`)
+ .attr("rowID", (d, i) => `${d["rowID"]}`)
+ .attr("start", (d, i) => `${d["start"]}`)
+ .attr("end", (d, i) => `${d["end"]}`)
+ .attr("strand", (d, i) => `${d["strand"]}`)
+ .attr("cluster", (d, i) => `${d["cluster"]}`)
+ .style("stroke-width", strokeWidth)
+ .style("stroke", stroke)
+ .style("cursor", cursor)
+ .each(function (d, i) {
+ const currentElement = d3.select(this);
+ // Set additional options as attributes
+ setStyleFromOptions(currentElement, additionalOptions);
+ // Override with itemStyle based on the index
+ applyStyleToElement(currentElement, itemStyle, i);
+ });
+
+ // Update the reference
+ this.genes = g.selectAll(".gene");
+
+ return this;
+};
+
+container.prototype.legendData = function (data) {
+
+ this.data = [...new Set(data)];
+
+ return this;
+
+};
+
+container.prototype.legend = function (group, show = true, parentId = null, options = {}) {
+ if (!show) {
+ return this;
+ }
+
+ const defaultOptions = {
+ x: 0,
+ y: 0,
+ width: null, // Default width set to null
+ orientation: "horizontal",
+ adjustHeight: true,
+ order: [],
+ legendOptions: {
+ cursor: "pointer",
+ colorScheme: null,
+ customColors: null
+ },
+ legendTextOptions: {
+ cursor: "pointer",
+ textAnchor: "start",
+ dy: ".35em",
+ fontSize: "12px",
+ fontFamily: "sans-serif"
+ }
+ };
+
+ const combinedOptions = mergeOptions.call(this, defaultOptions, 'legendOptions', options);
+ const { x, y, width, orientation, adjustHeight, order, legendOptions, legendTextOptions } = combinedOptions;
+
+ const additionalLegendOptions = extractAdditionalOptions(legendOptions, defaultOptions.legendOptions);
+ const additionalLegendTextOptions = extractAdditionalOptions(legendTextOptions, defaultOptions.legendTextOptions);
+
+ const svgLegend = this.svg;
+ const parentWidth = computeSize(width, svgLegend.node().getBoundingClientRect().width) ||
+ svgLegend.node().getBoundingClientRect().width;
+
+ let uniqueGroups = [...new Set(this.data.map(d => d[group]))];
+
+ const colorScale = getColorScale(legendOptions.colorScheme, legendOptions.customColors, uniqueGroups);
+
+ if (order && order.length > 0) {
+ uniqueGroups = order
+ .filter(item => uniqueGroups.includes(item))
+ .concat(uniqueGroups.filter(item => !order.includes(item)));
+ }
+
+ if (!uniqueGroups.length) {
+ console.error(`Error: No labels provided and the group "${group}" does not exist in the data.`);
+ return;
+ }
+
+ const legendSize = parseFloat(legendTextOptions.fontSize);
+ const legendPadding = legendSize / 2;
+
+ let currentX = x;
+ let currentY = y;
+
+ var g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ g.selectAll(".legend")
+ .data(uniqueGroups)
+ .enter()
+ .append("g")
+ .attr("class", "legend")
+ .each((d, i, nodes) => {
+ const legendGroup = d3.select(nodes[i]);
+
+ const textLabel = legendGroup
+ .append("text")
+ .attr("class", "legend-label")
+ .attr("id", (d, i) => `legend-label-${i}`)
+ .attr("dy", legendTextOptions.dy)
+ .style("text-anchor", legendTextOptions.textAnchor)
+ .style("font-size", legendTextOptions.fontSize)
+ .style("font-family", legendTextOptions.fontFamily)
+ .style("cursor", legendTextOptions.cursor)
+ .text(d)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalLegendTextOptions);
+ });
+
+ const textLength = textLabel.node().getComputedTextLength();
+ const availableWidth = this.width - this.margin.left - this.margin.right;
+ const totalItemWidth = textLength + legendSize + 2 * legendPadding;
+
+ if (currentX + totalItemWidth > availableWidth) {
+ currentX = x;
+ currentY += legendSize + legendPadding;
+ }
+
+ textLabel
+ .attr("x", currentX + legendSize + legendPadding)
+ .attr("y", currentY + legendSize / 2);
+
+ const rect = legendGroup
+ .append("rect")
+ .attr("class", "legend-marker")
+ .attr("id", (d, i) => `legend-marker-${i}`)
+ .style("cursor", legendOptions.cursor)
+ .attr("x", currentX)
+ .attr("y", currentY)
+ .attr("width", legendSize)
+ .attr("height", legendSize)
+ .style("fill", colorScale(d))
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalLegendOptions);
+ })
+
+ if (orientation === "horizontal") {
+ currentX += textLength + legendSize + 2 * legendPadding;
+ } else {
+ currentY += legendSize + legendPadding;
+ }
+ })
+ .on("mouseover", (event, d) => {
+ const element = d3.select(event.currentTarget);
+ element.classed("hovered", true);
+ })
+ .on("mouseout", (event, d) => {
+ const element = d3.select(event.currentTarget);
+ element.classed("hovered", false);
+ })
+ .on("click", (event, d) => {
+ const element = d3.select(event.currentTarget);
+ // If it's currently highlighted, unhighlight it, else highlight it
+ if (element.classed("unselected")) {
+ element.classed("unselected", false);
+ } else {
+ element.classed("unselected", true);
+ }
+
+ const unselectedLegend = d3.selectAll(".unselected").data();
+ const unselectedRowIds = this.data
+ .filter(item => unselectedLegend.includes(item[group]))
+ .map(item => item.rowID);
+
+ if (parentId && typeof parentId === 'string') {
+ // If parentId is not null and is a string, select within the parent
+ d3.select('#' + parentId).selectAll('[rowID]').each(function () {
+ const currentRowID = +d3.select(this).attr("rowID"); // Convert string to number
+ if (unselectedRowIds.includes(currentRowID)) {
+ d3.select(this).style("display", "none"); // Hide it
+ } else {
+ d3.select(this).style("display", ""); // Show it
+ }
+ });
+ } else {
+ // If parentId is null or not a string, select globally
+ d3.selectAll('[rowID]').each(function () {
+ const currentRowID = +d3.select(this).attr("rowID"); // Convert string to number
+ if (unselectedRowIds.includes(currentRowID)) {
+ d3.select(this).style("display", "none"); // Hide it
+ } else {
+ d3.select(this).style("display", ""); // Show it
+ }
+ });
+}
+ });
+
+ if (adjustHeight && this.height === 0) {
+ var padding = 20;
+ var contentHeight = currentY + legendSize + legendPadding + padding;
+
+ svgLegend.attr("height", contentHeight);
+ var viewBoxWidth = parentWidth;
+ svgLegend.attr("viewBox", `0 0 ${viewBoxWidth} ${contentHeight}`);
+ g.attr("transform", `translate(${this.margin.left}, ${this.margin.top + padding / 2})`);
+
+ }
+
+ return this;
+};
+
+container.prototype.links = function (links, clusterKey) {
+
+ if (!links || links.length === 0) {
+ return this;
+ }
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ const hasReverseStrand = this.data.some(item => item.strand === "reverse");
+ const clusterStrandSpacing = hasReverseStrand ? this.geneStrandSpacing * 1 : 0;
+
+ links.forEach(link => {
+ // Check if the link is relevant to cluster1
+ if (link.cluster1 === clusterKey) {
+ group.append("circle")
+ .attr("cx", this.xScale(link.start1))
+ .attr("cy", this.yScale(50) + clusterStrandSpacing)
+ .attr("position", link.start1)
+ .attr("r", 0)
+ .attr("cluster", clusterKey)
+ .attr("linkID", link.linkID)
+ .attr("class", "link-marker");
+
+ group.append("circle")
+ .attr("cx", this.xScale(link.end1))
+ .attr("cy", this.yScale(50) + clusterStrandSpacing)
+ .attr("position", link.end1)
+ .attr("class", "link-marker")
+ .attr("linkID", link.linkID)
+ .attr("cluster", clusterKey)
+ .attr("r", 0);
+ }
+
+ // Check if the link is relevant to cluster2
+ if (link.cluster2 === clusterKey) {
+
+ group.append("circle")
+ .attr("cx", this.xScale(link.start2))
+ .attr("cy", this.yScale(50) - clusterStrandSpacing)
+ .attr("position", link.start2)
+ .attr("class", "link-marker")
+ .attr("cluster", clusterKey)
+ .attr("linkID", link.linkID)
+ .attr("r", 0);
+
+ group.append("circle")
+ .attr("cx", this.xScale(link.end2))
+ .attr("cy", this.yScale(50) - clusterStrandSpacing)
+ .attr("position", link.end2)
+ .attr("class", "link-marker")
+ .attr("linkID", link.linkID)
+ .attr("cluster", clusterKey)
+ .attr("r", 0);
+ }
+ });
+
+ return this;
+};
+
+// Annotations
+
+container.prototype.trackMouse = function(track = true) {
+ if (!track) {
+ return this;
+ }
+
+ // Change cursor to crosshair
+ this.svg.style("cursor", "crosshair");
+ this.svg.selectAll("*").each(function() {
+ this.style.cssText += "cursor: crosshair !important;";
+ });
+
+ // Tooltip for displaying coordinates
+ const tooltip = d3.select("body").append("div")
+ .attr("class", "coordinate-tooltip")
+ .style("background-color", "rgba(255, 255, 255, 0.9)")
+ .style("padding", "8px")
+ .style("border-radius", "4px")
+ .style("border", "1px solid rgba(0,0,0,0.1)")
+ .style("box-shadow", "0 4px 6px rgba(0, 0, 0, 0.1)")
+ .style("pointer-events", "none")
+ .style("font-family", "Arial, sans-serif")
+ .style("font-size", "12px")
+ .style("color", "#333")
+ .style("line-height", "1.5")
+ .style("position", "absolute")
+ .style("visibility", "hidden")
+ .style("z-index", "1000");
+
+ const xScale = d3.scaleLinear().domain([0 + this.margin.left, this.width - this.margin.right]).range([0, 100]);
+ const yScale = d3.scaleLinear().domain([this.height - this.margin.bottom, 0 + this.margin.top]).range([0, 100]);
+ const linearScale = d3.scaleLinear().domain([0 - this.margin.left, this.width]).range([0, 100]);
+
+ this.svg.on("mousemove", (event) => {
+ const [x, y] = d3.pointer(event);
+ const adjustedX = x - this.margin.left
+ loci = Math.round(this.xScale.invert(adjustedX))
+ const format = d3.format(",");
+
+ tooltip.html(`x: ${format(loci)}
y: ${yScale(y).toFixed(1)}`)
+ .style("visibility", "visible")
+ .style("left", (event.pageX + 10) + "px")
+ .style("top", (event.pageY - 10) + "px");
+ });
+
+ this.svg.on("mouseout", () => {
+ tooltip.style("visibility", "hidden");
+ });
+
+ return this;
+};
+
+container.prototype.addAnnotations = function (annotations) {
+ if (!annotations || annotations.length === 0) {
+ return this;
+ }
+
+ var g = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+ annotations.forEach(annotation => {
+ this.createAnnotation(g, annotation);
+ });
+
+ return this;
+};
+
+container.prototype.createAnnotation = function (group, options) {
+
+ switch (options.type) {
+ case 'text':
+ this.createTextAnnotation(group, options);
+ break;
+ case 'line':
+ this.createLineAnnotation(group, options);
+ break;
+ case 'textMarker':
+ this.createTextMarkerAnnotation(group, options);
+ break;
+ case 'arrow':
+ this.createArrowAnnotation(group, options);
+ break;
+ case 'promoter':
+ this.createPromoterAnnotation(group, options);
+ break;
+ case 'terminator':
+ this.createTerminatorAnnotation(group, options);
+ break;
+ case 'rectangle':
+ this.createRectangleAnnotation(group, options);
+ break;
+ case 'symbol':
+ this.createSymbolAnnotation(group, options);
+ break;
+ default:
+ console.warn('Unsupported annotation type:', options.type);
+ }
+};
+
+container.prototype.createTextAnnotation = function (group, options) {
+ // Define default styles for text annotations
+ const defaultOptions = {
+ x: 0,
+ y: 60,
+ text: '',
+ style: {
+ fontSize: "10px",
+ fontStyle: "normal",
+ fontWeight: "normal",
+ textDecoration: "none",
+ fontFamily: "sans-serif",
+ cursor: "default"
+ }
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "textAnnotationOptions", options);
+ let { x, y, text, style } = combinedOptions;
+
+ // Convert x, y, and text to arrays if they are not already
+ if (!Array.isArray(x)) x = [x];
+ if (!Array.isArray(y)) y = [y];
+ if (!Array.isArray(text)) text = [text];
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each element in the arrays
+ for (let i = 0; i < Math.max(x.length, y.length, text.length); i++) {
+ const currentX = x[Math.min(i, x.length - 1)];
+ const currentY = y[Math.min(i, y.length - 1)];
+ const currentText = text[Math.min(i, text.length - 1)];
+
+ // Create the text element with merged styles for each set of values
+ group.append("text")
+ .attr("x", this.xScale(currentX))
+ .attr("y", this.yScale(currentY))
+ .style("font-size", style.fontSize)
+ .style("font-style", style.fontStyle)
+ .style("font-weight", style.fontWeight)
+ .style("text-decoration", style.textDecoration)
+ .style("font-family", style.fontFamily)
+ .style("cursor", style.cursor)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(currentText, currentElement, style);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+};
+
+container.prototype.createLineAnnotation = function (group, options) {
+ const defaultOptions = {
+ x1: 0,
+ y1: 70,
+ x2: 0,
+ y2: 50,
+ style: {
+ stroke: "black",
+ strokeWidth: 1
+ }
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "lineAnnotationOptions", options);
+ let { x1, y1, x2, y2, style } = combinedOptions;
+
+ // Convert x1, y1, x2, y2 to arrays if they are not already
+ if (!Array.isArray(x1)) x1 = [x1];
+ if (!Array.isArray(y1)) y1 = [y1];
+ if (!Array.isArray(x2)) x2 = [x2];
+ if (!Array.isArray(y2)) y2 = [y2];
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each set of line coordinates
+ for (let i = 0; i < Math.max(x1.length, y1.length, x2.length, y2.length); i++) {
+ const currentX1 = x1[Math.min(i, x1.length - 1)];
+ const currentY1 = y1[Math.min(i, y1.length - 1)];
+ const currentX2 = x2[Math.min(i, x2.length - 1)];
+ const currentY2 = y2[Math.min(i, y2.length - 1)];
+
+ // Create the line element with merged styles for each set of values
+ group.append("line")
+ .attr("x1", this.xScale(currentX1))
+ .attr("y1", this.yScale(currentY1))
+ .attr("x2", this.xScale(currentX2))
+ .attr("y2", this.yScale(currentY2))
+ .style("stroke", style.stroke)
+ .style("stroke-width", style.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+};
+
+container.prototype.createArrowAnnotation = function (group, options) {
+ const defaultOptions = {
+ x1: 1,
+ y1: 70,
+ x2: 1,
+ y2: 50,
+ arrowSize: 8,
+ arrowStyle: {
+ fill: "black",
+ },
+ lineStyle: {
+ stroke: "black",
+ strokeWidth: 1
+ }
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "arrowAnnotationOptions", options);
+ let { x1, y1, x2, y2, arrowSize, arrowStyle, lineStyle } = combinedOptions;
+
+ // Convert x1, y1, x2, y2, and arrowSize to arrays if they are not already
+ if (!Array.isArray(x1)) x1 = [x1];
+ if (!Array.isArray(y1)) y1 = [y1];
+ if (!Array.isArray(x2)) x2 = [x2];
+ if (!Array.isArray(y2)) y2 = [y2];
+ if (!Array.isArray(arrowSize)) arrowSize = [arrowSize];
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsLine = extractAdditionalOptions(lineStyle, defaultOptions.lineStyle);
+ const additionalOptionsArrow = extractAdditionalOptions(arrowStyle, defaultOptions.arrowStyle);
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each element in the arrays
+ for (let i = 0; i < Math.max(x1.length, y1.length, x2.length, y2.length, arrowSize.length); i++) {
+ const currentX1 = x1[Math.min(i, x1.length - 1)];
+ const currentY1 = y1[Math.min(i, y1.length - 1)];
+ const currentX2 = x2[Math.min(i, x2.length - 1)];
+ const currentY2 = y2[Math.min(i, y2.length - 1)];
+ const currentArrowSize = arrowSize[Math.min(i, arrowSize.length - 1)];
+
+ // Create a marker for each line
+ this.svg.append("defs").append("marker")
+ .attr("id", `arrowhead-${i}-${currentX1}-${currentX2}`)
+ .attr("viewBox", "-0 -5 10 10")
+ .attr("refX", 5)
+ .attr("refY", 0)
+ .attr("orient", "auto")
+ .attr("markerWidth", currentArrowSize)
+ .attr("markerHeight", currentArrowSize)
+ .attr("xoverflow", "visible")
+ .append("path")
+ .attr("d", "M 0,-5 L 10 ,0 L 0,5")
+ .attr("fill", arrowStyle.fill)
+ .each(function () {
+ d3.select(this).style(additionalOptionsArrow);
+ });
+
+ // Draw the line with the arrow marker for each set of values
+ group.append("line")
+ .attr("x1", this.xScale(currentX1))
+ .attr("y1", this.yScale(currentY1))
+ .attr("x2", this.xScale(currentX2))
+ .attr("y2", this.yScale(currentY2 + currentArrowSize / 2))
+ .attr("marker-end", `url(#arrowhead-${i}-${currentX1}-${currentX2})`)
+ .style("stroke", lineStyle.stroke)
+ .style("stroke-width", lineStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsLine);
+ });
+ }
+};
+
+container.prototype.createTextMarkerAnnotation = function (group, options) {
+ const defaultOptions = {
+ x1: null,
+ y1: 66,
+ x2: null,
+ y2: 50,
+ position: 0,
+ text: "",
+ labelX: 0,
+ labelY: 0,
+ showArrow: false,
+ arrowSize: 8,
+ textStyle: {
+ fontSize: "10px",
+ fontFamily: "sans-serif",
+ fill: "black",
+ textAnchor: "middle"
+ },
+ arrowStyle: {
+ fill: "black",
+ },
+ lineStyle: {
+ stroke: "black",
+ strokeWidth: 1
+ }
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "textMarkerAnnotationOptions", options);
+ let { textStyle, arrowStyle, lineStyle } = combinedOptions;
+
+ // Convert all options to arrays if not already
+ const keys = ['x1', 'y1', 'x2', 'y2', 'position', 'text', 'labelX', 'labelY', 'showArrow', 'arrowSize'];
+ keys.forEach(key => {
+ if (!Array.isArray(combinedOptions[key])) {
+ combinedOptions[key] = [combinedOptions[key]];
+ }
+ });
+
+ const additionalOptionsLine = extractAdditionalOptions(lineStyle, defaultOptions.lineStyle);
+ const additionalOptionsArrow = extractAdditionalOptions(arrowStyle, defaultOptions.arrowStyle);
+
+ // Calculate the maximum length across all arrays
+ const maxLength = Math.max(...keys.map(key => combinedOptions[key].length));
+
+ for (let i = 0; i < maxLength; i++) {
+ let currentValues = {};
+ keys.forEach(key => {
+ currentValues[key] = combinedOptions[key][i % combinedOptions[key].length];
+ });
+
+ let offsetY2 = currentValues.showArrow ? currentValues.arrowSize / 2 : 0;
+ var group = this.svg.insert("g", ":first-child")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ const currentX1 = Math.round(this.xScale(currentValues.x1 !== null ? currentValues.x1 : currentValues.position));
+ const currentX2 = Math.round(this.xScale(currentValues.x2 !== null ? currentValues.x2 : currentValues.position));
+
+ // Create line element
+ const line = group.append("line")
+ .attr("x1", currentX1)
+ .attr("y1", this.yScale(currentValues.y1))
+ .attr("x2", currentX2)
+ .attr("y2", this.yScale(currentValues.y2 + offsetY2))
+ .style("stroke", lineStyle.stroke)
+ .style("stroke-width", lineStyle.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsLine);
+ });
+
+ // Create a marker for each line with currentX1 and currentX2 in the ID
+ this.svg.append("defs").append("marker")
+ .attr("id", `arrowhead-marker-${i}-${currentX1}-${currentX2}`)
+ .attr("viewBox", "-0 -5 10 10")
+ .attr("refX", 5)
+ .attr("refY", 0)
+ .attr("orient", "auto")
+ .attr("markerWidth", currentValues.arrowSize)
+ .attr("markerHeight", currentValues.arrowSize)
+ .attr("xoverflow", "visible")
+ .append("path")
+ .attr("d", "M 0,-5 L 10 ,0 L 0,5")
+ .attr("fill", arrowStyle.fill)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsArrow);
+ });
+ // Add arrow marker to line if showArrow is true
+ if (currentValues.showArrow) {
+ line.attr("marker-end", `url(#arrowhead-marker-${i}-${currentX1}-${currentX2})`);
+ }
+
+ // Create text element if text is provided
+ if (currentValues.text) {
+ group.append("text")
+ .attr("x", this.xScale(currentValues.x1 !== null ? currentValues.x1 : currentValues.position) + currentValues.labelX)
+ .attr("y", this.yScale(currentValues.y1) - 5 - currentValues.labelY)
+ .style("text-anchor", textStyle.textAnchor)
+ .style("font-size", textStyle.fontSize)
+ .style("font-family", textStyle.fontFamily)
+ .style("fill", textStyle.fill)
+ .each(function () {
+ const currentElement = d3.select(this);
+ parseAndStyleText(currentValues.text, currentElement, textStyle);
+ setStyleFromOptions(currentElement, extractAdditionalOptions(textStyle, defaultOptions.textStyle));
+ });
+ }
+ }
+
+ return group;
+};
+
+container.prototype.createSymbolAnnotation = function(group, options) {
+ const defaultOptions = {
+ x: 0,
+ y: 50,
+ size: 64,
+ symbol: "circle",
+ style: {
+ fill: "black",
+ stroke: "black",
+ strokeWidth: 2
+ },
+ rotation: 0
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "symbolAnnotationOptions", options);
+ let { x, y, size, symbol, style, rotation } = combinedOptions;
+
+ // Convert x, y, symbol, and rotation to arrays if they are not already
+ if (!Array.isArray(x)) x = [x];
+ if (!Array.isArray(y)) y = [y];
+ if (!Array.isArray(symbol)) symbol = [symbol];
+ if (!Array.isArray(rotation)) rotation = [rotation];
+ if (!Array.isArray(size)) size = [size];
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ // Symbol type mapping
+ const symbolTypes = {
+ 'circle': d3.symbolCircle,
+ 'cross': d3.symbolCross,
+ 'diamond': d3.symbolDiamond,
+ 'square': d3.symbolSquare,
+ 'star': d3.symbolStar,
+ 'triangle': d3.symbolTriangle,
+ 'wye': d3.symbolWye
+ };
+
+ // Iterate over each element in the arrays
+ for (let i = 0; i < Math.max(x.length, y.length, symbol.length, rotation.length); i++) {
+ const currentX = x[Math.min(i, x.length - 1)];
+ const currentY = y[Math.min(i, y.length - 1)];
+ const currentSymbol = symbol[Math.min(i, symbol.length - 1)];
+ const currentRotation = rotation[Math.min(i, rotation.length - 1)];
+ const currentSize = size[Math.min(i, size.length - 1)];
+
+ // Check if the symbol type is valid
+ if (!symbolTypes[currentSymbol]) {
+ console.error(`Unsupported symbol type: ${currentSymbol}`);
+ continue;
+ }
+
+ // Create the symbol
+ const d3Symbol = d3.symbol().type(symbolTypes[currentSymbol]).size(currentSize);
+
+ // Create the symbol element with merged styles for each set of values
+ group.append("path")
+ .attr("d", d3Symbol)
+ .attr("transform", `translate(${this.xScale(currentX)}, ${this.yScale(currentY)}) rotate(${currentRotation})`)
+ .style("fill", style.fill)
+ .style("stroke", style.stroke)
+ .style("stroke-width", style.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+
+ return group;
+};
+
+container.prototype.createRectangleAnnotation = function(group, options) {
+ const defaultOptions = {
+ position: [[[9300, 20], [9400, 50]]], // Default as a nested array for multiple rectangles
+ rotation: [0], // Array to support multiple rotations
+ style: {
+ fill: "#0000",
+ stroke: "black",
+ strokeWidth: 2
+ }
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "rectangleAnnotationOptions", options);
+ let { position, style, rotation } = combinedOptions;
+
+ // Normalize position to be an array of arrays
+ if (!Array.isArray(position[0][0])) {
+ position = [position];
+ }
+ if (!Array.isArray(rotation)) rotation = [rotation];
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each element in the position and rotation arrays
+ const numRects = Math.max(position.length, rotation.length);
+ for (let i = 0; i < numRects; i++) {
+ const currentPos = position[Math.min(i, position.length - 1)];
+ const currentRot = rotation[Math.min(i, rotation.length - 1)];
+
+ // Calculate x, y, width, and height from the position array
+ const x1 = Math.min(currentPos[0][0], currentPos[1][0]);
+ const x2 = Math.max(currentPos[0][0], currentPos[1][0]);
+ const y1 = Math.max(currentPos[0][1], currentPos[1][1]);
+ const y2 = Math.min(currentPos[0][1], currentPos[1][1]);
+ const width = Math.abs(x1 - x2);
+ const height = Math.abs(y2 - y1);
+
+ // Create the rectangle element with merged styles
+ group.append("rect")
+ .attr("x", this.xScale(x1))
+ .attr("y", this.yScale(y1))
+ .attr("width", this.xScale(x2) - this.xScale(x1))
+ .attr("height", this.yScale(y2) - this.yScale(y1))
+ .attr("transform", `rotate(${currentRot}, ${this.xScale(x1 + width / 2)}, ${this.yScale(y1 + height / 2)})`)
+ .style("fill", style.fill)
+ .style("stroke", style.stroke)
+ .style("stroke-width", style.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+};
+
+container.prototype.createPromoterAnnotation = function(group, options) {
+
+ const defaultOptions = {
+ x: 0,
+ y: 50,
+ direction: 'forward',
+ rotation: 0,
+ scale: 1,
+ style: {
+ fill: "none",
+ stroke: "black",
+ strokeWidth: 1
+ },
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "PromoterAnnotationOptions", options);
+ let { x, y, direction, rotation, scale, style } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ if (!Array.isArray(x)) x = [x];
+ if (!Array.isArray(y)) y = [y];
+ if (!Array.isArray(direction)) direction = [direction];
+ if (!Array.isArray(rotation)) rotation = [rotation];
+ if (!Array.isArray(scale)) scale = [scale];
+
+ // Define the custom path and mirrored path
+ const mirroredPath = "M -8 -17.5 L -13 -14 l 5 3.5 M -13 -14 L 0 -14 v 14";
+ const customPath = "M 8 -17.5 L 13 -14 l -5 3.5 M 13 -14 H 0 v 14";
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each element in the arrays
+ for (let i = 0; i < Math.max(x.length, y.length, direction.length, rotation.length, scale.length); i++) {
+
+ const currentX = x[Math.min(i, x.length - 1)];
+ const currentY = y[Math.min(i, y.length - 1)];
+ const currentDirection = direction[Math.min(i, direction.length - 1)];
+ const currentRotation = rotation[Math.min(i, rotation.length - 1)];
+ const currentScale = scale[Math.min(i, scale.length - 1)];
+
+ // Choose the appropriate path based on direction
+ const pathToUse = currentDirection === "forward" ? customPath : mirroredPath;
+
+ // Calculate the x position
+ const xPosition = this.xScale(currentX);
+
+ // Create the symbol element for each set of values
+ group.append("path")
+ .attr("d", pathToUse)
+ .attr("transform", `translate(${xPosition}, ${this.yScale(currentY)}) scale(${currentScale}) rotate(${currentRotation})`)
+ .style("fill", style.fill)
+ .style("stroke", style.stroke)
+ .style("stroke-width", style.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+};
+
+container.prototype.createTerminatorAnnotation = function(group, options) {
+
+ const defaultOptions = {
+ x: 0,
+ y: 50,
+ direction: 'forward',
+ rotation: 0,
+ scale: 1,
+ style: {
+ fill: "none",
+ stroke: "black",
+ strokeWidth: 1
+ },
+ };
+
+ // Merge default options and user-specified options
+ const combinedOptions = mergeOptions.call(this, defaultOptions, "terminatorAnnotationOptions", options);
+ let { x, y, direction, rotation, scale, style } = combinedOptions;
+
+ // Extract additional options that are not in defaultOptions
+ const additionalOptionsStyle = extractAdditionalOptions(style, defaultOptions.style);
+
+ if (!Array.isArray(x)) x = [x];
+ if (!Array.isArray(y)) y = [y];
+ if (!Array.isArray(direction)) direction = [direction];
+ if (!Array.isArray(rotation)) rotation = [rotation];
+ if (!Array.isArray(scale)) scale = [scale];
+
+ // Define the custom paths
+ const customPath = "M -8 17.5 L -13 14 l 5 -3.5 M -13 14 H 0 v -14";
+ const mirroredPath = "M 8 17.5 L 13 14 l -5 -3.5 M 13 14 L 0 14 v -14";
+
+ var group = this.svg.append("g")
+ .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
+
+ // Iterate over each element in the arrays
+ for (let i = 0; i < Math.max(x.length, y.length, direction.length, rotation.length, scale.length); i++) {
+
+ const currentX = x[Math.min(i, x.length - 1)];
+ const currentY = y[Math.min(i, y.length - 1)];
+ const currentDirection = direction[Math.min(i, direction.length - 1)];
+ const currentRotation = rotation[Math.min(i, rotation.length - 1)];
+ const currentScale = scale[Math.min(i, scale.length - 1)];
+
+ // Choose the appropriate path based on direction
+ const pathToUse = currentDirection === "forward" ? customPath : mirroredPath;
+
+ // Calculate the x position
+ const xPosition = this.xScale(currentX);
+
+ // Create the symbol element for each set of values
+ group.append("path")
+ .attr("d", pathToUse)
+ .attr("transform", `translate(${xPosition}, ${this.yScale(currentY)}) scale(${currentScale}) rotate(${currentRotation})`)
+ .style("fill", style.fill)
+ .style("stroke", style.stroke)
+ .style("stroke-width", style.strokeWidth)
+ .each(function () {
+ const currentElement = d3.select(this);
+ setStyleFromOptions(currentElement, additionalOptionsStyle);
+ });
+ }
+};
diff --git a/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/styles.css b/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/styles.css
new file mode 100644
index 0000000..08b8c1a
--- /dev/null
+++ b/docs/articles/LoadFastaFiles_files/D3-7.8.5/lib/styles.css
@@ -0,0 +1,42 @@
+.geneviewer-container {
+ display: flex;
+ flex-direction: column; /* or row, depending on the desired layout */
+ align-content: stretch;
+}
+
+.geneviewer-svg-content {
+ flex: 1 1 auto; /* This makes the SVGs grow and shrink as needed */
+ min-height: 0; /* This may be needed for correct sizing in some browsers */
+}
+
+.hovered {
+ cursor: pointer !important;
+}
+
+.geneviewer-svg-content {
+ z-index: 2;
+}
+
+/* Common styles */
+.legend .legend-marker,
+.legend .legend-label
+.label. {
+ transition: 0.3s ease-in-out; /* Smooth transitions */
+}
+
+/* Hover styles: Gives a dim effect to the marker and text when hovered */
+.legend.hovered .legend-marker,
+.legend.hovered .legend-label,
+.label.hovered {
+ opacity: 0.7;
+}
+
+/* Highlighted/Clicked styles: Gives a more dimmed effect to both marker and text when clicked */
+.legend.unselected .legend-marker,
+.legend.unselected .legend-label {
+ opacity: 0.5;
+}
+
+.gene.hovered {
+ filter: brightness(80%);
+}
diff --git a/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewer.yaml b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewer.yaml
new file mode 100644
index 0000000..9e444ff
--- /dev/null
+++ b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewer.yaml
@@ -0,0 +1,19 @@
+dependencies:
+ - name: geneviewerwidget
+ version: 0.1.4
+ src: htmlwidgets
+ script: geneviewerwidget.js
+ - name: Themes
+ version: 0.1.4
+ src: htmlwidgets
+ script: ./lib/geneviewer-0.1.4/Themes.js
+ - name: geneviewer
+ version: 0.1.4
+ src: htmlwidgets
+ script: ./lib/geneviewer-0.1.4/geneviewer.js
+ - name: D3
+ version: 7.8.5
+ src: htmlwidgets
+ script: ./lib/D3-7.8.5/d3.min.js
+ stylesheet: ./lib/styles.css
+
diff --git a/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewerwidget.js b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewerwidget.js
new file mode 100644
index 0000000..59a687e
--- /dev/null
+++ b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/geneviewerwidget.js
@@ -0,0 +1,198 @@
+HTMLWidgets.widget({
+ name: 'geneviewer',
+ type: 'output',
+
+ factory: function (el, width, height) {
+ var
+ graphContainer,
+ style,
+ data,
+ links,
+ series,
+ titleOptions,
+ legendOptions;
+
+ var widgetId = el.id.split('-')[1];
+
+ var draw = function (width, height) {
+
+ // Clear out the container if it has anything
+ d3.select(el).selectAll('*').remove();
+
+ el.style["height"] = "100%"
+
+ // Apply styles
+ if (style && typeof style === 'object' && Object.keys(style).length > 0) {
+ // Apply styles from the style object
+ for (var key in style) {
+ if (style.hasOwnProperty(key)) {
+ el.style[key] = style[key];
+ }
+ }
+ }
+
+ // Add Title
+
+ if (titleOptions !== null && titleOptions?.height !== null && titleOptions?.show) {
+
+ var titleContainer = d3.select(el)
+ .append("div")
+ .attr("id", `geneviewer-title-container-${widgetId}`)
+ .classed("geneviewer-container", true);
+
+
+ titleOptions.width = el.clientWidth
+ titleOptions.height = computeSize(titleOptions.height, el.clientHeight)
+
+ var title = createContainer(
+ `#geneviewer-title-container-${widgetId}`,
+ "svg-container",
+ "titleOptions",
+ titleOptions)
+ .title(titleOptions?.title, titleOptions?.subtitle, titleOptions?.show ?? false, titleOptions)
+ }
+
+ // Add legend
+
+ var legendHeight = (legendOptions?.show === false) ? 0 : computeSize(legendOptions?.height, el.clientHeight);
+
+ if (legendOptions?.group !== null && legendOptions?.show) {
+
+ var legendContainer = d3.select(el)
+ .append("div")
+ .attr("id", `geneviewer-legend-container-${widgetId}`)
+ .classed("geneviewer-container", true);
+
+ legendOptions.width = width
+ legendOptions.height = computeSize(legendOptions.height, el.clientHeight)
+
+ var legendContainer = createContainer(`#geneviewer-legend-container-${widgetId}`,
+ "svg-container",
+ "legendOptions",
+ legendOptions)
+ .legendData(data)
+ .legend(legendOptions?.group ?? false, legendOptions?.show ?? false, el.id, legendOptions);
+
+ var legendElement = d3.select(`#geneviewer-legend-container-${widgetId}`).node();
+ var legendDimensions = legendElement.getBoundingClientRect();
+ legendHeight = legendDimensions.height;
+
+ }
+
+ var graph = d3.select(el)
+ .append("div")
+ .attr("id", `geneviewer-graph-container-${widgetId}`)
+ .style("flex-direction", graphContainer["direction"])
+ .classed("geneviewer-container", true);
+
+ // Add Clusters
+ var clusters = Object.keys(series);
+ // Add Links
+ if(links && links.length > 0){
+ var graphLinks = links.reduce((acc, entry) => {
+ const convertedData = HTMLWidgets.dataframeToD3(entry.data);
+ acc = acc.concat(convertedData);
+ return acc;
+ }, []);
+ }
+
+ clusters.forEach(function (clusterKey) {
+
+ var cluster = series[clusterKey],
+ containerOptions = cluster.container,
+ clusterOptions = cluster.cluster,
+ clusterData = HTMLWidgets.dataframeToD3(series[clusterKey].data),
+ scaleOptions = cluster.scale,
+ clusterTitleOptions = cluster.clusterTitle,
+ footerOptions = cluster.footer,
+ clusterLabelOptions = cluster.clusterLabel,
+ labelOptions = cluster.labels,
+ sequenceOptions = cluster.sequence,
+ geneOptions = cluster.genes,
+ coordinateOptions = cluster.coordinates;
+ scaleBarOptions = cluster.scaleBar;
+ annotationOptions = cluster.annotations;
+ trackMouse = cluster.trackMouse;
+ tooltipOptions = cluster.tooltip;
+
+ var clonedContainerOptions = JSON.parse(JSON.stringify(containerOptions));
+ clonedContainerOptions.height = computeSize(clonedContainerOptions.height, el.clientHeight);
+ clonedContainerOptions.height -= titleOptions.height ? (titleOptions.height / clusters.length) : 0;
+ clonedContainerOptions.height -= legendHeight ? (legendHeight / clusters.length) : 0;
+ clonedContainerOptions.width = computeSize(clonedContainerOptions.width, el.clientWidth);
+
+ var clusterLinks = getClusterLinks(graphLinks, clusterKey);
+
+ var cluster = createContainer(`#geneviewer-graph-container-${widgetId}`, "svg-container", 'containerOptions', clonedContainerOptions)
+ .cluster(clusterOptions)
+ .theme("preset")
+ .title(clusterTitleOptions?.title, clusterTitleOptions?.subtitle, clusterTitleOptions?.show ?? false, clusterTitleOptions)
+ .footer(footerOptions?.title, footerOptions?.subtitle, footerOptions?.show ?? false, footerOptions)
+ .clusterLabel(clusterLabelOptions?.title, clusterLabelOptions?.show ?? false, clusterLabelOptions)
+ .geneData(data, clusterData)
+ .scale(scaleOptions)
+ .sequence(sequenceOptions?.show ?? false, sequenceOptions)
+ .genes(geneOptions?.group, geneOptions?.show ?? false, geneOptions)
+ .links(clusterLinks, clusterKey)
+ .coordinates(coordinateOptions?.show ?? false, coordinateOptions)
+ .labels(labelOptions?.label, labelOptions?.show ?? false, labelOptions)
+ .scaleBar(scaleBarOptions?.show ?? false, scaleBarOptions)
+ .addAnnotations(annotationOptions)
+ .trackMouse(trackMouse?.show ?? false)
+ .tooltip(tooltipOptions?.show ?? false, tooltipOptions);
+
+ });
+
+ // Bottom Legend
+ if (legendOptions?.position == "bottom" && legendOptions?.show && legendOptions?.group !== null) {
+
+ d3.select(`#geneviewer-legend-container-${widgetId}`).remove();
+
+ var legendContainer = d3.select(el)
+ .append("div")
+ .attr("id", `geneviewer-legend-container-${widgetId}`)
+ .classed("geneviewer-container", true);
+
+ var legendContainer = createContainer(`#geneviewer-legend-container-${widgetId}`,
+ "svg-container",
+ "legendOptions",
+ legendOptions)
+ .legendData(data)
+ .legend(legendOptions?.group ?? false, legendOptions?.show ?? false, el.id, legendOptions);
+
+ }
+
+ };
+
+ var addLinks = function(width, height) {
+
+ if (!links || links.length === 0) {
+ return;
+ }
+ // Remove all existing links
+ const graphContainer = d3.select(`#geneviewer-graph-container-${widgetId}`);
+ //graphContainer.selectAll(".link-marker").remove();
+
+ makeLinks(graphContainer, links);
+
+ };
+
+ return {
+ renderValue: function (input) {
+ graphContainer = input.graphContainer;
+ style = input.style;
+ data = HTMLWidgets.dataframeToD3(input.data);
+ links = input.links;
+ series = input.series;
+ titleOptions = input.title;
+ legendOptions = input.legend;
+ draw(width, height);
+ addLinks(width, height);
+ },
+ resize: function (width, height) {
+ draw(width, height);
+ addLinks(width, height);
+ }
+ };
+ }
+});
diff --git a/docs/articles/LoadFastaFiles_files/Themes-0.1.4/lib/D3-7.8.5/d3.min.js b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/lib/D3-7.8.5/d3.min.js
new file mode 100644
index 0000000..8d56002
--- /dev/null
+++ b/docs/articles/LoadFastaFiles_files/Themes-0.1.4/lib/D3-7.8.5/d3.min.js
@@ -0,0 +1,2 @@
+// https://d3js.org v7.8.5 Copyright 2010-2023 Mike Bostock
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<0?e=r+1:i=r}while(en(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<=0?e=r+1:i=r}while(e{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r=o&&(e=r=o):(e>o&&(e=o),r0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function q(t,n){return Array.from(n,(n=>t[n]))}function U(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=O(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>O(e[t],e[n])))),q(t,r)}return t.sort(I(e))}function I(t=n){if(t===n)return O;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function O(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}var B=Array.prototype.slice;function Y(t){return()=>t}const L=Math.sqrt(50),j=Math.sqrt(10),H=Math.sqrt(2);function X(t,n,e){const r=(n-t)/Math.max(0,e),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=L?10:o>=j?5:o>=H?2:1;let u,c,f;return i<0?(f=Math.pow(10,-i)/a,u=Math.round(t*f),c=Math.round(n*f),u/fn&&--c,f=-f):(f=Math.pow(10,i)*a,u=Math.round(t/f),c=Math.round(n/f),u*fn&&--c),c0))return[];if((t=+t)===(n=+n))return[t];const r=n=i))return[];const u=o-i+1,c=new Array(u);if(r)if(a<0)for(let t=0;t0?(t=Math.floor(t/i)*i,n=Math.ceil(n/i)*i):i<0&&(t=Math.ceil(t*i)/i,n=Math.floor(n*i)/i),r=i}}function K(t){return Math.max(1,Math.ceil(Math.log(v(t))/Math.LN2)+1)}function Q(){var t=k,n=M,e=K;function r(r){Array.isArray(r)||(r=Array.from(r));var i,o,a,u=r.length,c=new Array(u);for(i=0;i=h)if(t>=h&&n===M){const t=V(l,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}for(var p=d.length,g=0,y=p;d[g]<=l;)++g;for(;d[y-1]>h;)--y;(g||y0?d[i-1]:l,v.x1=i
0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function tt(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e=o)&&(e=o,r=i);return r}function nt(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}function et(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e>o||void 0===e&&o>=o)&&(e=o,r=i);return r}function rt(t,n,e=0,r=1/0,i){if(n=Math.floor(n),e=Math.floor(Math.max(0,e)),r=Math.floor(Math.min(t.length-1,r)),!(e<=n&&n<=r))return t;for(i=void 0===i?O:I(i);r>e;){if(r-e>600){const o=r-e+1,a=n-e+1,u=Math.log(o),c=.5*Math.exp(2*u/3),f=.5*Math.sqrt(u*c*(o-c)/o)*(a-o/2<0?-1:1);rt(t,n,Math.max(e,Math.floor(n-a*c/o+f)),Math.min(r,Math.floor(n+(o-a)*c/o+f)),i)}const o=t[n];let a=e,u=r;for(it(t,e,n),i(t[r],o)>0&&it(t,e,r);a0;)--u}0===i(t[e],o)?it(t,e,u):(++u,it(t,u,r)),u<=n&&(e=u+1),n<=u&&(r=u-1)}return t}function it(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function ot(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)>0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)>0:0===e(n,n))&&(r=n,i=!0);return r}function at(t,n,e){if(t=Float64Array.from(function*(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}(t,e)),(r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return nt(t);if(n>=1)return J(t);var r,i=(r-1)*n,o=Math.floor(i),a=J(rt(t,o).subarray(0,o+1));return a+(nt(t.subarray(o+1))-a)*(i-o)}}function ut(t,n,e=o){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,a=Math.floor(i),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(i-a)}}function ct(t,n,e=o){if(!isNaN(n=+n)){if(r=Float64Array.from(t,((n,r)=>o(e(t[r],r,t)))),n<=0)return et(r);if(n>=1)return tt(r);var r,i=Uint32Array.from(t,((t,n)=>n)),a=r.length-1,u=Math.floor(a*n);return rt(i,u,0,a,((t,n)=>O(r[t],r[n]))),(u=ot(i.subarray(0,u+1),(t=>r[t])))>=0?u:-1}}function ft(t){return Array.from(function*(t){for(const n of t)yield*n}(t))}function st(t,n){return[t,n]}function lt(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r+t(n)}function kt(t,n){return n=Math.max(0,t.bandwidth()-2*n)/2,t.round()&&(n=Math.round(n)),e=>+t(e)+n}function Ct(){return!this.__axis}function Pt(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,f=t===xt||t===Tt?-1:1,s=t===Tt||t===wt?"x":"y",l=t===xt||t===Mt?St:Et;function h(h){var d=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,p=null==i?n.tickFormat?n.tickFormat.apply(n,e):mt:i,g=Math.max(o,0)+u,y=n.range(),v=+y[0]+c,_=+y[y.length-1]+c,b=(n.bandwidth?kt:Nt)(n.copy(),c),m=h.selection?h.selection():h,x=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(d,n).order(),M=w.exit(),T=w.enter().append("g").attr("class","tick"),A=w.select("line"),S=w.select("text");x=x.merge(x.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),w=w.merge(T),A=A.merge(T.append("line").attr("stroke","currentColor").attr(s+"2",f*o)),S=S.merge(T.append("text").attr("fill","currentColor").attr(s,f*g).attr("dy",t===xt?"0em":t===Mt?"0.71em":"0.32em")),h!==m&&(x=x.transition(h),w=w.transition(h),A=A.transition(h),S=S.transition(h),M=M.transition(h).attr("opacity",At).attr("transform",(function(t){return isFinite(t=b(t))?l(t+c):this.getAttribute("transform")})),T.attr("opacity",At).attr("transform",(function(t){var n=this.parentNode.__axis;return l((n&&isFinite(n=n(t))?n:b(t))+c)}))),M.remove(),x.attr("d",t===Tt||t===wt?a?"M"+f*a+","+v+"H"+c+"V"+_+"H"+f*a:"M"+c+","+v+"V"+_:a?"M"+v+","+f*a+"V"+c+"H"+_+"V"+f*a:"M"+v+","+c+"H"+_),w.attr("opacity",1).attr("transform",(function(t){return l(b(t)+c)})),A.attr(s+"2",f*o),S.attr(s,f*g).text(p),m.filter(Ct).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===wt?"start":t===Tt?"end":"middle"),m.each((function(){this.__axis=b}))}return h.scale=function(t){return arguments.length?(n=t,h):n},h.ticks=function(){return e=Array.from(arguments),h},h.tickArguments=function(t){return arguments.length?(e=null==t?[]:Array.from(t),h):e.slice()},h.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),h):r&&r.slice()},h.tickFormat=function(t){return arguments.length?(i=t,h):i},h.tickSize=function(t){return arguments.length?(o=a=+t,h):o},h.tickSizeInner=function(t){return arguments.length?(o=+t,h):o},h.tickSizeOuter=function(t){return arguments.length?(a=+t,h):a},h.tickPadding=function(t){return arguments.length?(u=+t,h):u},h.offset=function(t){return arguments.length?(c=+t,h):c},h}var zt={value:()=>{}};function $t(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ut.hasOwnProperty(n)?{space:Ut[n],local:t}:t}function Ot(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===qt&&n.documentElement.namespaceURI===qt?n.createElement(t):n.createElementNS(e,t)}}function Bt(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Yt(t){var n=It(t);return(n.local?Bt:Ot)(n)}function Lt(){}function jt(t){return null==t?Lt:function(){return this.querySelector(t)}}function Ht(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function Xt(){return[]}function Gt(t){return null==t?Xt:function(){return this.querySelectorAll(t)}}function Vt(t){return function(){return this.matches(t)}}function Wt(t){return function(n){return n.matches(t)}}var Zt=Array.prototype.find;function Kt(){return this.firstElementChild}var Qt=Array.prototype.filter;function Jt(){return Array.from(this.children)}function tn(t){return new Array(t.length)}function nn(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function en(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function cn(t){return function(){this.removeAttribute(t)}}function fn(t){return function(){this.removeAttributeNS(t.space,t.local)}}function sn(t,n){return function(){this.setAttribute(t,n)}}function ln(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function hn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function dn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function pn(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function gn(t){return function(){this.style.removeProperty(t)}}function yn(t,n,e){return function(){this.style.setProperty(t,n,e)}}function vn(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function _n(t,n){return t.style.getPropertyValue(n)||pn(t).getComputedStyle(t,null).getPropertyValue(n)}function bn(t){return function(){delete this[t]}}function mn(t,n){return function(){this[t]=n}}function xn(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function wn(t){return t.trim().split(/^|\s+/)}function Mn(t){return t.classList||new Tn(t)}function Tn(t){this._node=t,this._names=wn(t.getAttribute("class")||"")}function An(t,n){for(var e=Mn(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Gn=[null];function Vn(t,n){this._groups=t,this._parents=n}function Wn(){return new Vn([[document.documentElement]],Gn)}function Zn(t){return"string"==typeof t?new Vn([[document.querySelector(t)]],[document.documentElement]):new Vn([[t]],Gn)}Vn.prototype=Wn.prototype={constructor:Vn,select:function(t){"function"!=typeof t&&(t=jt(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=y[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=un);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?gn:"function"==typeof n?vn:yn)(t,n,null==e?"":e)):_n(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?bn:"function"==typeof n?xn:mn)(t,n)):this.node()[t]},classed:function(t,n){var e=wn(t+"");if(arguments.length<2){for(var r=Mn(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Ln:Yn,r=0;r()=>t;function fe(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:c,dy:f,dispatch:s}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:c,enumerable:!0,configurable:!0},dy:{value:f,enumerable:!0,configurable:!0},_:{value:s}})}function se(t){return!t.ctrlKey&&!t.button}function le(){return this.parentNode}function he(t,n){return null==n?{x:t.x,y:t.y}:n}function de(){return navigator.maxTouchPoints||"ontouchstart"in this}function pe(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function ge(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function ye(){}fe.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var ve=.7,_e=1/ve,be="\\s*([+-]?\\d+)\\s*",me="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",xe="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",we=/^#([0-9a-f]{3,8})$/,Me=new RegExp(`^rgb\\(${be},${be},${be}\\)$`),Te=new RegExp(`^rgb\\(${xe},${xe},${xe}\\)$`),Ae=new RegExp(`^rgba\\(${be},${be},${be},${me}\\)$`),Se=new RegExp(`^rgba\\(${xe},${xe},${xe},${me}\\)$`),Ee=new RegExp(`^hsl\\(${me},${xe},${xe}\\)$`),Ne=new RegExp(`^hsla\\(${me},${xe},${xe},${me}\\)$`),ke={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Ce(){return this.rgb().formatHex()}function Pe(){return this.rgb().formatRgb()}function ze(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=we.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?$e(n):3===e?new qe(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?De(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?De(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Me.exec(t))?new qe(n[1],n[2],n[3],1):(n=Te.exec(t))?new qe(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Ae.exec(t))?De(n[1],n[2],n[3],n[4]):(n=Se.exec(t))?De(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ee.exec(t))?Le(n[1],n[2]/100,n[3]/100,1):(n=Ne.exec(t))?Le(n[1],n[2]/100,n[3]/100,n[4]):ke.hasOwnProperty(t)?$e(ke[t]):"transparent"===t?new qe(NaN,NaN,NaN,0):null}function $e(t){return new qe(t>>16&255,t>>8&255,255&t,1)}function De(t,n,e,r){return r<=0&&(t=n=e=NaN),new qe(t,n,e,r)}function Re(t){return t instanceof ye||(t=ze(t)),t?new qe((t=t.rgb()).r,t.g,t.b,t.opacity):new qe}function Fe(t,n,e,r){return 1===arguments.length?Re(t):new qe(t,n,e,null==r?1:r)}function qe(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ue(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}`}function Ie(){const t=Oe(this.opacity);return`${1===t?"rgb(":"rgba("}${Be(this.r)}, ${Be(this.g)}, ${Be(this.b)}${1===t?")":`, ${t})`}`}function Oe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Be(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ye(t){return((t=Be(t))<16?"0":"")+t.toString(16)}function Le(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xe(t,n,e,r)}function je(t){if(t instanceof Xe)return new Xe(t.h,t.s,t.l,t.opacity);if(t instanceof ye||(t=ze(t)),!t)return new Xe;if(t instanceof Xe)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new Xe(a,u,c,t.opacity)}function He(t,n,e,r){return 1===arguments.length?je(t):new Xe(t,n,e,null==r?1:r)}function Xe(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ge(t){return(t=(t||0)%360)<0?t+360:t}function Ve(t){return Math.max(0,Math.min(1,t||0))}function We(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}pe(ye,ze,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Ce,formatHex:Ce,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return je(this).formatHsl()},formatRgb:Pe,toString:Pe}),pe(qe,Fe,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new qe(Be(this.r),Be(this.g),Be(this.b),Oe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ue,formatHex:Ue,formatHex8:function(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}${Ye(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ie,toString:Ie})),pe(Xe,He,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new Xe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new Xe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new qe(We(t>=240?t-240:t+120,i,r),We(t,i,r),We(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Xe(Ge(this.h),Ve(this.s),Ve(this.l),Oe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Oe(this.opacity);return`${1===t?"hsl(":"hsla("}${Ge(this.h)}, ${100*Ve(this.s)}%, ${100*Ve(this.l)}%${1===t?")":`, ${t})`}`}}));const Ze=Math.PI/180,Ke=180/Math.PI,Qe=.96422,Je=1,tr=.82521,nr=4/29,er=6/29,rr=3*er*er,ir=er*er*er;function or(t){if(t instanceof ur)return new ur(t.l,t.a,t.b,t.opacity);if(t instanceof pr)return gr(t);t instanceof qe||(t=Re(t));var n,e,r=lr(t.r),i=lr(t.g),o=lr(t.b),a=cr((.2225045*r+.7168786*i+.0606169*o)/Je);return r===i&&i===o?n=e=a:(n=cr((.4360747*r+.3850649*i+.1430804*o)/Qe),e=cr((.0139322*r+.0971045*i+.7141733*o)/tr)),new ur(116*a-16,500*(n-a),200*(a-e),t.opacity)}function ar(t,n,e,r){return 1===arguments.length?or(t):new ur(t,n,e,null==r?1:r)}function ur(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function cr(t){return t>ir?Math.pow(t,1/3):t/rr+nr}function fr(t){return t>er?t*t*t:rr*(t-nr)}function sr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function lr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hr(t){if(t instanceof pr)return new pr(t.h,t.c,t.l,t.opacity);if(t instanceof ur||(t=or(t)),0===t.a&&0===t.b)return new pr(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r()=>t;function Cr(t,n){return function(e){return t+e*n}}function Pr(t,n){var e=n-t;return e?Cr(t,e>180||e<-180?e-360*Math.round(e/360):e):kr(isNaN(t)?n:t)}function zr(t){return 1==(t=+t)?$r:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):kr(isNaN(n)?e:n)}}function $r(t,n){var e=n-t;return e?Cr(t,e):kr(isNaN(t)?n:t)}var Dr=function t(n){var e=zr(n);function r(t,n){var r=e((t=Fe(t)).r,(n=Fe(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=$r(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Rr(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:Yr(e,r)})),o=Hr.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Yr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Yr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Yr(t,e)},{i:u-2,x:Yr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(void 0,t),n=n._next;--yi}function Ci(){xi=(mi=Mi.now())+wi,yi=vi=0;try{ki()}finally{yi=0,function(){var t,n,e=pi,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pi=n);gi=t,zi(r)}(),xi=0}}function Pi(){var t=Mi.now(),n=t-mi;n>bi&&(wi-=n,mi=t)}function zi(t){yi||(vi&&(vi=clearTimeout(vi)),t-xi>24?(t<1/0&&(vi=setTimeout(Ci,t-Mi.now()-wi)),_i&&(_i=clearInterval(_i))):(_i||(mi=Mi.now(),_i=setInterval(Pi,bi)),yi=1,Ti(Ci)))}function $i(t,n,e){var r=new Ei;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ei.prototype=Ni.prototype={constructor:Ei,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Ai():+e)+(null==n?0:+n),this._next||gi===this||(gi?gi._next=this:pi=this,gi=this),this._call=t,this._time=e,zi()},stop:function(){this._call&&(this._call=null,this._time=1/0,zi())}};var Di=$t("start","end","cancel","interrupt"),Ri=[],Fi=0,qi=1,Ui=2,Ii=3,Oi=4,Bi=5,Yi=6;function Li(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=qi,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var f,s,l,h;if(e.state!==qi)return c();for(f in i)if((h=i[f]).name===e.name){if(h.state===Ii)return $i(a);h.state===Oi?(h.state=Yi,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fFi)throw new Error("too late; already scheduled");return e}function Hi(t,n){var e=Xi(t,n);if(e.state>Ii)throw new Error("too late; already running");return e}function Xi(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Gi(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>Ui&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ji:Hi;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=It(t),r="transform"===e?ni:Ki;return this.attrTween(t,"function"==typeof n?(e.local?ro:eo)(e,r,Zi(this,"attr."+t,n)):null==n?(e.local?Ji:Qi)(e):(e.local?no:to)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=It(t);return this.tween(e,(r.local?io:oo)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ti:Ki;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=_n(this,t),a=(this.style.removeProperty(t),_n(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,lo(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=_n(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=_n(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Zi(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Hi(this,t),f=c.on,s=null==c.value[a]?o||(o=lo(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=_n(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Zi(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Xi(this.node(),e).tween,o=0,a=i.length;o()=>t;function Qo(t,{sourceEvent:n,target:e,selection:r,mode:i,dispatch:o}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},selection:{value:r,enumerable:!0,configurable:!0},mode:{value:i,enumerable:!0,configurable:!0},_:{value:o}})}function Jo(t){t.preventDefault(),t.stopImmediatePropagation()}var ta={name:"drag"},na={name:"space"},ea={name:"handle"},ra={name:"center"};const{abs:ia,max:oa,min:aa}=Math;function ua(t){return[+t[0],+t[1]]}function ca(t){return[ua(t[0]),ua(t[1])]}var fa={name:"x",handles:["w","e"].map(va),input:function(t,n){return null==t?null:[[+t[0],n[0][1]],[+t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},sa={name:"y",handles:["n","s"].map(va),input:function(t,n){return null==t?null:[[n[0][0],+t[0]],[n[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},la={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(va),input:function(t){return null==t?null:ca(t)},output:function(t){return t}},ha={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},da={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},pa={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},ga={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},ya={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function va(t){return{type:t}}function _a(t){return!t.ctrlKey&&!t.button}function ba(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function ma(){return navigator.maxTouchPoints||"ontouchstart"in this}function xa(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function wa(t){var n,e=ba,r=_a,i=ma,o=!0,a=$t("start","brush","end"),u=6;function c(n){var e=n.property("__brush",g).selectAll(".overlay").data([va("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",ha.overlay).merge(e).each((function(){var t=xa(this).extent;Zn(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),n.selectAll(".selection").data([va("selection")]).enter().append("rect").attr("class","selection").attr("cursor",ha.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=n.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return ha[t.type]})),n.each(f).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",h).filter(i).on("touchstart.brush",h).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function f(){var t=Zn(this),n=xa(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?n[1][0]-u/2:n[0][0]-u/2})).attr("y",(function(t){return"s"===t.type[0]?n[1][1]-u/2:n[0][1]-u/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+u:u})).attr("height",(function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+u:u}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function s(t,n,e){var r=t.__brush.emitter;return!r||e&&r.clean?new l(t,n,e):r}function l(t,n,e){this.that=t,this.args=n,this.state=t.__brush,this.active=0,this.clean=e}function h(e){if((!n||e.touches)&&r.apply(this,arguments)){var i,a,u,c,l,h,d,p,g,y,v,_=this,b=e.target.__data__.type,m="selection"===(o&&e.metaKey?b="overlay":b)?ta:o&&e.altKey?ra:ea,x=t===sa?null:ga[b],w=t===fa?null:ya[b],M=xa(_),T=M.extent,A=M.selection,S=T[0][0],E=T[0][1],N=T[1][0],k=T[1][1],C=0,P=0,z=x&&w&&o&&e.shiftKey,$=Array.from(e.touches||[e],(t=>{const n=t.identifier;return(t=ne(t,_)).point0=t.slice(),t.identifier=n,t}));Gi(_);var D=s(_,arguments,!0).beforestart();if("overlay"===b){A&&(g=!0);const n=[$[0],$[1]||$[0]];M.selection=A=[[i=t===sa?S:aa(n[0][0],n[1][0]),u=t===fa?E:aa(n[0][1],n[1][1])],[l=t===sa?N:oa(n[0][0],n[1][0]),d=t===fa?k:oa(n[0][1],n[1][1])]],$.length>1&&I(e)}else i=A[0][0],u=A[0][1],l=A[1][0],d=A[1][1];a=i,c=u,h=l,p=d;var R=Zn(_).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",ha[b]);if(e.touches)D.moved=U,D.ended=O;else{var q=Zn(e.view).on("mousemove.brush",U,!0).on("mouseup.brush",O,!0);o&&q.on("keydown.brush",(function(t){switch(t.keyCode){case 16:z=x&&w;break;case 18:m===ea&&(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra,I(t));break;case 32:m!==ea&&m!==ra||(x<0?l=h-C:x>0&&(i=a-C),w<0?d=p-P:w>0&&(u=c-P),m=na,F.attr("cursor",ha.selection),I(t));break;default:return}Jo(t)}),!0).on("keyup.brush",(function(t){switch(t.keyCode){case 16:z&&(y=v=z=!1,I(t));break;case 18:m===ra&&(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea,I(t));break;case 32:m===na&&(t.altKey?(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra):(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea),F.attr("cursor",ha[b]),I(t));break;default:return}Jo(t)}),!0),ae(e.view)}f.call(_),D.start(e,m.name)}function U(t){for(const n of t.changedTouches||[t])for(const t of $)t.identifier===n.identifier&&(t.cur=ne(n,_));if(z&&!y&&!v&&1===$.length){const t=$[0];ia(t.cur[0]-t[0])>ia(t.cur[1]-t[1])?v=!0:y=!0}for(const t of $)t.cur&&(t[0]=t.cur[0],t[1]=t.cur[1]);g=!0,Jo(t),I(t)}function I(t){const n=$[0],e=n.point0;var r;switch(C=n[0]-e[0],P=n[1]-e[1],m){case na:case ta:x&&(C=oa(S-i,aa(N-l,C)),a=i+C,h=l+C),w&&(P=oa(E-u,aa(k-d,P)),c=u+P,p=d+P);break;case ea:$[1]?(x&&(a=oa(S,aa(N,$[0][0])),h=oa(S,aa(N,$[1][0])),x=1),w&&(c=oa(E,aa(k,$[0][1])),p=oa(E,aa(k,$[1][1])),w=1)):(x<0?(C=oa(S-i,aa(N-i,C)),a=i+C,h=l):x>0&&(C=oa(S-l,aa(N-l,C)),a=i,h=l+C),w<0?(P=oa(E-u,aa(k-u,P)),c=u+P,p=d):w>0&&(P=oa(E-d,aa(k-d,P)),c=u,p=d+P));break;case ra:x&&(a=oa(S,aa(N,i-C*x)),h=oa(S,aa(N,l+C*x))),w&&(c=oa(E,aa(k,u-P*w)),p=oa(E,aa(k,d+P*w)))}ht+e))}function za(t,n){var e=0,r=null,i=null,o=null;function a(a){var u,c=a.length,f=new Array(c),s=Pa(0,c),l=new Array(c*c),h=new Array(c),d=0;a=Float64Array.from({length:c*c},n?(t,n)=>a[n%c][n/c|0]:(t,n)=>a[n/c|0][n%c]);for(let n=0;nr(f[t],f[n])));for(const e of s){const r=n;if(t){const t=Pa(1+~c,c).filter((t=>t<0?a[~t*c+e]:a[e*c+t]));i&&t.sort(((t,n)=>i(t<0?-a[~t*c+e]:a[e*c+t],n<0?-a[~n*c+e]:a[e*c+n])));for(const r of t)if(r<0){(l[~r*c+e]||(l[~r*c+e]={source:null,target:null})).target={index:e,startAngle:n,endAngle:n+=a[~r*c+e]*d,value:a[~r*c+e]}}else{(l[e*c+r]||(l[e*c+r]={source:null,target:null})).source={index:e,startAngle:n,endAngle:n+=a[e*c+r]*d,value:a[e*c+r]}}h[e]={index:e,startAngle:r,endAngle:n,value:f[e]}}else{const t=Pa(0,c).filter((t=>a[e*c+t]||a[t*c+e]));i&&t.sort(((t,n)=>i(a[e*c+t],a[e*c+n])));for(const r of t){let t;if(e=0))throw new Error(`invalid digits: ${t}`);if(n>15)return qa;const e=10**n;return function(t){this._+=t[0];for(let n=1,r=t.length;nRa)if(Math.abs(s*u-c*f)>Ra&&i){let h=e-o,d=r-a,p=u*u+c*c,g=h*h+d*d,y=Math.sqrt(p),v=Math.sqrt(l),_=i*Math.tan(($a-Math.acos((p+l-g)/(2*y*v)))/2),b=_/v,m=_/y;Math.abs(b-1)>Ra&&this._append`L${t+b*f},${n+b*s}`,this._append`A${i},${i},0,0,${+(s*h>f*d)},${this._x1=t+m*u},${this._y1=n+m*c}`}else this._append`L${this._x1=t},${this._y1=n}`;else;}arc(t,n,e,r,i,o){if(t=+t,n=+n,o=!!o,(e=+e)<0)throw new Error(`negative radius: ${e}`);let a=e*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;null===this._x1?this._append`M${c},${f}`:(Math.abs(this._x1-c)>Ra||Math.abs(this._y1-f)>Ra)&&this._append`L${c},${f}`,e&&(l<0&&(l=l%Da+Da),l>Fa?this._append`A${e},${e},0,1,${s},${t-a},${n-u}A${e},${e},0,1,${s},${this._x1=c},${this._y1=f}`:l>Ra&&this._append`A${e},${e},0,${+(l>=$a)},${s},${this._x1=t+e*Math.cos(i)},${this._y1=n+e*Math.sin(i)}`)}rect(t,n,e,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${e=+e}v${+r}h${-e}Z`}toString(){return this._}};function Ia(){return new Ua}Ia.prototype=Ua.prototype;var Oa=Array.prototype.slice;function Ba(t){return function(){return t}}function Ya(t){return t.source}function La(t){return t.target}function ja(t){return t.radius}function Ha(t){return t.startAngle}function Xa(t){return t.endAngle}function Ga(){return 0}function Va(){return 10}function Wa(t){var n=Ya,e=La,r=ja,i=ja,o=Ha,a=Xa,u=Ga,c=null;function f(){var f,s=n.apply(this,arguments),l=e.apply(this,arguments),h=u.apply(this,arguments)/2,d=Oa.call(arguments),p=+r.apply(this,(d[0]=s,d)),g=o.apply(this,d)-Ea,y=a.apply(this,d)-Ea,v=+i.apply(this,(d[0]=l,d)),_=o.apply(this,d)-Ea,b=a.apply(this,d)-Ea;if(c||(c=f=Ia()),h>Ca&&(Ma(y-g)>2*h+Ca?y>g?(g+=h,y-=h):(g-=h,y+=h):g=y=(g+y)/2,Ma(b-_)>2*h+Ca?b>_?(_+=h,b-=h):(_-=h,b+=h):_=b=(_+b)/2),c.moveTo(p*Ta(g),p*Aa(g)),c.arc(0,0,p,g,y),g!==_||y!==b)if(t){var m=v-+t.apply(this,arguments),x=(_+b)/2;c.quadraticCurveTo(0,0,m*Ta(_),m*Aa(_)),c.lineTo(v*Ta(x),v*Aa(x)),c.lineTo(m*Ta(b),m*Aa(b))}else c.quadraticCurveTo(0,0,v*Ta(_),v*Aa(_)),c.arc(0,0,v,_,b);if(c.quadraticCurveTo(0,0,p*Ta(g),p*Aa(g)),c.closePath(),f)return c=null,f+""||null}return t&&(f.headRadius=function(n){return arguments.length?(t="function"==typeof n?n:Ba(+n),f):t}),f.radius=function(t){return arguments.length?(r=i="function"==typeof t?t:Ba(+t),f):r},f.sourceRadius=function(t){return arguments.length?(r="function"==typeof t?t:Ba(+t),f):r},f.targetRadius=function(t){return arguments.length?(i="function"==typeof t?t:Ba(+t),f):i},f.startAngle=function(t){return arguments.length?(o="function"==typeof t?t:Ba(+t),f):o},f.endAngle=function(t){return arguments.length?(a="function"==typeof t?t:Ba(+t),f):a},f.padAngle=function(t){return arguments.length?(u="function"==typeof t?t:Ba(+t),f):u},f.source=function(t){return arguments.length?(n=t,f):n},f.target=function(t){return arguments.length?(e=t,f):e},f.context=function(t){return arguments.length?(c=null==t?null:t,f):c},f}var Za=Array.prototype.slice;function Ka(t,n){return t-n}var Qa=t=>()=>t;function Ja(t,n){for(var e,r=-1,i=n.length;++rr!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function nu(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function eu(){}var ru=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iu(){var t=1,n=1,e=K,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(Ka);else{const e=M(t,ou);for(n=G(...Z(e[0],e[1],n),n);n[n.length-1]>=e[1];)n.pop();for(;n[1]o(t,n)))}function o(e,i){const o=null==i?NaN:+i;if(isNaN(o))throw new Error(`invalid value: ${i}`);var u=[],c=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=au(e[0],r),ru[f<<1].forEach(p);for(;++o=r,ru[s<<2].forEach(p);for(;++o0?u.push([t]):c.push(t)})),c.forEach((function(t){for(var n,e=0,r=u.length;e0&&o0&&a=0&&o>=0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:eu,i):r===u},i}function ou(t){return isFinite(t)?t:NaN}function au(t,n){return null!=t&&+t>=n}function uu(t){return null==t||isNaN(t=+t)?-1/0:t}function cu(t,n,e,r){const i=r-n,o=e-n,a=isFinite(i)||isFinite(o)?i/o:Math.sign(i)/Math.sign(o);return isNaN(a)?t:t+a-.5}function fu(t){return t[0]}function su(t){return t[1]}function lu(){return 1}const hu=134217729,du=33306690738754706e-32;function pu(t,n,e,r,i){let o,a,u,c,f=n[0],s=r[0],l=0,h=0;s>f==s>-f?(o=f,f=n[++l]):(o=s,s=r[++h]);let d=0;if(lf==s>-f?(a=f+o,u=o-(a-f),f=n[++l]):(a=s+o,u=o-(a-s),s=r[++h]),o=a,0!==u&&(i[d++]=u);lf==s>-f?(a=o+f,c=a-o,u=o-(a-c)+(f-c),f=n[++l]):(a=o+s,c=a-o,u=o-(a-c)+(s-c),s=r[++h]),o=a,0!==u&&(i[d++]=u);for(;l=33306690738754716e-32*f?c:-function(t,n,e,r,i,o,a){let u,c,f,s,l,h,d,p,g,y,v,_,b,m,x,w,M,T;const A=t-i,S=e-i,E=n-o,N=r-o;m=A*N,h=hu*A,d=h-(h-A),p=A-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=E*S,h=hu*E,d=h-(h-E),p=E-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,_u[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,_u[1]=b-(v+l)+(l-w),T=_+v,l=T-_,_u[2]=_-(T-l)+(v-l),_u[3]=T;let k=function(t,n){let e=n[0];for(let r=1;r=C||-k>=C)return k;if(l=t-A,u=t-(A+l)+(l-i),l=e-S,f=e-(S+l)+(l-i),l=n-E,c=n-(E+l)+(l-o),l=r-N,s=r-(N+l)+(l-o),0===u&&0===c&&0===f&&0===s)return k;if(C=vu*a+du*Math.abs(k),k+=A*s+N*u-(E*f+S*c),k>=C||-k>=C)return k;m=u*N,h=hu*u,d=h-(h-u),p=u-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=c*S,h=hu*c,d=h-(h-c),p=c-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const P=pu(4,_u,4,wu,bu);m=A*s,h=hu*A,d=h-(h-A),p=A-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=E*f,h=hu*E,d=h-(h-E),p=E-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const z=pu(P,bu,4,wu,mu);m=u*s,h=hu*u,d=h-(h-u),p=u-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=c*f,h=hu*c,d=h-(h-c),p=c-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const $=pu(z,mu,4,wu,xu);return xu[$-1]}(t,n,e,r,i,o,f)}const Tu=Math.pow(2,-52),Au=new Uint32Array(512);class Su{static from(t,n=zu,e=$u){const r=t.length,i=new Float64Array(2*r);for(let o=0;o>1;if(n>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const e=Math.max(2*n-5,0);this._triangles=new Uint32Array(3*e),this._halfedges=new Int32Array(3*e),this._hashSize=Math.ceil(Math.sqrt(n)),this._hullPrev=new Uint32Array(n),this._hullNext=new Uint32Array(n),this._hullTri=new Uint32Array(n),this._hullHash=new Int32Array(this._hashSize).fill(-1),this._ids=new Uint32Array(n),this._dists=new Float64Array(n),this.update()}update(){const{coords:t,_hullPrev:n,_hullNext:e,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,u=1/0,c=-1/0,f=-1/0;for(let n=0;nc&&(c=e),r>f&&(f=r),this._ids[n]=n}const s=(a+c)/2,l=(u+f)/2;let h,d,p,g=1/0;for(let n=0;n