@grunwaldlab/heat-tree 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/heat-tree.es.min.js +510 -310
- package/dist/heat-tree.es.min.js.map +1 -1
- package/dist/heat-tree.iife.min.js +2 -2
- package/dist/heat-tree.iife.min.js.map +1 -1
- package/dist/heat-tree.umd.min.js +2 -2
- package/dist/heat-tree.umd.min.js.map +1 -1
- package/package.json +10 -2
package/dist/heat-tree.es.min.js
CHANGED
|
@@ -2869,14 +2869,20 @@ function zoom() {
|
|
|
2869
2869
|
};
|
|
2870
2870
|
return zoom2;
|
|
2871
2871
|
}
|
|
2872
|
-
const styles = ".ht-widget{display:flex;flex-direction:column;width:100%;height:100%}.ht-toolbar{flex:0 0 auto;margin-bottom:4px;display:flex;flex-direction:column;position:relative}.ht-toggle-container{position:absolute;top:0;right:0;padding:4px 8px;
|
|
2873
|
-
|
|
2872
|
+
const styles = ".ht-widget{color-scheme:light!important;--color-background: #ffffff;--color-text: #333333;--color-border: #dddddd;--color-muted: #666666;--color-accent: #007bff;background-color:var(--color-background);color:var(--color-text);outline:2px solid #ddd;border-radius:5px;overflow:hidden;display:flex;flex-direction:column;width:100%;height:100%}.ht-toolbar{flex:0 0 auto;margin-bottom:4px;display:flex;flex-direction:column;position:relative}.ht-toggle-container{position:absolute;top:0;right:0;padding:4px 8px;z-index:10}.ht-control-panel-toggle{background-color:transparent;border:none;cursor:pointer;display:flex;align-items:center;gap:4px;transition:opacity .2s}.ht-control-panel-toggle:hover{opacity:.7}.ht-control-panel-toggle .ht-toggle-arrow{transition:transform .3s}.ht-control-panel-toggle.collapsed .ht-toggle-arrow{transform:rotate(180deg)}.ht-control-panel-toggle .ht-hamburger-icon{color:#333}.ht-collapsible-panel{max-height:1000px;overflow:hidden;transition:max-height .3s ease-in-out}.ht-collapsible-panel.ht-panel-collapsed{max-height:0}.ht-widget .ht-tabs{display:flex;gap:20px;padding:4px 8px;background-color:#f5f5f5;border-bottom:2px solid #ddd;-webkit-user-select:none;user-select:none}.ht-widget .ht-tab{cursor:pointer;padding:0 4px;font-family:sans-serif;font-size:12px;color:#333;border-bottom:2px solid transparent;transition:all .2s}.ht-widget .ht-tab:hover{color:#666}.ht-widget .ht-tab.active{color:#000;font-weight:700;border-bottom-color:#007bff}.ht-widget .ht-tab.active:hover{color:#000}.ht-widget .ht-tab.disabled{color:#999;cursor:not-allowed;opacity:.5}.ht-widget .ht-tab.disabled:hover{color:#999}.ht-widget .ht-controls{padding:2px 8px;background-color:#fafafa;border-bottom:1px solid #ddd;display:flex;flex-wrap:wrap;column-gap:8px;row-gap:2px;align-items:center;min-height:22px}.ht-controls.hidden{display:none}.ht-control-group{display:flex;align-items:center;gap:4px;white-space:nowrap;padding:2px 0;border-bottom:2px solid transparent;transition:border-bottom-color .2s}.ht-control-group.ht-aesthetic-editing{border-bottom-color:#007bff}.ht-widget .ht-control-label{font-family:sans-serif;font-size:12px;color:#333;white-space:nowrap;display:flex;align-items:center}.ht-button{font-size:12px;font-family:sans-serif;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s;white-space:nowrap}.ht-button:hover{background-color:#d0d0d0}.ht-button:disabled{background-color:#f0f0f0;color:#999;cursor:not-allowed}.ht-button.primary{background-color:#007bff;color:#fff;font-weight:700}.ht-button.primary:hover{background-color:#0056b3}.ht-icon-button{font-size:12px;font-family:sans-serif;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s;display:flex;align-items:center;justify-content:center;padding:2px}.ht-icon-button:hover{background-color:#d0d0d0}.ht-icon-button svg{display:block}.ht-select{padding:1px 2px;font-size:12px;border:1px solid #ccc;border-radius:4px}.ht-slider{cursor:pointer;width:100px}.ht-toggle{width:30px;background-color:#ccc;border-radius:12px;position:relative;cursor:pointer;transition:background-color .3s;flex-shrink:0}.ht-toggle.active{background-color:#007bff}.ht-toggle-knob{background-color:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:left .3s}.ht-toggle.active .ht-toggle-knob{left:calc(100% - 2px);transform:translate(-100%)}.ht-number-input{font-size:12px;border:1px solid #ccc;border-radius:4px;width:60px}.ht-text-input{font-size:12px;border:1px solid #ccc;border-radius:4px;padding:2px 4px;width:100px}.ht-widget .ht-aesthetic-settings-content{padding:2px 8px;background-color:#f0f0f0;border-bottom:1px solid #ddd;display:flex;flex-wrap:wrap;column-gap:8px;row-gap:2px;align-items:center;max-height:1000px;overflow:hidden;transition:max-height .3s ease-in-out,padding .3s ease-in-out}.ht-aesthetic-settings.hidden{max-height:0;padding-top:0;padding-bottom:0;border-bottom:none}.ht-color-palette-editor{display:flex;gap:8px;align-items:center}.ht-palette-buttons-container{display:flex;flex-direction:column;gap:2px}.ht-palette-button{width:20px;height:20px;font-size:12px;font-weight:700;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s}.ht-palette-button:hover{background-color:#d0d0d0}.ht-gradient-container{display:flex;gap:8px}.ht-gradient-column{display:flex;flex-direction:column;gap:4px}.ht-color-squares-container{display:flex;position:relative;height:16px;width:200px}.ht-color-square-wrapper{position:absolute;display:flex;flex-direction:column;align-items:center;transform:translate(-50%)}.ht-color-square{width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:pointer}.ht-color-square-tick{width:2px;height:8px;background-color:#333}.ht-gradient-box{width:200px;height:20px;border:1px solid #ccc;border-radius:4px;position:relative}.ht-widget .ht-range-slider-container{width:200px;height:12px;position:relative;border:1px solid #ccc;border-radius:4px;background:#f0f0f0;display:flex;align-items:center}.ht-range-handle{position:absolute;width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:ew-resize;transform:translate(-50%)}.ht-range-handle-indicator{position:absolute;bottom:90%;left:50%;transform:translate(-50%) rotate(180deg);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid #333;border-radius:2px;margin-bottom:2px}.ht-null-color-column{display:flex;flex-direction:column;gap:4px}.ht-null-color-square-container{display:flex;position:relative;height:16px;justify-content:center}.ht-null-color-square{width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:pointer}.ht-null-color-square-tick{width:2px;height:6px;background-color:#333;position:absolute;bottom:-6px;left:50%;transform:translate(-50%)}.ht-null-color-box{width:16px;height:20px;border:1px solid #ccc;border-radius:4px;position:relative}.missing-data-x-container{position:relative;display:flex;align-items:center;justify-content:center}.missing-data-x-container-triangle{position:absolute;bottom:85%;left:50%;transform:translate(-50%) rotate(180deg);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid #333;border-radius:2px;margin-bottom:2px}.missing-data-x{font-size:16px;color:#d00;font-weight:700;line-height:1;-webkit-user-select:none;user-select:none;cursor:pointer}.ht-tree{flex:1 1 auto;min-height:0;position:relative}.ht-tree svg{display:block;height:100%;width:100%}.picker_wrapper,.picker_wrapper.popup{z-index:999999!important;position:fixed!important}.picker_wrapper *,.picker_selector,.picker_slider,.picker_editor,.picker_sample,.picker_done,.picker_cancel{pointer-events:auto!important}";
|
|
2873
|
+
const pickerStyles = '.picker_wrapper.no_alpha .picker_alpha{display:none}.picker_wrapper.no_editor .picker_editor{position:absolute;z-index:-1;opacity:0}.picker_wrapper.no_cancel .picker_cancel{display:none}.layout_default.picker_wrapper{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:stretch;font-size:10px;width:25em;padding:.5em}.layout_default.picker_wrapper input,.layout_default.picker_wrapper button{font-size:1rem}.layout_default.picker_wrapper>*{margin:.5em}.layout_default.picker_wrapper:before{content:"";display:block;width:100%;height:0;order:1}.layout_default .picker_slider,.layout_default .picker_selector{padding:1em}.layout_default .picker_hue{width:100%}.layout_default .picker_sl{flex:1 1 auto}.layout_default .picker_sl:before{content:"";display:block;padding-bottom:100%}.layout_default .picker_editor{order:1;width:6.5rem}.layout_default .picker_editor input{width:100%;height:100%}.layout_default .picker_sample{order:1;flex:1 1 auto}.layout_default .picker_done,.layout_default .picker_cancel{order:1}.picker_wrapper{box-sizing:border-box;background:#f2f2f2;box-shadow:0 0 0 1px silver;cursor:default;font-family:sans-serif;color:#444;pointer-events:auto}.picker_wrapper:focus{outline:none}.picker_wrapper button,.picker_wrapper input{box-sizing:border-box;border:none;box-shadow:0 0 0 1px silver;outline:none}.picker_wrapper button:focus,.picker_wrapper button:active,.picker_wrapper input:focus,.picker_wrapper input:active{box-shadow:0 0 2px 1px #1e90ff}.picker_wrapper button{padding:.4em .6em;cursor:pointer;background-color:#f5f5f5;background-image:linear-gradient(0deg,gainsboro,transparent)}.picker_wrapper button:active{background-image:linear-gradient(0deg,transparent,gainsboro)}.picker_wrapper button:hover{background-color:#fff}.picker_selector{position:absolute;z-index:1;display:block;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:2px solid #fff;border-radius:100%;box-shadow:0 0 3px 1px #67b9ff;background:currentColor;cursor:pointer}.picker_slider .picker_selector{border-radius:2px}.picker_hue{position:relative;background-image:linear-gradient(90deg,red,#ff0,#0f0,#0ff,#00f,#f0f,red);box-shadow:0 0 0 1px silver}.picker_sl{position:relative;box-shadow:0 0 0 1px silver;background-image:linear-gradient(180deg,#fff,#fff0 50%),linear-gradient(0deg,#000,#0000 50%),linear-gradient(90deg,gray,#80808000)}.picker_alpha,.picker_sample{position:relative;background:linear-gradient(45deg,lightgrey 25%,transparent 25%,transparent 75%,lightgrey 75%) 0 0/2em 2em,linear-gradient(45deg,#d3d3d3 25%,#fff 25% 75%,#d3d3d3 75%) 1em 1em/2em 2em;box-shadow:0 0 0 1px silver}.picker_alpha .picker_selector,.picker_sample .picker_selector{background:none}.picker_editor input{font-family:monospace;padding:.2em .4em}.picker_sample:before{content:"";position:absolute;display:block;width:100%;height:100%;background:currentColor}.picker_arrow{position:absolute;z-index:-1}.picker_wrapper.popup{position:absolute;z-index:2;margin:1.5em}.picker_wrapper.popup,.picker_wrapper.popup .picker_arrow:before,.picker_wrapper.popup .picker_arrow:after{background:#f2f2f2;box-shadow:0 0 10px 1px #0006}.picker_wrapper.popup .picker_arrow{width:3em;height:3em;margin:0}.picker_wrapper.popup .picker_arrow:before,.picker_wrapper.popup .picker_arrow:after{content:"";display:block;position:absolute;top:0;left:0;z-index:-99}.picker_wrapper.popup .picker_arrow:before{width:100%;height:100%;-webkit-transform:skew(45deg);transform:skew(45deg);-webkit-transform-origin:0 100%;transform-origin:0 100%}.picker_wrapper.popup .picker_arrow:after{width:150%;height:150%;box-shadow:none}.popup.popup_top{bottom:100%;left:0}.popup.popup_top .picker_arrow{bottom:0;left:0;-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.popup.popup_bottom{top:100%;left:0}.popup.popup_bottom .picker_arrow{top:0;left:0;-webkit-transform:rotate(90deg) scale(1,-1);transform:rotate(90deg) scaleY(-1)}.popup.popup_left{top:0;right:100%}.popup.popup_left .picker_arrow{top:0;right:0;-webkit-transform:scale(-1,1);transform:scaleX(-1)}.popup.popup_right{top:0;left:100%}.popup.popup_right .picker_arrow{top:0;left:0}';
|
|
2874
|
+
function injectStyles(root2 = document) {
|
|
2874
2875
|
const styleId = "heat-tree-styles";
|
|
2875
|
-
|
|
2876
|
+
const existing = root2.querySelector ? root2.querySelector(`#${styleId}`) : document.getElementById(styleId);
|
|
2877
|
+
if (!existing) {
|
|
2876
2878
|
const styleElement = document.createElement("style");
|
|
2877
2879
|
styleElement.id = styleId;
|
|
2878
|
-
styleElement.textContent = styles;
|
|
2879
|
-
document
|
|
2880
|
+
styleElement.textContent = styles + "\n" + pickerStyles;
|
|
2881
|
+
if (root2 === document) {
|
|
2882
|
+
document.head.appendChild(styleElement);
|
|
2883
|
+
} else {
|
|
2884
|
+
root2.prepend(styleElement);
|
|
2885
|
+
}
|
|
2880
2886
|
}
|
|
2881
2887
|
}
|
|
2882
2888
|
function niceNumber(n) {
|
|
@@ -3074,6 +3080,61 @@ class ContainerResizeHandler {
|
|
|
3074
3080
|
clearTimeout(this.timeoutId);
|
|
3075
3081
|
}
|
|
3076
3082
|
}
|
|
3083
|
+
function isNexusFormat(str) {
|
|
3084
|
+
const firstLine = str.trim().split(/\r?\n/)[0].trim();
|
|
3085
|
+
return /^#NEXUS$/i.test(firstLine);
|
|
3086
|
+
}
|
|
3087
|
+
function applyTranslate(node, translateMap) {
|
|
3088
|
+
if (node.name) {
|
|
3089
|
+
const num = parseInt(node.name, 10);
|
|
3090
|
+
if (!isNaN(num) && translateMap.has(num)) {
|
|
3091
|
+
node.name = translateMap.get(num);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
if (node.children) {
|
|
3095
|
+
node.children.forEach((child) => applyTranslate(child, translateMap));
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
function parseTreesBlock(blockContent) {
|
|
3099
|
+
const trees = [];
|
|
3100
|
+
const translateMap = /* @__PURE__ */ new Map();
|
|
3101
|
+
const translateMatch = blockContent.match(/translate\s+([^;]+);/i);
|
|
3102
|
+
if (translateMatch) {
|
|
3103
|
+
const translateContent = translateMatch[1];
|
|
3104
|
+
const pairs = translateContent.split(/,\s*/);
|
|
3105
|
+
pairs.forEach((pair) => {
|
|
3106
|
+
const match2 = pair.trim().match(/^(\d+)\s+(.+)$/);
|
|
3107
|
+
if (match2) {
|
|
3108
|
+
const num = parseInt(match2[1], 10);
|
|
3109
|
+
const name = match2[2].trim();
|
|
3110
|
+
translateMap.set(num, name);
|
|
3111
|
+
}
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
const treeRegex = /tree\s*\*?\s*(?:(\S+))?\s*=\s*(?:\[\S+\])?\s*([^;]+);/gi;
|
|
3115
|
+
let match;
|
|
3116
|
+
while ((match = treeRegex.exec(blockContent)) !== null) {
|
|
3117
|
+
let treeName = match[1].replace(/['"]+/g, "") || null;
|
|
3118
|
+
if (treeName.toLowerCase() == "untitled") {
|
|
3119
|
+
treeName = null;
|
|
3120
|
+
}
|
|
3121
|
+
const newickStr = match[2].trim();
|
|
3122
|
+
const treeData = parseNewick(newickStr);
|
|
3123
|
+
applyTranslate(treeData, translateMap);
|
|
3124
|
+
trees.push({ treeName, treeData });
|
|
3125
|
+
}
|
|
3126
|
+
return trees;
|
|
3127
|
+
}
|
|
3128
|
+
function parseNexus(nexusStr) {
|
|
3129
|
+
const trees = [];
|
|
3130
|
+
const treesBlockRegex = /begin\s+trees\s*;([^]*?)end\s*;/gi;
|
|
3131
|
+
let blockMatch;
|
|
3132
|
+
while ((blockMatch = treesBlockRegex.exec(nexusStr)) !== null) {
|
|
3133
|
+
const blockTrees = parseTreesBlock(blockMatch[1]);
|
|
3134
|
+
trees.push(...blockTrees);
|
|
3135
|
+
}
|
|
3136
|
+
return trees;
|
|
3137
|
+
}
|
|
3077
3138
|
function parseNewick(newickStr) {
|
|
3078
3139
|
newickStr = newickStr.trim();
|
|
3079
3140
|
if (newickStr[newickStr.length - 1] === ";") {
|
|
@@ -3135,7 +3196,7 @@ function parseTable(tsvStr, valid_ids, sep = " ") {
|
|
|
3135
3196
|
const metadata = {};
|
|
3136
3197
|
for (let j = 0; j < headers.length; j++) {
|
|
3137
3198
|
const colName = headers[j];
|
|
3138
|
-
const value = values[j];
|
|
3199
|
+
const value = values[j] === "" ? void 0 : values[j];
|
|
3139
3200
|
metadata[colName] = value;
|
|
3140
3201
|
columnValues.get(colName).push(value);
|
|
3141
3202
|
}
|
|
@@ -3144,7 +3205,7 @@ function parseTable(tsvStr, valid_ids, sep = " ") {
|
|
|
3144
3205
|
headers.forEach((col) => {
|
|
3145
3206
|
const values = columnValues.get(col);
|
|
3146
3207
|
const numericValues = values.map((v) => parseFloat(v)).filter((v) => !isNaN(v));
|
|
3147
|
-
const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !==
|
|
3208
|
+
const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !== void 0).length;
|
|
3148
3209
|
columnTypes.set(col, isContinuous ? "continuous" : "categorical");
|
|
3149
3210
|
let matchCount = 0;
|
|
3150
3211
|
for (const value of values) {
|
|
@@ -3427,6 +3488,9 @@ class CategoricalColorScale {
|
|
|
3427
3488
|
};
|
|
3428
3489
|
this.frequencyMap = /* @__PURE__ */ new Map();
|
|
3429
3490
|
for (const category of categoryData) {
|
|
3491
|
+
if (category === null || category === void 0 || category === "") {
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3430
3494
|
if (this.frequencyMap.has(category)) {
|
|
3431
3495
|
this.frequencyMap.set(category, this.frequencyMap.get(category) + 1);
|
|
3432
3496
|
} else {
|
|
@@ -3479,7 +3543,9 @@ class CategoricalColorScale {
|
|
|
3479
3543
|
}
|
|
3480
3544
|
return;
|
|
3481
3545
|
}
|
|
3482
|
-
if (this.categories.length ===
|
|
3546
|
+
if (this.categories.length === 0) {
|
|
3547
|
+
return;
|
|
3548
|
+
} else if (this.categories.length === 1) {
|
|
3483
3549
|
const color2 = this._getColorAtPosition(this.state.transformMin);
|
|
3484
3550
|
this.categoryColorMap.set(this.categories[0], color2);
|
|
3485
3551
|
} else {
|
|
@@ -4356,11 +4422,69 @@ var Picker = (function() {
|
|
|
4356
4422
|
}]);
|
|
4357
4423
|
return Picker2;
|
|
4358
4424
|
})();
|
|
4359
|
-
{
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4425
|
+
function createControlGroup() {
|
|
4426
|
+
const group = document.createElement("div");
|
|
4427
|
+
group.className = "ht-control-group";
|
|
4428
|
+
return group;
|
|
4429
|
+
}
|
|
4430
|
+
function createLabel(text, height) {
|
|
4431
|
+
const label = document.createElement("label");
|
|
4432
|
+
label.className = "ht-control-label";
|
|
4433
|
+
label.textContent = text;
|
|
4434
|
+
label.style.height = `${height}px`;
|
|
4435
|
+
return label;
|
|
4436
|
+
}
|
|
4437
|
+
function createButton(text, title = "", height) {
|
|
4438
|
+
const button = document.createElement("button");
|
|
4439
|
+
button.className = "ht-button";
|
|
4440
|
+
button.textContent = text;
|
|
4441
|
+
button.title = title;
|
|
4442
|
+
button.style.height = `${height}px`;
|
|
4443
|
+
return button;
|
|
4444
|
+
}
|
|
4445
|
+
function createIconButton(iconSvg, title = "", height) {
|
|
4446
|
+
const button = document.createElement("button");
|
|
4447
|
+
button.className = "ht-icon-button";
|
|
4448
|
+
button.innerHTML = iconSvg;
|
|
4449
|
+
button.title = title;
|
|
4450
|
+
button.style.height = `${height}px`;
|
|
4451
|
+
button.style.width = `${height}px`;
|
|
4452
|
+
return button;
|
|
4453
|
+
}
|
|
4454
|
+
function createSlider(min, max, value, step, height) {
|
|
4455
|
+
const slider = document.createElement("input");
|
|
4456
|
+
slider.type = "range";
|
|
4457
|
+
slider.className = "ht-slider";
|
|
4458
|
+
slider.min = min;
|
|
4459
|
+
slider.max = max;
|
|
4460
|
+
slider.value = value;
|
|
4461
|
+
slider.step = step;
|
|
4462
|
+
slider.style.height = `${height}px`;
|
|
4463
|
+
return slider;
|
|
4464
|
+
}
|
|
4465
|
+
function createToggle(initialState, height) {
|
|
4466
|
+
const toggleHeight = Math.min(24, height - 4);
|
|
4467
|
+
const knobSize = toggleHeight - 4;
|
|
4468
|
+
const toggle = document.createElement("div");
|
|
4469
|
+
toggle.className = initialState ? "ht-toggle active" : "ht-toggle";
|
|
4470
|
+
toggle.style.height = `${toggleHeight}px`;
|
|
4471
|
+
const knob = document.createElement("div");
|
|
4472
|
+
knob.className = "ht-toggle-knob";
|
|
4473
|
+
knob.style.width = `${knobSize}px`;
|
|
4474
|
+
knob.style.height = `${knobSize}px`;
|
|
4475
|
+
toggle.appendChild(knob);
|
|
4476
|
+
return toggle;
|
|
4477
|
+
}
|
|
4478
|
+
function createNumberInput(value, min, max, step, height) {
|
|
4479
|
+
const input = document.createElement("input");
|
|
4480
|
+
input.type = "number";
|
|
4481
|
+
input.className = "ht-number-input";
|
|
4482
|
+
input.value = value;
|
|
4483
|
+
input.min = min;
|
|
4484
|
+
input.max = max;
|
|
4485
|
+
input.step = step;
|
|
4486
|
+
input.style.height = `${height}px`;
|
|
4487
|
+
return input;
|
|
4364
4488
|
}
|
|
4365
4489
|
class Aesthetic extends Subscribable {
|
|
4366
4490
|
state;
|
|
@@ -4369,7 +4493,9 @@ class Aesthetic extends Subscribable {
|
|
|
4369
4493
|
// the actual scale instance
|
|
4370
4494
|
values;
|
|
4371
4495
|
// Store the original values for scale recreation
|
|
4372
|
-
defaultPalette = ["#440154", "#
|
|
4496
|
+
defaultPalette = ["#440154", "#365C8D", "#1FA187", "#9FDA3A"];
|
|
4497
|
+
treeState;
|
|
4498
|
+
// Reference to the tree state for updates
|
|
4373
4499
|
constructor(values, options = {}) {
|
|
4374
4500
|
super();
|
|
4375
4501
|
if (!options.scaleType) {
|
|
@@ -4379,6 +4505,7 @@ class Aesthetic extends Subscribable {
|
|
|
4379
4505
|
throw new Error("default is required");
|
|
4380
4506
|
}
|
|
4381
4507
|
this.values = values;
|
|
4508
|
+
this.treeState = options.treeState || null;
|
|
4382
4509
|
this.state = {
|
|
4383
4510
|
scaleType: void 0,
|
|
4384
4511
|
default: void 0,
|
|
@@ -4396,7 +4523,8 @@ class Aesthetic extends Subscribable {
|
|
|
4396
4523
|
transformMin: 0,
|
|
4397
4524
|
transformMax: 1,
|
|
4398
4525
|
transformFn: null,
|
|
4399
|
-
nullValue:
|
|
4526
|
+
nullValue: "#808080",
|
|
4527
|
+
showNullInLegend: true,
|
|
4400
4528
|
...options
|
|
4401
4529
|
};
|
|
4402
4530
|
this.updateScale(false);
|
|
@@ -4499,23 +4627,113 @@ class Aesthetic extends Subscribable {
|
|
|
4499
4627
|
* Create settings widget(s) for this aesthetic
|
|
4500
4628
|
* @param {Object} options - Configuration options
|
|
4501
4629
|
* @param {number} options.controlHeight - Height of controls
|
|
4630
|
+
* @param {string} options.columnId - The column ID this aesthetic is mapped to
|
|
4502
4631
|
* @returns {HTMLElement|null} The settings widget container, or null if no settings available
|
|
4503
4632
|
*/
|
|
4504
4633
|
createSettingsWidget(options = {}) {
|
|
4505
4634
|
const {
|
|
4506
|
-
controlHeight =
|
|
4635
|
+
controlHeight = 20,
|
|
4636
|
+
columnId = null,
|
|
4637
|
+
root: root2 = document
|
|
4507
4638
|
} = options;
|
|
4639
|
+
if (!columnId) {
|
|
4640
|
+
const message = document.createElement("div");
|
|
4641
|
+
message.textContent = "Select a metadata column to edit its settings";
|
|
4642
|
+
message.style.padding = "10px";
|
|
4643
|
+
message.style.color = "#666";
|
|
4644
|
+
return message;
|
|
4645
|
+
}
|
|
4646
|
+
const container = document.createElement("div");
|
|
4647
|
+
container.className = "ht-aesthetic-settings-content";
|
|
4508
4648
|
if (this.state.scaleType === "color") {
|
|
4509
|
-
|
|
4649
|
+
const paletteEditor = this.createColorPaletteEditor(controlHeight, root2);
|
|
4650
|
+
if (paletteEditor) {
|
|
4651
|
+
container.appendChild(paletteEditor);
|
|
4652
|
+
}
|
|
4653
|
+
if (this.state.isCategorical) {
|
|
4654
|
+
const maxCategoriesGroup = createControlGroup();
|
|
4655
|
+
const maxCategoriesLabel = createLabel("Max colors:", controlHeight);
|
|
4656
|
+
maxCategoriesGroup.appendChild(maxCategoriesLabel);
|
|
4657
|
+
const maxCategoriesInput = createNumberInput(
|
|
4658
|
+
this.state.maxCategories || 7,
|
|
4659
|
+
2,
|
|
4660
|
+
100,
|
|
4661
|
+
1,
|
|
4662
|
+
controlHeight
|
|
4663
|
+
);
|
|
4664
|
+
maxCategoriesInput.addEventListener("input", (e) => {
|
|
4665
|
+
const value = parseInt(e.target.value);
|
|
4666
|
+
if (isNaN(value) || value < 1) return;
|
|
4667
|
+
this.updateState({ maxCategories: value });
|
|
4668
|
+
this.updateScale(this.values);
|
|
4669
|
+
if (this.treeState) {
|
|
4670
|
+
this.treeState.state.treeData.tree.each((node) => {
|
|
4671
|
+
const columnValue = node[columnId];
|
|
4672
|
+
if (columnValue !== void 0 && columnValue !== null) {
|
|
4673
|
+
node.tipLabelColor = this.getValue(columnValue);
|
|
4674
|
+
}
|
|
4675
|
+
});
|
|
4676
|
+
this.treeState.updateCoordinates();
|
|
4677
|
+
this.treeState.notify("legendsChange");
|
|
4678
|
+
}
|
|
4679
|
+
});
|
|
4680
|
+
maxCategoriesGroup.appendChild(maxCategoriesInput);
|
|
4681
|
+
container.appendChild(maxCategoriesGroup);
|
|
4682
|
+
}
|
|
4683
|
+
const titleGroup = createControlGroup();
|
|
4684
|
+
const titleLabel = createLabel("Legend title:", controlHeight);
|
|
4685
|
+
titleGroup.appendChild(titleLabel);
|
|
4686
|
+
const titleInput = document.createElement("input");
|
|
4687
|
+
titleInput.type = "text";
|
|
4688
|
+
titleInput.className = "ht-text-input";
|
|
4689
|
+
titleInput.style.height = `${controlHeight}px`;
|
|
4690
|
+
titleInput.style.flex = "1";
|
|
4691
|
+
titleInput.value = this.state.title || "";
|
|
4692
|
+
titleInput.placeholder = "Enter legend title";
|
|
4693
|
+
titleInput.addEventListener("input", (e) => {
|
|
4694
|
+
this.updateState({ title: e.target.value });
|
|
4695
|
+
if (this.treeState) {
|
|
4696
|
+
this.treeState.notify("legendsChange");
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4699
|
+
titleGroup.appendChild(titleInput);
|
|
4700
|
+
container.appendChild(titleGroup);
|
|
4701
|
+
if (!this.state.isCategorical) {
|
|
4702
|
+
const unitsGroup = createControlGroup();
|
|
4703
|
+
const unitsLabel = createLabel("Units label:", controlHeight);
|
|
4704
|
+
unitsGroup.appendChild(unitsLabel);
|
|
4705
|
+
const unitsInput = document.createElement("input");
|
|
4706
|
+
unitsInput.type = "text";
|
|
4707
|
+
unitsInput.className = "ht-text-input";
|
|
4708
|
+
unitsInput.style.height = `${controlHeight}px`;
|
|
4709
|
+
unitsInput.style.flex = "1";
|
|
4710
|
+
unitsInput.value = this.state.inputUnits || "";
|
|
4711
|
+
unitsInput.placeholder = "Enter units (e.g., °C, km)";
|
|
4712
|
+
unitsInput.addEventListener("input", (e) => {
|
|
4713
|
+
this.updateState({ inputUnits: e.target.value });
|
|
4714
|
+
if (this.treeState) {
|
|
4715
|
+
this.treeState.notify("legendsChange");
|
|
4716
|
+
}
|
|
4717
|
+
});
|
|
4718
|
+
unitsGroup.appendChild(unitsInput);
|
|
4719
|
+
container.appendChild(unitsGroup);
|
|
4720
|
+
}
|
|
4510
4721
|
}
|
|
4511
|
-
|
|
4722
|
+
if (container.children.length === 0) {
|
|
4723
|
+
const placeholder = document.createElement("div");
|
|
4724
|
+
placeholder.textContent = "No settings available for this aesthetic";
|
|
4725
|
+
placeholder.style.padding = "10px";
|
|
4726
|
+
placeholder.style.color = "#666";
|
|
4727
|
+
container.appendChild(placeholder);
|
|
4728
|
+
}
|
|
4729
|
+
return container;
|
|
4512
4730
|
}
|
|
4513
4731
|
/**
|
|
4514
4732
|
* Create a color palette editor widget
|
|
4515
4733
|
* @param {number} controlHeight - Height of controls
|
|
4516
4734
|
* @returns {HTMLElement} The palette editor container
|
|
4517
4735
|
*/
|
|
4518
|
-
createColorPaletteEditor(controlHeight) {
|
|
4736
|
+
createColorPaletteEditor(controlHeight, root2 = document) {
|
|
4519
4737
|
if (!this.scale) {
|
|
4520
4738
|
return null;
|
|
4521
4739
|
}
|
|
@@ -4538,16 +4756,17 @@ class Aesthetic extends Subscribable {
|
|
|
4538
4756
|
};
|
|
4539
4757
|
updateGradientDisplay();
|
|
4540
4758
|
let currentPickerParent = null;
|
|
4759
|
+
const pickerParent = root2 !== document && root2.appendChild ? root2 : document.body;
|
|
4541
4760
|
const pickerContainer = document.createElement("div");
|
|
4542
4761
|
pickerContainer.style.position = "fixed";
|
|
4543
4762
|
pickerContainer.style.zIndex = "999999";
|
|
4544
4763
|
pickerContainer.style.pointerEvents = "auto";
|
|
4545
|
-
|
|
4764
|
+
pickerParent.appendChild(pickerContainer);
|
|
4546
4765
|
const nullPickerContainer = document.createElement("div");
|
|
4547
4766
|
nullPickerContainer.style.position = "fixed";
|
|
4548
4767
|
nullPickerContainer.style.zIndex = "999999";
|
|
4549
4768
|
nullPickerContainer.style.pointerEvents = "auto";
|
|
4550
|
-
|
|
4769
|
+
pickerParent.appendChild(nullPickerContainer);
|
|
4551
4770
|
const closeGradientPicker = () => {
|
|
4552
4771
|
pickerContainer.style.display = "none";
|
|
4553
4772
|
currentPickerParent = null;
|
|
@@ -4599,12 +4818,13 @@ class Aesthetic extends Subscribable {
|
|
|
4599
4818
|
}
|
|
4600
4819
|
});
|
|
4601
4820
|
pickerContainer.style.display = "none";
|
|
4821
|
+
const clickTarget = root2 !== document && root2.addEventListener ? root2 : document;
|
|
4602
4822
|
const closePickerOnClickOutside = (e) => {
|
|
4603
4823
|
if (!pickerContainer.contains(e.target) && !e.target.closest(".ht-color-square")) {
|
|
4604
4824
|
closeGradientPicker();
|
|
4605
4825
|
}
|
|
4606
4826
|
};
|
|
4607
|
-
|
|
4827
|
+
clickTarget.addEventListener("click", closePickerOnClickOutside);
|
|
4608
4828
|
const nullColorPicker = new Picker({
|
|
4609
4829
|
parent: nullPickerContainer,
|
|
4610
4830
|
popup: false,
|
|
@@ -4628,11 +4848,11 @@ class Aesthetic extends Subscribable {
|
|
|
4628
4848
|
closeNullPicker();
|
|
4629
4849
|
}
|
|
4630
4850
|
};
|
|
4631
|
-
|
|
4851
|
+
clickTarget.addEventListener("click", closeNullPickerOnClickOutside);
|
|
4632
4852
|
container.dataset.pickerCleanup = "cleanup";
|
|
4633
4853
|
container.cleanupFunction = () => {
|
|
4634
|
-
|
|
4635
|
-
|
|
4854
|
+
clickTarget.removeEventListener("click", closePickerOnClickOutside);
|
|
4855
|
+
clickTarget.removeEventListener("click", closeNullPickerOnClickOutside);
|
|
4636
4856
|
if (pickerContainer.parentElement) {
|
|
4637
4857
|
pickerContainer.parentElement.removeChild(pickerContainer);
|
|
4638
4858
|
}
|
|
@@ -4733,27 +4953,18 @@ class Aesthetic extends Subscribable {
|
|
|
4733
4953
|
nullPickerContainer.style.top = `${rect.bottom + 5}px`;
|
|
4734
4954
|
nullPickerContainer.style.display = "block";
|
|
4735
4955
|
});
|
|
4736
|
-
const
|
|
4737
|
-
|
|
4738
|
-
resetContainer.title = "Reset to default missing data color";
|
|
4956
|
+
const missingDataXContainer = document.createElement("div");
|
|
4957
|
+
missingDataXContainer.className = "missing-data-x-container";
|
|
4739
4958
|
const resetIndicator = document.createElement("div");
|
|
4740
|
-
resetIndicator.className = "
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
this.state.nullValue = defaultNullColor;
|
|
4747
|
-
nullSquare.style.backgroundColor = defaultNullColor;
|
|
4748
|
-
nullColorBox.style.backgroundColor = defaultNullColor;
|
|
4749
|
-
nullColorPicker.setColor(defaultNullColor, true);
|
|
4750
|
-
this.updateScale();
|
|
4751
|
-
});
|
|
4752
|
-
resetContainer.appendChild(resetIndicator);
|
|
4753
|
-
resetContainer.appendChild(resetX);
|
|
4959
|
+
resetIndicator.className = "missing-data-x-container-triangle";
|
|
4960
|
+
const missingDataX = document.createElement("div");
|
|
4961
|
+
missingDataX.className = "missing-data-x";
|
|
4962
|
+
missingDataX.textContent = "✕";
|
|
4963
|
+
missingDataXContainer.appendChild(resetIndicator);
|
|
4964
|
+
missingDataXContainer.appendChild(missingDataX);
|
|
4754
4965
|
nullColorColumn.appendChild(nullColorSquareContainer);
|
|
4755
4966
|
nullColorColumn.appendChild(nullColorBox);
|
|
4756
|
-
nullColorColumn.appendChild(
|
|
4967
|
+
nullColorColumn.appendChild(missingDataXContainer);
|
|
4757
4968
|
gradientContainer.appendChild(gradientColumn);
|
|
4758
4969
|
gradientContainer.appendChild(nullColorColumn);
|
|
4759
4970
|
const leftButtonsContainer = document.createElement("div");
|
|
@@ -4909,9 +5120,15 @@ class TreeData extends Subscribable {
|
|
|
4909
5120
|
validIdColumns = /* @__PURE__ */ new Map();
|
|
4910
5121
|
// Map of table ID to array of column names that contain valid node IDs
|
|
4911
5122
|
#nextTableId = 0;
|
|
4912
|
-
|
|
5123
|
+
/**
|
|
5124
|
+
* Create TreeData from a parsed tree object
|
|
5125
|
+
* @param {Object} treeDataObj - Parsed tree object from parseNewick or parseNexus
|
|
5126
|
+
* @param {Array} metadataTables - Optional array of metadata table strings
|
|
5127
|
+
* @param {Array} metadataTableNames - Optional array of metadata table names
|
|
5128
|
+
*/
|
|
5129
|
+
constructor(treeDataObj, metadataTables = [], metadataTableNames = []) {
|
|
4913
5130
|
super();
|
|
4914
|
-
this.tree = this.
|
|
5131
|
+
this.tree = this.createHierarchy(treeDataObj);
|
|
4915
5132
|
if (Array.isArray(metadataTables)) {
|
|
4916
5133
|
metadataTables.forEach((tableStr, index) => {
|
|
4917
5134
|
this.addTable(tableStr, metadataTableNames[index]);
|
|
@@ -4919,12 +5136,53 @@ class TreeData extends Subscribable {
|
|
|
4919
5136
|
}
|
|
4920
5137
|
}
|
|
4921
5138
|
/**
|
|
4922
|
-
*
|
|
4923
|
-
* @param {string}
|
|
5139
|
+
* Static factory method to create TreeData from tree string (Newick or NEXUS)
|
|
5140
|
+
* @param {string} treeString - Newick or NEXUS formatted string
|
|
5141
|
+
* @param {Array} metadataTables - Optional array of metadata table strings
|
|
5142
|
+
* @param {Array} metadataTableNames - Optional array of metadata table names
|
|
5143
|
+
* @returns {TreeData} New TreeData instance
|
|
5144
|
+
*/
|
|
5145
|
+
static fromTreeString(treeString, metadataTables = [], metadataTableNames = []) {
|
|
5146
|
+
const trees = TreeData.parseTrees(treeString, "Tree");
|
|
5147
|
+
if (trees.length !== 1) {
|
|
5148
|
+
throw new Error(`Expected exactly one tree, but found ${trees.length}. Use TreeData.parseTrees() for multiple trees.`);
|
|
5149
|
+
}
|
|
5150
|
+
return new TreeData(trees[0].treeData, metadataTables, metadataTableNames);
|
|
5151
|
+
}
|
|
5152
|
+
/**
|
|
5153
|
+
* Parse tree input (Newick or NEXUS) and return array of trees with naming
|
|
5154
|
+
* @param {string} input - Tree input string
|
|
5155
|
+
* @param {string} sourceName - Base name for the tree(s) (e.g., filename or user-provided)
|
|
5156
|
+
* @returns {Array<{name: string, treeData: Object}>} Array of parsed trees with names
|
|
5157
|
+
*/
|
|
5158
|
+
static parseTrees(input, sourceName = "Tree") {
|
|
5159
|
+
let trees;
|
|
5160
|
+
if (isNexusFormat(input)) {
|
|
5161
|
+
trees = parseNexus(input);
|
|
5162
|
+
} else {
|
|
5163
|
+
const treeData = parseNewick(input);
|
|
5164
|
+
trees = [{ treeName: null, treeData }];
|
|
5165
|
+
}
|
|
5166
|
+
const multiple = trees.length > 1;
|
|
5167
|
+
return trees.map((tree, index) => {
|
|
5168
|
+
let name;
|
|
5169
|
+
if (tree.treeName) {
|
|
5170
|
+
name = multiple ? `${sourceName} ${tree.treeName}` : `${sourceName} - ${tree.treeName}`;
|
|
5171
|
+
} else {
|
|
5172
|
+
name = multiple ? `${sourceName} ${index + 1}` : sourceName;
|
|
5173
|
+
}
|
|
5174
|
+
return {
|
|
5175
|
+
name,
|
|
5176
|
+
treeData: tree.treeData
|
|
5177
|
+
};
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
/**
|
|
5181
|
+
* Create a D3 hierarchy from parsed tree data
|
|
5182
|
+
* @param {Object} treeData - Parsed tree object from parseNewick
|
|
4924
5183
|
* @returns {object} D3 hierarchy object
|
|
4925
5184
|
*/
|
|
4926
|
-
|
|
4927
|
-
const treeData = parseNewick(newickStr);
|
|
5185
|
+
createHierarchy(treeData) {
|
|
4928
5186
|
const root2 = hierarchy(treeData, (d) => d.children).sum((d) => d.children ? 0 : 1).each(function(d) {
|
|
4929
5187
|
d.leafCount = d.value;
|
|
4930
5188
|
delete d.value;
|
|
@@ -4938,10 +5196,10 @@ class TreeData extends Subscribable {
|
|
|
4938
5196
|
}
|
|
4939
5197
|
/**
|
|
4940
5198
|
* Parse and set the tree data
|
|
4941
|
-
* @param {
|
|
5199
|
+
* @param {Object} treeDataObj - Parsed tree object
|
|
4942
5200
|
*/
|
|
4943
|
-
setTree(
|
|
4944
|
-
this.tree = this.
|
|
5201
|
+
setTree(treeDataObj) {
|
|
5202
|
+
this.tree = this.createHierarchy(treeDataObj);
|
|
4945
5203
|
this.metadata.keys().forEach((tableId) => this.#attachTable(tableId));
|
|
4946
5204
|
this.notify("treeUpdated", this);
|
|
4947
5205
|
}
|
|
@@ -5170,15 +5428,26 @@ class TreeData extends Subscribable {
|
|
|
5170
5428
|
}
|
|
5171
5429
|
let values = [];
|
|
5172
5430
|
this.tree.each((node) => {
|
|
5173
|
-
if (
|
|
5431
|
+
if (state.subset == "tips" && node.children) {
|
|
5432
|
+
return;
|
|
5433
|
+
}
|
|
5434
|
+
if (node.metadata) {
|
|
5174
5435
|
values.push(node.metadata[columnId]);
|
|
5436
|
+
} else {
|
|
5437
|
+
values.push(void 0);
|
|
5175
5438
|
}
|
|
5176
5439
|
});
|
|
5177
5440
|
if (values.length === 0) {
|
|
5178
5441
|
console.error(`No values found for column ${columnId}`);
|
|
5179
5442
|
}
|
|
5180
5443
|
const displayName = this.columnDisplayName.get(columnId) || columnId;
|
|
5444
|
+
let scaleType = state.scaleType;
|
|
5445
|
+
if (!scaleType) {
|
|
5446
|
+
scaleType = "color";
|
|
5447
|
+
}
|
|
5181
5448
|
const aesthetic = new Aesthetic(values, {
|
|
5449
|
+
scaleType,
|
|
5450
|
+
default: state.default !== void 0 ? state.default : isCategorical ? null : 0,
|
|
5182
5451
|
isCategorical,
|
|
5183
5452
|
inputUnits: displayName,
|
|
5184
5453
|
...state
|
|
@@ -5367,7 +5636,7 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5367
5636
|
}
|
|
5368
5637
|
const minLabelScale = Math.min(...leafData.map((a) => a.labelScale));
|
|
5369
5638
|
const maxBranchX = Math.max(...leafData.map((a) => a.radius));
|
|
5370
|
-
const
|
|
5639
|
+
const meanBranchX = leafData.reduce((sum, x) => sum + x.radius, 0) / leafData.length;
|
|
5371
5640
|
const nonZeroBranches = root2.descendants().filter((a) => a.data.length > 0 && a.children);
|
|
5372
5641
|
const minBranchLength = nonZeroBranches.length > 0 ? Math.min(...nonZeroBranches.map((a) => a.data.length)) : Infinity;
|
|
5373
5642
|
applyLabelMin(options.minFontPx / minLabelScale);
|
|
@@ -5378,6 +5647,15 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5378
5647
|
))
|
|
5379
5648
|
);
|
|
5380
5649
|
}
|
|
5650
|
+
const totalAnnotationHeight = leafData.reduce((sum, a) => sum + a.height, 0);
|
|
5651
|
+
if (totalAnnotationHeight > 0 && meanBranchX > 0) {
|
|
5652
|
+
if (branchLenToPxFactor_min !== branchLenToPxFactor_max) {
|
|
5653
|
+
applyBranchMin(totalAnnotationHeight * labelSizeToPxFactor_min / (meanBranchX * 2 * Math.PI));
|
|
5654
|
+
}
|
|
5655
|
+
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5656
|
+
applyLabelMax(maxBranchX * branchLenToPxFactor_max * 2 * Math.PI / totalAnnotationHeight);
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5381
5659
|
function applyBranchViewConstraint() {
|
|
5382
5660
|
const rightBranchFactor = Math.min(...leafData.filter((a) => a.cos > 0).map((a) => (options.viewWidth / 2 - a.width * a.cos * labelSizeToPxFactor_min) / (a.radius * a.cos)));
|
|
5383
5661
|
const leftBranchFactor = Math.min(...leafData.filter((a) => a.cos < 0).map((a) => (options.viewWidth / 2 - a.width * -a.cos * labelSizeToPxFactor_min) / (a.radius * -a.cos)));
|
|
@@ -5400,18 +5678,6 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5400
5678
|
}
|
|
5401
5679
|
applyBranchViewConstraint();
|
|
5402
5680
|
applyLabelViewConstraint();
|
|
5403
|
-
const totalAnnotationHeight = leafData.reduce((sum, a) => sum + a.height, 0);
|
|
5404
|
-
if (totalAnnotationHeight > 0 && minBranchX > 0) {
|
|
5405
|
-
if (branchLenToPxFactor_min !== branchLenToPxFactor_max) {
|
|
5406
|
-
applyBranchMin(totalAnnotationHeight * labelSizeToPxFactor_min / (minBranchX * 2 * Math.PI));
|
|
5407
|
-
}
|
|
5408
|
-
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5409
|
-
applyLabelMax(maxBranchX * branchLenToPxFactor_max * 2 * Math.PI / totalAnnotationHeight);
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5413
|
-
applyLabelViewConstraint();
|
|
5414
|
-
}
|
|
5415
5681
|
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5416
5682
|
applyLabelMin(options.idealFontPx / minLabelScale);
|
|
5417
5683
|
}
|
|
@@ -5511,7 +5777,8 @@ class TreeState extends Subscribable {
|
|
|
5511
5777
|
default: "",
|
|
5512
5778
|
nullValue: "",
|
|
5513
5779
|
downstream: ["updateTipLabelText", "updateCoordinates"],
|
|
5514
|
-
hasLegend: false
|
|
5780
|
+
hasLegend: false,
|
|
5781
|
+
subset: "tips"
|
|
5515
5782
|
},
|
|
5516
5783
|
tipLabelColor: {
|
|
5517
5784
|
title: "Tip label color",
|
|
@@ -5520,7 +5787,8 @@ class TreeState extends Subscribable {
|
|
|
5520
5787
|
nullValue: "#808080",
|
|
5521
5788
|
otherCategory: "#555555",
|
|
5522
5789
|
downstream: [],
|
|
5523
|
-
hasLegend: true
|
|
5790
|
+
hasLegend: true,
|
|
5791
|
+
subset: "tips"
|
|
5524
5792
|
},
|
|
5525
5793
|
tipLabelSize: {
|
|
5526
5794
|
title: "Tip label size",
|
|
@@ -5530,7 +5798,8 @@ class TreeState extends Subscribable {
|
|
|
5530
5798
|
isCategorical: false,
|
|
5531
5799
|
outputRange: [0.5, 2],
|
|
5532
5800
|
downstream: ["updateCoordinates"],
|
|
5533
|
-
hasLegend: true
|
|
5801
|
+
hasLegend: true,
|
|
5802
|
+
subset: "tips"
|
|
5534
5803
|
},
|
|
5535
5804
|
tipLabelFont: {
|
|
5536
5805
|
title: "Tip label font",
|
|
@@ -5538,7 +5807,8 @@ class TreeState extends Subscribable {
|
|
|
5538
5807
|
default: "sans-serif",
|
|
5539
5808
|
nullValue: "sans-serif",
|
|
5540
5809
|
downstream: ["updateCoordinates"],
|
|
5541
|
-
hasLegend: false
|
|
5810
|
+
hasLegend: false,
|
|
5811
|
+
subset: "tips"
|
|
5542
5812
|
},
|
|
5543
5813
|
tipLabelStyle: {
|
|
5544
5814
|
title: "Tip label font style",
|
|
@@ -5548,7 +5818,8 @@ class TreeState extends Subscribable {
|
|
|
5548
5818
|
nullValue: "normal",
|
|
5549
5819
|
otherCategory: "italic",
|
|
5550
5820
|
downstream: ["updateCoordinates"],
|
|
5551
|
-
hasLegend: false
|
|
5821
|
+
hasLegend: false,
|
|
5822
|
+
subset: "tips"
|
|
5552
5823
|
},
|
|
5553
5824
|
nodeLabelText: {
|
|
5554
5825
|
title: "Node label text",
|
|
@@ -5556,7 +5827,8 @@ class TreeState extends Subscribable {
|
|
|
5556
5827
|
default: "",
|
|
5557
5828
|
nullValue: "",
|
|
5558
5829
|
downstream: ["updateNodeLabelText"],
|
|
5559
|
-
hasLegend: false
|
|
5830
|
+
hasLegend: false,
|
|
5831
|
+
subset: "all"
|
|
5560
5832
|
},
|
|
5561
5833
|
nodeLabelSize: {
|
|
5562
5834
|
title: "Node label size",
|
|
@@ -5566,7 +5838,8 @@ class TreeState extends Subscribable {
|
|
|
5566
5838
|
isCategorical: false,
|
|
5567
5839
|
outputRange: [0.5, 2],
|
|
5568
5840
|
downstream: ["updateCoordinates"],
|
|
5569
|
-
hasLegend: false
|
|
5841
|
+
hasLegend: false,
|
|
5842
|
+
subset: "all"
|
|
5570
5843
|
}
|
|
5571
5844
|
};
|
|
5572
5845
|
state = {
|
|
@@ -6396,9 +6669,11 @@ class TextColorLegend extends LegendBase {
|
|
|
6396
6669
|
let currentX = 0;
|
|
6397
6670
|
let currentY = titleHeightOffset + this.verticalSpacing + this.squareSize / 2;
|
|
6398
6671
|
let rowHeight = this.squareSize;
|
|
6399
|
-
categories.slice(0, aesthetic.state.maxCategories)
|
|
6672
|
+
const categoriesToShow = categories.slice(0, aesthetic.state.maxCategories);
|
|
6673
|
+
categoriesToShow.forEach((category, i) => {
|
|
6400
6674
|
const color2 = aesthetic.scale.getValue(category);
|
|
6401
|
-
const
|
|
6675
|
+
const labelText = category === "" ? "No data" : category;
|
|
6676
|
+
const labelSize = this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6402
6677
|
const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
|
|
6403
6678
|
if (currentX > 0 && currentX + itemWidth > maxWidth) {
|
|
6404
6679
|
currentX = 0;
|
|
@@ -6409,7 +6684,7 @@ class TextColorLegend extends LegendBase {
|
|
|
6409
6684
|
x: currentX,
|
|
6410
6685
|
y: currentY,
|
|
6411
6686
|
color: color2,
|
|
6412
|
-
label: i < aesthetic.state.maxCategories - 1 ?
|
|
6687
|
+
label: i < aesthetic.state.maxCategories - 1 || i == categories.length - 1 ? labelText : aesthetic.state.otherLabel,
|
|
6413
6688
|
squareX: currentX,
|
|
6414
6689
|
squareY: currentY - this.squareSize / 2,
|
|
6415
6690
|
labelX: currentX + this.squareSize + this.itemLabelGap,
|
|
@@ -6418,6 +6693,30 @@ class TextColorLegend extends LegendBase {
|
|
|
6418
6693
|
currentX += itemWidth + this.itemGap;
|
|
6419
6694
|
this.coordinates.width = Math.max(this.coordinates.width, currentX - this.itemGap);
|
|
6420
6695
|
});
|
|
6696
|
+
const hasNullValues = aesthetic.values.some((x) => x == void 0);
|
|
6697
|
+
if (aesthetic.state.showNullInLegend && hasNullValues) {
|
|
6698
|
+
const color2 = aesthetic.state.nullValue;
|
|
6699
|
+
const labelText = "No Data";
|
|
6700
|
+
const labelSize = this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6701
|
+
const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
|
|
6702
|
+
if (currentX > 0 && currentX + itemWidth > maxWidth) {
|
|
6703
|
+
currentX = 0;
|
|
6704
|
+
currentY += rowHeight + this.verticalSpacing;
|
|
6705
|
+
rowHeight = this.squareSize;
|
|
6706
|
+
}
|
|
6707
|
+
this.coordinates.items.push({
|
|
6708
|
+
x: currentX,
|
|
6709
|
+
y: currentY,
|
|
6710
|
+
color: color2,
|
|
6711
|
+
label: labelText,
|
|
6712
|
+
squareX: currentX,
|
|
6713
|
+
squareY: currentY - this.squareSize / 2,
|
|
6714
|
+
labelX: currentX + this.squareSize + this.itemLabelGap,
|
|
6715
|
+
labelY: currentY
|
|
6716
|
+
});
|
|
6717
|
+
currentX += itemWidth + this.itemGap;
|
|
6718
|
+
this.coordinates.width = Math.max(this.coordinates.width, currentX - this.itemGap);
|
|
6719
|
+
}
|
|
6421
6720
|
this.coordinates.height = currentY + this.squareSize / 2;
|
|
6422
6721
|
}
|
|
6423
6722
|
/**
|
|
@@ -6441,7 +6740,7 @@ class TextColorLegend extends LegendBase {
|
|
|
6441
6740
|
const ticksY = gradientY + this.gradientHeight;
|
|
6442
6741
|
const labelsY = ticksY + this.tickHeight;
|
|
6443
6742
|
const unitsSize = this.textSizeEstimator.getTextSize(aesthetic.state.inputUnits || "", this.state.labelFontSize);
|
|
6444
|
-
|
|
6743
|
+
let height = labelsY + this.state.labelFontSize + unitsSize.heightPx;
|
|
6445
6744
|
this.coordinates = {
|
|
6446
6745
|
width,
|
|
6447
6746
|
height,
|
|
@@ -6464,7 +6763,8 @@ class TextColorLegend extends LegendBase {
|
|
|
6464
6763
|
x: width / 2,
|
|
6465
6764
|
y: height,
|
|
6466
6765
|
text: aesthetic.state.inputUnits || ""
|
|
6467
|
-
}
|
|
6766
|
+
},
|
|
6767
|
+
nullItem: null
|
|
6468
6768
|
};
|
|
6469
6769
|
if (minValue === maxValue) {
|
|
6470
6770
|
const x = leftOverhang + baseWidth / 2;
|
|
@@ -6495,6 +6795,23 @@ class TextColorLegend extends LegendBase {
|
|
|
6495
6795
|
});
|
|
6496
6796
|
});
|
|
6497
6797
|
}
|
|
6798
|
+
if (aesthetic.state.showNullInLegend) {
|
|
6799
|
+
const color2 = aesthetic.state.nullValue;
|
|
6800
|
+
const labelText = "No Data";
|
|
6801
|
+
this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6802
|
+
const nullItemY = height + this.verticalSpacing;
|
|
6803
|
+
this.coordinates.nullItem = {
|
|
6804
|
+
x: leftOverhang,
|
|
6805
|
+
y: nullItemY + this.squareSize / 2,
|
|
6806
|
+
color: color2,
|
|
6807
|
+
label: labelText,
|
|
6808
|
+
squareX: leftOverhang,
|
|
6809
|
+
squareY: nullItemY,
|
|
6810
|
+
labelX: leftOverhang + this.squareSize + this.itemLabelGap,
|
|
6811
|
+
labelY: nullItemY + this.squareSize / 2
|
|
6812
|
+
};
|
|
6813
|
+
this.coordinates.height = nullItemY + this.squareSize;
|
|
6814
|
+
}
|
|
6498
6815
|
}
|
|
6499
6816
|
/**
|
|
6500
6817
|
* Render the legend in the specified SVG element
|
|
@@ -7575,7 +7892,7 @@ class TreeView {
|
|
|
7575
7892
|
}
|
|
7576
7893
|
}
|
|
7577
7894
|
}
|
|
7578
|
-
function exportTree(treeView, exportState, filename) {
|
|
7895
|
+
function exportTree(treeView, exportState, filename, root2 = document) {
|
|
7579
7896
|
const bounds = treeView.getCurrentBoundsWithLegends();
|
|
7580
7897
|
const contentWidth = bounds.maxX - bounds.minX;
|
|
7581
7898
|
const contentHeight = bounds.maxY - bounds.minY;
|
|
@@ -7646,72 +7963,8 @@ function convertSvgToPng(svgString, width, height, filename) {
|
|
|
7646
7963
|
};
|
|
7647
7964
|
img.src = url;
|
|
7648
7965
|
}
|
|
7649
|
-
function
|
|
7650
|
-
const
|
|
7651
|
-
group.className = "ht-control-group";
|
|
7652
|
-
return group;
|
|
7653
|
-
}
|
|
7654
|
-
function createLabel(text, height) {
|
|
7655
|
-
const label = document.createElement("label");
|
|
7656
|
-
label.className = "ht-control-label";
|
|
7657
|
-
label.textContent = text;
|
|
7658
|
-
label.style.height = `${height}px`;
|
|
7659
|
-
return label;
|
|
7660
|
-
}
|
|
7661
|
-
function createButton(text, title = "", height) {
|
|
7662
|
-
const button = document.createElement("button");
|
|
7663
|
-
button.className = "ht-button";
|
|
7664
|
-
button.textContent = text;
|
|
7665
|
-
button.title = title;
|
|
7666
|
-
button.style.height = `${height}px`;
|
|
7667
|
-
return button;
|
|
7668
|
-
}
|
|
7669
|
-
function createIconButton(iconSvg, title = "", height) {
|
|
7670
|
-
const button = document.createElement("button");
|
|
7671
|
-
button.className = "ht-icon-button";
|
|
7672
|
-
button.innerHTML = iconSvg;
|
|
7673
|
-
button.title = title;
|
|
7674
|
-
button.style.height = `${height}px`;
|
|
7675
|
-
button.style.width = `${height}px`;
|
|
7676
|
-
return button;
|
|
7677
|
-
}
|
|
7678
|
-
function createSlider(min, max, value, step, height) {
|
|
7679
|
-
const slider = document.createElement("input");
|
|
7680
|
-
slider.type = "range";
|
|
7681
|
-
slider.className = "ht-slider";
|
|
7682
|
-
slider.min = min;
|
|
7683
|
-
slider.max = max;
|
|
7684
|
-
slider.value = value;
|
|
7685
|
-
slider.step = step;
|
|
7686
|
-
slider.style.height = `${height}px`;
|
|
7687
|
-
return slider;
|
|
7688
|
-
}
|
|
7689
|
-
function createToggle(initialState, height) {
|
|
7690
|
-
const toggleHeight = Math.min(24, height - 4);
|
|
7691
|
-
const knobSize = toggleHeight - 4;
|
|
7692
|
-
const toggle = document.createElement("div");
|
|
7693
|
-
toggle.className = initialState ? "ht-toggle active" : "ht-toggle";
|
|
7694
|
-
toggle.style.height = `${toggleHeight}px`;
|
|
7695
|
-
const knob = document.createElement("div");
|
|
7696
|
-
knob.className = "ht-toggle-knob";
|
|
7697
|
-
knob.style.width = `${knobSize}px`;
|
|
7698
|
-
knob.style.height = `${knobSize}px`;
|
|
7699
|
-
toggle.appendChild(knob);
|
|
7700
|
-
return toggle;
|
|
7701
|
-
}
|
|
7702
|
-
function createNumberInput(value, min, max, step, height) {
|
|
7703
|
-
const input = document.createElement("input");
|
|
7704
|
-
input.type = "number";
|
|
7705
|
-
input.className = "ht-number-input";
|
|
7706
|
-
input.value = value;
|
|
7707
|
-
input.min = min;
|
|
7708
|
-
input.max = max;
|
|
7709
|
-
input.step = step;
|
|
7710
|
-
input.style.height = `${height}px`;
|
|
7711
|
-
return input;
|
|
7712
|
-
}
|
|
7713
|
-
function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCurrentTreeView, switchToTree, addNewTree, options) {
|
|
7714
|
-
const CONTROL_HEIGHT = 24;
|
|
7966
|
+
function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCurrentTreeView, switchToTree, addNewTree, options, root2 = document) {
|
|
7967
|
+
const CONTROL_HEIGHT = 20;
|
|
7715
7968
|
let currentTab = null;
|
|
7716
7969
|
let selectedMetadata = null;
|
|
7717
7970
|
let currentAestheticSettings = null;
|
|
@@ -7795,12 +8048,15 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
7795
8048
|
}
|
|
7796
8049
|
}
|
|
7797
8050
|
}
|
|
7798
|
-
toggleButton.addEventListener("click", () => {
|
|
8051
|
+
toggleButton.addEventListener("click", (e) => {
|
|
8052
|
+
e.preventDefault();
|
|
8053
|
+
e.stopPropagation();
|
|
7799
8054
|
controlPanelVisible = !controlPanelVisible;
|
|
7800
8055
|
if (controlPanelVisible) {
|
|
7801
8056
|
collapsiblePanel.classList.remove("ht-panel-collapsed");
|
|
7802
8057
|
toggleButton.classList.remove("collapsed");
|
|
7803
8058
|
} else {
|
|
8059
|
+
closeAestheticSettings();
|
|
7804
8060
|
collapsiblePanel.classList.add("ht-panel-collapsed");
|
|
7805
8061
|
toggleButton.classList.add("collapsed");
|
|
7806
8062
|
}
|
|
@@ -7889,14 +8145,24 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
7889
8145
|
aestheticSettingsContainer.innerHTML = "";
|
|
7890
8146
|
const treeState = getCurrentTreeState();
|
|
7891
8147
|
if (!treeState) return;
|
|
7892
|
-
|
|
7893
|
-
|
|
8148
|
+
const columnId = treeState.state.aesthetics[aestheticId];
|
|
8149
|
+
const aesthetic = treeState.aestheticsScales[aestheticId];
|
|
8150
|
+
if (!aesthetic) {
|
|
8151
|
+
const message = document.createElement("div");
|
|
8152
|
+
message.textContent = "Error: Could not find aesthetic";
|
|
8153
|
+
message.style.padding = "10px";
|
|
8154
|
+
message.style.color = "#d00";
|
|
8155
|
+
aestheticSettingsContainer.appendChild(message);
|
|
7894
8156
|
return;
|
|
7895
8157
|
}
|
|
7896
|
-
const
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
8158
|
+
const settingsWidget = aesthetic.createSettingsWidget({
|
|
8159
|
+
controlHeight: CONTROL_HEIGHT,
|
|
8160
|
+
columnId,
|
|
8161
|
+
root: root2
|
|
8162
|
+
});
|
|
8163
|
+
if (settingsWidget) {
|
|
8164
|
+
aestheticSettingsContainer.appendChild(settingsWidget);
|
|
8165
|
+
}
|
|
7900
8166
|
}
|
|
7901
8167
|
function openTab(tabId) {
|
|
7902
8168
|
const tabDef = tabs.find((t) => t.id === tabId);
|
|
@@ -7984,7 +8250,7 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
7984
8250
|
);
|
|
7985
8251
|
break;
|
|
7986
8252
|
case "export":
|
|
7987
|
-
populateExportControls(controlsContainer, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, CONTROL_HEIGHT);
|
|
8253
|
+
populateExportControls(controlsContainer, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, CONTROL_HEIGHT, root2);
|
|
7988
8254
|
break;
|
|
7989
8255
|
}
|
|
7990
8256
|
}
|
|
@@ -8012,14 +8278,13 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
8012
8278
|
toolbarDiv.appendChild(toggleContainer);
|
|
8013
8279
|
toolbarDiv.appendChild(collapsiblePanel);
|
|
8014
8280
|
updateTabStates();
|
|
8015
|
-
openTab(tabs[0].id);
|
|
8016
8281
|
resetSelectedMetadata();
|
|
8017
8282
|
return refreshCurrentTab;
|
|
8018
8283
|
}
|
|
8019
8284
|
function populateDataControls(container, treeDataInstances, getCurrentTreeState, switchToTree, addNewTree, getCurrentMetadataNames, getSelectedMetadata, setSelectedMetadata, resetSelectedMetadata, refreshCurrentTab, options, controlHeight) {
|
|
8020
8285
|
container.innerHTML = "";
|
|
8021
8286
|
const treeGroup = createControlGroup();
|
|
8022
|
-
const treeLabel = createLabel("
|
|
8287
|
+
const treeLabel = createLabel("Tree:", controlHeight);
|
|
8023
8288
|
treeGroup.appendChild(treeLabel);
|
|
8024
8289
|
const treeSelect = document.createElement("select");
|
|
8025
8290
|
treeSelect.className = "ht-select";
|
|
@@ -8051,24 +8316,19 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8051
8316
|
container.appendChild(treeGroup);
|
|
8052
8317
|
const treeFileInput = document.createElement("input");
|
|
8053
8318
|
treeFileInput.type = "file";
|
|
8054
|
-
treeFileInput.accept = ".nwk,.newick,.tree,.tre,.treefile";
|
|
8055
8319
|
treeFileInput.style.display = "none";
|
|
8056
8320
|
treeFileInput.addEventListener("change", async (e) => {
|
|
8057
8321
|
const file = e.target.files[0];
|
|
8058
8322
|
if (!file) return;
|
|
8059
8323
|
try {
|
|
8060
|
-
const
|
|
8061
|
-
let treeName = file.name.replace(/\.
|
|
8062
|
-
|
|
8063
|
-
let counter = 1;
|
|
8064
|
-
while (treeDataInstances.has(uniqueName)) {
|
|
8065
|
-
uniqueName = `${treeName} (${counter})`;
|
|
8066
|
-
counter++;
|
|
8067
|
-
}
|
|
8068
|
-
addNewTree(uniqueName, newickStr);
|
|
8324
|
+
const treeString = await file.text();
|
|
8325
|
+
let treeName = file.name.replace(/\.[^/.]+$/, "");
|
|
8326
|
+
const addedNames = addNewTree(treeName, treeString);
|
|
8069
8327
|
treeFileInput.value = "";
|
|
8070
8328
|
refreshCurrentTab();
|
|
8071
|
-
|
|
8329
|
+
if (addedNames.length > 0) {
|
|
8330
|
+
switchToTree(addedNames[0]);
|
|
8331
|
+
}
|
|
8072
8332
|
} catch (error) {
|
|
8073
8333
|
console.error("Error loading tree file:", error);
|
|
8074
8334
|
alert(`Error loading tree file: ${error.message}`);
|
|
@@ -8084,7 +8344,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8084
8344
|
return;
|
|
8085
8345
|
}
|
|
8086
8346
|
const metadataGroup = createControlGroup();
|
|
8087
|
-
const metadataLabel = createLabel("
|
|
8347
|
+
const metadataLabel = createLabel("Metadata:", controlHeight);
|
|
8088
8348
|
metadataGroup.appendChild(metadataLabel);
|
|
8089
8349
|
const metadataSelect = document.createElement("select");
|
|
8090
8350
|
metadataSelect.className = "ht-select";
|
|
@@ -8169,7 +8429,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8169
8429
|
const validIdColumns = treeData.getValidIdColumns(selectedTableId);
|
|
8170
8430
|
const currentIdColumn = treeData.getNodeIdColumn(selectedTableId);
|
|
8171
8431
|
const nodeIdGroup = createControlGroup();
|
|
8172
|
-
const nodeIdLabel = createLabel("
|
|
8432
|
+
const nodeIdLabel = createLabel("ID Column:", controlHeight);
|
|
8173
8433
|
nodeIdGroup.appendChild(nodeIdLabel);
|
|
8174
8434
|
const nodeIdSelect = document.createElement("select");
|
|
8175
8435
|
nodeIdSelect.className = "ht-select";
|
|
@@ -8184,7 +8444,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8184
8444
|
validIdColumns.forEach((columnName) => {
|
|
8185
8445
|
const option = document.createElement("option");
|
|
8186
8446
|
option.value = columnName;
|
|
8187
|
-
option.textContent = treeData.
|
|
8447
|
+
option.textContent = treeData.columnName.get(columnName);
|
|
8188
8448
|
if (columnName === currentIdColumn) {
|
|
8189
8449
|
option.selected = true;
|
|
8190
8450
|
}
|
|
@@ -8345,7 +8605,7 @@ function populateTreeManipulationControls(container, getCurrentTreeState, refres
|
|
|
8345
8605
|
setShowHiddenBtn(showHiddenBtn);
|
|
8346
8606
|
container.appendChild(showHiddenBtn);
|
|
8347
8607
|
const branchLengthGroup = createControlGroup();
|
|
8348
|
-
const branchLengthLabel = createLabel("
|
|
8608
|
+
const branchLengthLabel = createLabel("Width", controlHeight);
|
|
8349
8609
|
branchLengthGroup.appendChild(branchLengthLabel);
|
|
8350
8610
|
const scaleToSlider = (scale, max = 10) => {
|
|
8351
8611
|
const logMin = Math.log10(1 / max);
|
|
@@ -8368,7 +8628,7 @@ function populateTreeManipulationControls(container, getCurrentTreeState, refres
|
|
|
8368
8628
|
branchLengthGroup.appendChild(branchLengthSlider);
|
|
8369
8629
|
container.appendChild(branchLengthGroup);
|
|
8370
8630
|
const treeHeightGroup = createControlGroup();
|
|
8371
|
-
const treeHeightLabel = createLabel("
|
|
8631
|
+
const treeHeightLabel = createLabel("Height", controlHeight);
|
|
8372
8632
|
treeHeightGroup.appendChild(treeHeightLabel);
|
|
8373
8633
|
const treeHeightSlider = createSlider(0, 100, scaleToSlider(treeState.state.treeHeightScale), 0.1, controlHeight);
|
|
8374
8634
|
treeHeightSlider.addEventListener("input", (e) => {
|
|
@@ -8379,7 +8639,7 @@ function populateTreeManipulationControls(container, getCurrentTreeState, refres
|
|
|
8379
8639
|
treeHeightGroup.appendChild(treeHeightSlider);
|
|
8380
8640
|
container.appendChild(treeHeightGroup);
|
|
8381
8641
|
const radialLayoutGroup = createControlGroup();
|
|
8382
|
-
const radialLayoutLabel = createLabel("Radial
|
|
8642
|
+
const radialLayoutLabel = createLabel("Radial:", controlHeight);
|
|
8383
8643
|
radialLayoutGroup.appendChild(radialLayoutLabel);
|
|
8384
8644
|
const isCircular = treeState.state.layout === "circular";
|
|
8385
8645
|
const radialLayoutToggle = createToggle(isCircular, controlHeight);
|
|
@@ -8506,101 +8766,6 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
|
|
|
8506
8766
|
tipLabelFontGroup.appendChild(tipLabelFontSelect);
|
|
8507
8767
|
container.appendChild(tipLabelFontGroup);
|
|
8508
8768
|
}
|
|
8509
|
-
function populateTipLabelColorSettings(container, treeState, controlHeight) {
|
|
8510
|
-
const columnId = treeState.state.aesthetics.tipLabelColor;
|
|
8511
|
-
if (!columnId) {
|
|
8512
|
-
const message = document.createElement("div");
|
|
8513
|
-
message.textContent = "Select a metadata column for tip label color to edit its settings";
|
|
8514
|
-
message.style.padding = "10px";
|
|
8515
|
-
message.style.color = "#666";
|
|
8516
|
-
container.appendChild(message);
|
|
8517
|
-
return;
|
|
8518
|
-
}
|
|
8519
|
-
const aesthetic = treeState.aestheticsScales.tipLabelColor;
|
|
8520
|
-
if (!aesthetic) {
|
|
8521
|
-
const message = document.createElement("div");
|
|
8522
|
-
message.textContent = "Error: Could not find color aesthetic";
|
|
8523
|
-
message.style.padding = "10px";
|
|
8524
|
-
message.style.color = "#d00";
|
|
8525
|
-
container.appendChild(message);
|
|
8526
|
-
return;
|
|
8527
|
-
}
|
|
8528
|
-
const settingsWidget = aesthetic.createSettingsWidget({
|
|
8529
|
-
controlHeight
|
|
8530
|
-
});
|
|
8531
|
-
if (settingsWidget) {
|
|
8532
|
-
container.appendChild(settingsWidget);
|
|
8533
|
-
} else {
|
|
8534
|
-
const message = document.createElement("div");
|
|
8535
|
-
message.textContent = "No settings available for this aesthetic";
|
|
8536
|
-
message.style.padding = "10px";
|
|
8537
|
-
message.style.color = "#666";
|
|
8538
|
-
container.appendChild(message);
|
|
8539
|
-
return;
|
|
8540
|
-
}
|
|
8541
|
-
if (aesthetic.state.isCategorical) {
|
|
8542
|
-
const maxCategoriesGroup = createControlGroup();
|
|
8543
|
-
const maxCategoriesLabel = createLabel("Max colors:", controlHeight);
|
|
8544
|
-
maxCategoriesGroup.appendChild(maxCategoriesLabel);
|
|
8545
|
-
const maxCategoriesInput = createNumberInput(
|
|
8546
|
-
aesthetic.state.maxCategories || 7,
|
|
8547
|
-
1,
|
|
8548
|
-
100,
|
|
8549
|
-
1,
|
|
8550
|
-
controlHeight
|
|
8551
|
-
);
|
|
8552
|
-
maxCategoriesInput.addEventListener("input", (e) => {
|
|
8553
|
-
const value = parseInt(e.target.value);
|
|
8554
|
-
if (isNaN(value) || value < 1) return;
|
|
8555
|
-
aesthetic.updateState({ maxCategories: value });
|
|
8556
|
-
aesthetic.updateScale(aesthetic.values);
|
|
8557
|
-
treeState.state.treeData.tree.each((node) => {
|
|
8558
|
-
const columnValue = node[columnId];
|
|
8559
|
-
if (columnValue !== void 0 && columnValue !== null) {
|
|
8560
|
-
node.tipLabelColor = aesthetic.getValue(columnValue);
|
|
8561
|
-
}
|
|
8562
|
-
});
|
|
8563
|
-
treeState.updateCoordinates();
|
|
8564
|
-
treeState.notify("legendsChange");
|
|
8565
|
-
});
|
|
8566
|
-
maxCategoriesGroup.appendChild(maxCategoriesInput);
|
|
8567
|
-
container.appendChild(maxCategoriesGroup);
|
|
8568
|
-
}
|
|
8569
|
-
const titleGroup = createControlGroup();
|
|
8570
|
-
const titleLabel = createLabel("Legend title:", controlHeight);
|
|
8571
|
-
titleGroup.appendChild(titleLabel);
|
|
8572
|
-
const titleInput = document.createElement("input");
|
|
8573
|
-
titleInput.type = "text";
|
|
8574
|
-
titleInput.className = "ht-text-input";
|
|
8575
|
-
titleInput.style.height = `${controlHeight}px`;
|
|
8576
|
-
titleInput.style.flex = "1";
|
|
8577
|
-
titleInput.value = aesthetic.state.title || "";
|
|
8578
|
-
titleInput.placeholder = "Enter legend title";
|
|
8579
|
-
titleInput.addEventListener("input", (e) => {
|
|
8580
|
-
aesthetic.updateState({ title: e.target.value });
|
|
8581
|
-
treeState.notify("legendsChange");
|
|
8582
|
-
});
|
|
8583
|
-
titleGroup.appendChild(titleInput);
|
|
8584
|
-
container.appendChild(titleGroup);
|
|
8585
|
-
if (!aesthetic.state.isCategorical) {
|
|
8586
|
-
const unitsGroup = createControlGroup();
|
|
8587
|
-
const unitsLabel = createLabel("Units label:", controlHeight);
|
|
8588
|
-
unitsGroup.appendChild(unitsLabel);
|
|
8589
|
-
const unitsInput = document.createElement("input");
|
|
8590
|
-
unitsInput.type = "text";
|
|
8591
|
-
unitsInput.className = "ht-text-input";
|
|
8592
|
-
unitsInput.style.height = `${controlHeight}px`;
|
|
8593
|
-
unitsInput.style.flex = "1";
|
|
8594
|
-
unitsInput.value = aesthetic.state.inputUnits || "";
|
|
8595
|
-
unitsInput.placeholder = "Enter units (e.g., °C, km)";
|
|
8596
|
-
unitsInput.addEventListener("input", (e) => {
|
|
8597
|
-
aesthetic.updateState({ inputUnits: e.target.value });
|
|
8598
|
-
treeState.notify("legendsChange");
|
|
8599
|
-
});
|
|
8600
|
-
unitsGroup.appendChild(unitsInput);
|
|
8601
|
-
container.appendChild(unitsGroup);
|
|
8602
|
-
}
|
|
8603
|
-
}
|
|
8604
8769
|
function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null, getCurrentAestheticSettings = null, populateAestheticSettings = null) {
|
|
8605
8770
|
const select2 = document.createElement("select");
|
|
8606
8771
|
select2.className = "ht-select";
|
|
@@ -8664,7 +8829,7 @@ function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlH
|
|
|
8664
8829
|
});
|
|
8665
8830
|
return select2;
|
|
8666
8831
|
}
|
|
8667
|
-
function populateExportControls(container, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, controlHeight) {
|
|
8832
|
+
function populateExportControls(container, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, controlHeight, root2 = document) {
|
|
8668
8833
|
container.innerHTML = "";
|
|
8669
8834
|
const treeState = getCurrentTreeState();
|
|
8670
8835
|
const treeView = getCurrentTreeView();
|
|
@@ -8722,11 +8887,11 @@ function populateExportControls(container, getCurrentTreeState, getCurrentTreeVi
|
|
|
8722
8887
|
const sanitizedName = treeName.replace(/[^a-z0-9_-]/gi, "_");
|
|
8723
8888
|
const extension = exportState.format === "svg" ? "svg" : "png";
|
|
8724
8889
|
const filename = `${sanitizedName}.${extension}`;
|
|
8725
|
-
exportTree(treeView, exportState, filename);
|
|
8890
|
+
exportTree(treeView, exportState, filename, root2);
|
|
8726
8891
|
});
|
|
8727
8892
|
container.appendChild(exportBtn);
|
|
8728
8893
|
const formatGroup = createControlGroup();
|
|
8729
|
-
const formatLabel = createLabel("
|
|
8894
|
+
const formatLabel = createLabel("Format:", controlHeight);
|
|
8730
8895
|
formatGroup.appendChild(formatLabel);
|
|
8731
8896
|
const formatSelect = document.createElement("select");
|
|
8732
8897
|
formatSelect.className = "ht-select";
|
|
@@ -8812,30 +8977,32 @@ function populateExportControls(container, getCurrentTreeState, getCurrentTreeVi
|
|
|
8812
8977
|
marginGroup.appendChild(marginInput);
|
|
8813
8978
|
container.appendChild(marginGroup);
|
|
8814
8979
|
}
|
|
8815
|
-
function heatTree(
|
|
8980
|
+
function heatTree(containerOrSelector, treesInput = [], options = {}) {
|
|
8816
8981
|
if (treesInput && !Array.isArray(treesInput)) {
|
|
8817
8982
|
treesInput = [treesInput];
|
|
8818
8983
|
}
|
|
8819
8984
|
if (treesInput === void 0 || treesInput === null) {
|
|
8820
8985
|
treesInput = [];
|
|
8821
8986
|
}
|
|
8822
|
-
injectStyles();
|
|
8823
8987
|
options = {
|
|
8824
8988
|
buttonSize: 25,
|
|
8825
8989
|
transitionDuration: 500,
|
|
8826
8990
|
manualZoomAndPanEnabled: true,
|
|
8827
8991
|
autoZoom: "Default",
|
|
8828
8992
|
autoPan: "Default",
|
|
8993
|
+
isolation: "shadow",
|
|
8829
8994
|
...options
|
|
8830
8995
|
};
|
|
8996
|
+
const isolation = options.isolation;
|
|
8997
|
+
let root2;
|
|
8831
8998
|
const textSizeEstimator = new TextSizeEstimator();
|
|
8832
8999
|
const treeDataInstances = /* @__PURE__ */ new Map();
|
|
8833
9000
|
const treeConfigAesthetics = /* @__PURE__ */ new Map();
|
|
8834
9001
|
treesInput.forEach((treeConfig, index) => {
|
|
8835
|
-
if (!treeConfig.
|
|
8836
|
-
throw new Error(`Tree at index ${index} is missing
|
|
9002
|
+
if (!treeConfig.tree) {
|
|
9003
|
+
throw new Error(`Tree at index ${index} is missing tree string`);
|
|
8837
9004
|
}
|
|
8838
|
-
const
|
|
9005
|
+
const sourceName = treeConfig.name || `Tree ${index + 1}`;
|
|
8839
9006
|
let metadataTables = [];
|
|
8840
9007
|
let metadataNames = [];
|
|
8841
9008
|
if (treeConfig.metadata) {
|
|
@@ -8850,31 +9017,55 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8850
9017
|
}
|
|
8851
9018
|
});
|
|
8852
9019
|
}
|
|
8853
|
-
const
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
9020
|
+
const parsedTrees = TreeData.parseTrees(treeConfig.tree, sourceName);
|
|
9021
|
+
parsedTrees.forEach(({ name: parsedName, treeData: parsedTreeData }, treeIndex) => {
|
|
9022
|
+
let uniqueName = parsedName;
|
|
9023
|
+
let counter = 1;
|
|
9024
|
+
while (treeDataInstances.has(uniqueName)) {
|
|
9025
|
+
uniqueName = `${parsedName} (${counter})`;
|
|
9026
|
+
counter++;
|
|
9027
|
+
}
|
|
9028
|
+
const treeData = new TreeData(parsedTreeData, metadataTables, metadataNames);
|
|
9029
|
+
let treeAesthetics;
|
|
9030
|
+
if (treeConfig.aesthetics) {
|
|
9031
|
+
treeAesthetics = Object.fromEntries(
|
|
9032
|
+
Object.entries(treeConfig.aesthetics).map(([aes, col]) => {
|
|
9033
|
+
for (const [assignedColId, originalName] of treeData.columnName.entries()) {
|
|
9034
|
+
if (originalName === col) {
|
|
9035
|
+
return [aes, assignedColId];
|
|
9036
|
+
}
|
|
8861
9037
|
}
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
9038
|
+
return void 0;
|
|
9039
|
+
}).filter((entry) => entry !== void 0)
|
|
9040
|
+
);
|
|
9041
|
+
} else {
|
|
9042
|
+
treeAesthetics = void 0;
|
|
9043
|
+
}
|
|
9044
|
+
treeDataInstances.set(uniqueName, treeData);
|
|
9045
|
+
treeConfigAesthetics.set(uniqueName, treeAesthetics);
|
|
9046
|
+
});
|
|
8871
9047
|
});
|
|
8872
9048
|
const treeStateCache = /* @__PURE__ */ new Map();
|
|
8873
9049
|
const treeViewCache = /* @__PURE__ */ new Map();
|
|
8874
|
-
|
|
8875
|
-
if (
|
|
8876
|
-
|
|
9050
|
+
let container;
|
|
9051
|
+
if (typeof containerOrSelector === "string") {
|
|
9052
|
+
container = document.querySelector(containerOrSelector);
|
|
9053
|
+
if (!container) {
|
|
9054
|
+
throw new Error(`Container element not found: ${containerOrSelector}`);
|
|
9055
|
+
}
|
|
9056
|
+
} else if (containerOrSelector instanceof HTMLElement) {
|
|
9057
|
+
container = containerOrSelector;
|
|
9058
|
+
} else {
|
|
9059
|
+
throw new Error("First argument must be a CSS selector string or an HTMLElement");
|
|
9060
|
+
}
|
|
9061
|
+
let shadowRoot = null;
|
|
9062
|
+
if (isolation === "shadow") {
|
|
9063
|
+
shadowRoot = container.attachShadow({ mode: "open" });
|
|
9064
|
+
root2 = shadowRoot;
|
|
9065
|
+
} else {
|
|
9066
|
+
root2 = document;
|
|
8877
9067
|
}
|
|
9068
|
+
injectStyles(root2);
|
|
8878
9069
|
const widgetDiv = document.createElement("div");
|
|
8879
9070
|
widgetDiv.className = "ht-widget";
|
|
8880
9071
|
const toolbarDiv = document.createElement("div");
|
|
@@ -8882,12 +9073,14 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8882
9073
|
const treeDiv = document.createElement("div");
|
|
8883
9074
|
treeDiv.className = "ht-tree";
|
|
8884
9075
|
const treeSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
8885
|
-
treeSvg.setAttribute("width", "100%");
|
|
8886
|
-
treeSvg.setAttribute("height", "100%");
|
|
8887
9076
|
treeDiv.appendChild(treeSvg);
|
|
8888
9077
|
widgetDiv.appendChild(toolbarDiv);
|
|
8889
9078
|
widgetDiv.appendChild(treeDiv);
|
|
8890
|
-
|
|
9079
|
+
if (shadowRoot) {
|
|
9080
|
+
shadowRoot.appendChild(widgetDiv);
|
|
9081
|
+
} else {
|
|
9082
|
+
container.appendChild(widgetDiv);
|
|
9083
|
+
}
|
|
8891
9084
|
let currentTreeName = null;
|
|
8892
9085
|
let currentTreeState = null;
|
|
8893
9086
|
let currentTreeView = null;
|
|
@@ -8904,16 +9097,21 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8904
9097
|
immediate: true
|
|
8905
9098
|
}
|
|
8906
9099
|
);
|
|
8907
|
-
function addNewTree(treeName,
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
uniqueName =
|
|
8912
|
-
counter
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
9100
|
+
function addNewTree(treeName, treeString, metadataTables = [], metadataNames = []) {
|
|
9101
|
+
const parsedTrees = TreeData.parseTrees(treeString, treeName);
|
|
9102
|
+
const addedNames = [];
|
|
9103
|
+
parsedTrees.forEach(({ name: parsedName, treeData: parsedTreeData }) => {
|
|
9104
|
+
let uniqueName = parsedName;
|
|
9105
|
+
let counter = 1;
|
|
9106
|
+
while (treeDataInstances.has(uniqueName)) {
|
|
9107
|
+
uniqueName = `${parsedName} (${counter})`;
|
|
9108
|
+
counter++;
|
|
9109
|
+
}
|
|
9110
|
+
const treeData = new TreeData(parsedTreeData, metadataTables, metadataNames);
|
|
9111
|
+
treeDataInstances.set(uniqueName, treeData);
|
|
9112
|
+
addedNames.push(uniqueName);
|
|
9113
|
+
});
|
|
9114
|
+
return addedNames;
|
|
8917
9115
|
}
|
|
8918
9116
|
function switchToTree(treeName) {
|
|
8919
9117
|
if (!treeDataInstances.has(treeName)) {
|
|
@@ -8956,7 +9154,8 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8956
9154
|
() => currentTreeView,
|
|
8957
9155
|
switchToTree,
|
|
8958
9156
|
addNewTree,
|
|
8959
|
-
options
|
|
9157
|
+
options,
|
|
9158
|
+
root2
|
|
8960
9159
|
);
|
|
8961
9160
|
if (treeDataInstances.size > 0) {
|
|
8962
9161
|
const firstTreeName = Array.from(treeDataInstances.keys())[0];
|
|
@@ -8971,7 +9170,8 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8971
9170
|
getCurrentTreeName: () => currentTreeName,
|
|
8972
9171
|
switchToTree,
|
|
8973
9172
|
addNewTree,
|
|
8974
|
-
container: widgetDiv
|
|
9173
|
+
container: widgetDiv,
|
|
9174
|
+
shadowRoot
|
|
8975
9175
|
};
|
|
8976
9176
|
}
|
|
8977
9177
|
export {
|