@grunwaldlab/heat-tree 0.2.0 → 0.3.0
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 +450 -275
- 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,7 +2869,7 @@ 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;
|
|
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);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;padding:0 4px;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:150px}.ht-toggle{width:50px;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:80px}.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}.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
2873
|
function injectStyles() {
|
|
2874
2874
|
const styleId = "heat-tree-styles";
|
|
2875
2875
|
if (!document.getElementById(styleId)) {
|
|
@@ -3074,6 +3074,61 @@ class ContainerResizeHandler {
|
|
|
3074
3074
|
clearTimeout(this.timeoutId);
|
|
3075
3075
|
}
|
|
3076
3076
|
}
|
|
3077
|
+
function isNexusFormat(str) {
|
|
3078
|
+
const firstLine = str.trim().split(/\r?\n/)[0].trim();
|
|
3079
|
+
return /^#NEXUS$/i.test(firstLine);
|
|
3080
|
+
}
|
|
3081
|
+
function applyTranslate(node, translateMap) {
|
|
3082
|
+
if (node.name) {
|
|
3083
|
+
const num = parseInt(node.name, 10);
|
|
3084
|
+
if (!isNaN(num) && translateMap.has(num)) {
|
|
3085
|
+
node.name = translateMap.get(num);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (node.children) {
|
|
3089
|
+
node.children.forEach((child) => applyTranslate(child, translateMap));
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function parseTreesBlock(blockContent) {
|
|
3093
|
+
const trees = [];
|
|
3094
|
+
const translateMap = /* @__PURE__ */ new Map();
|
|
3095
|
+
const translateMatch = blockContent.match(/translate\s+([^;]+);/i);
|
|
3096
|
+
if (translateMatch) {
|
|
3097
|
+
const translateContent = translateMatch[1];
|
|
3098
|
+
const pairs = translateContent.split(/,\s*/);
|
|
3099
|
+
pairs.forEach((pair) => {
|
|
3100
|
+
const match2 = pair.trim().match(/^(\d+)\s+(.+)$/);
|
|
3101
|
+
if (match2) {
|
|
3102
|
+
const num = parseInt(match2[1], 10);
|
|
3103
|
+
const name = match2[2].trim();
|
|
3104
|
+
translateMap.set(num, name);
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
const treeRegex = /tree\s*\*?\s*(?:(\S+))?\s*=\s*(?:\[\S+\])?\s*([^;]+);/gi;
|
|
3109
|
+
let match;
|
|
3110
|
+
while ((match = treeRegex.exec(blockContent)) !== null) {
|
|
3111
|
+
let treeName = match[1].replace(/['"]+/g, "") || null;
|
|
3112
|
+
if (treeName.toLowerCase() == "untitled") {
|
|
3113
|
+
treeName = null;
|
|
3114
|
+
}
|
|
3115
|
+
const newickStr = match[2].trim();
|
|
3116
|
+
const treeData = parseNewick(newickStr);
|
|
3117
|
+
applyTranslate(treeData, translateMap);
|
|
3118
|
+
trees.push({ treeName, treeData });
|
|
3119
|
+
}
|
|
3120
|
+
return trees;
|
|
3121
|
+
}
|
|
3122
|
+
function parseNexus(nexusStr) {
|
|
3123
|
+
const trees = [];
|
|
3124
|
+
const treesBlockRegex = /begin\s+trees\s*;([^]*?)end\s*;/gi;
|
|
3125
|
+
let blockMatch;
|
|
3126
|
+
while ((blockMatch = treesBlockRegex.exec(nexusStr)) !== null) {
|
|
3127
|
+
const blockTrees = parseTreesBlock(blockMatch[1]);
|
|
3128
|
+
trees.push(...blockTrees);
|
|
3129
|
+
}
|
|
3130
|
+
return trees;
|
|
3131
|
+
}
|
|
3077
3132
|
function parseNewick(newickStr) {
|
|
3078
3133
|
newickStr = newickStr.trim();
|
|
3079
3134
|
if (newickStr[newickStr.length - 1] === ";") {
|
|
@@ -3135,7 +3190,7 @@ function parseTable(tsvStr, valid_ids, sep = " ") {
|
|
|
3135
3190
|
const metadata = {};
|
|
3136
3191
|
for (let j = 0; j < headers.length; j++) {
|
|
3137
3192
|
const colName = headers[j];
|
|
3138
|
-
const value = values[j];
|
|
3193
|
+
const value = values[j] === "" ? void 0 : values[j];
|
|
3139
3194
|
metadata[colName] = value;
|
|
3140
3195
|
columnValues.get(colName).push(value);
|
|
3141
3196
|
}
|
|
@@ -3144,7 +3199,7 @@ function parseTable(tsvStr, valid_ids, sep = " ") {
|
|
|
3144
3199
|
headers.forEach((col) => {
|
|
3145
3200
|
const values = columnValues.get(col);
|
|
3146
3201
|
const numericValues = values.map((v) => parseFloat(v)).filter((v) => !isNaN(v));
|
|
3147
|
-
const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !==
|
|
3202
|
+
const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !== void 0).length;
|
|
3148
3203
|
columnTypes.set(col, isContinuous ? "continuous" : "categorical");
|
|
3149
3204
|
let matchCount = 0;
|
|
3150
3205
|
for (const value of values) {
|
|
@@ -3427,6 +3482,9 @@ class CategoricalColorScale {
|
|
|
3427
3482
|
};
|
|
3428
3483
|
this.frequencyMap = /* @__PURE__ */ new Map();
|
|
3429
3484
|
for (const category of categoryData) {
|
|
3485
|
+
if (category === null || category === void 0 || category === "") {
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3430
3488
|
if (this.frequencyMap.has(category)) {
|
|
3431
3489
|
this.frequencyMap.set(category, this.frequencyMap.get(category) + 1);
|
|
3432
3490
|
} else {
|
|
@@ -3479,7 +3537,9 @@ class CategoricalColorScale {
|
|
|
3479
3537
|
}
|
|
3480
3538
|
return;
|
|
3481
3539
|
}
|
|
3482
|
-
if (this.categories.length ===
|
|
3540
|
+
if (this.categories.length === 0) {
|
|
3541
|
+
return;
|
|
3542
|
+
} else if (this.categories.length === 1) {
|
|
3483
3543
|
const color2 = this._getColorAtPosition(this.state.transformMin);
|
|
3484
3544
|
this.categoryColorMap.set(this.categories[0], color2);
|
|
3485
3545
|
} else {
|
|
@@ -4362,6 +4422,70 @@ var Picker = (function() {
|
|
|
4362
4422
|
document.documentElement.firstElementChild.appendChild(style);
|
|
4363
4423
|
Picker.StyleElement = style;
|
|
4364
4424
|
}
|
|
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;
|
|
4488
|
+
}
|
|
4365
4489
|
class Aesthetic extends Subscribable {
|
|
4366
4490
|
state;
|
|
4367
4491
|
// Object containing all configuration used to infer the scale
|
|
@@ -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,16 +4627,105 @@ 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
|
|
4507
4637
|
} = options;
|
|
4638
|
+
if (!columnId) {
|
|
4639
|
+
const message = document.createElement("div");
|
|
4640
|
+
message.textContent = "Select a metadata column to edit its settings";
|
|
4641
|
+
message.style.padding = "10px";
|
|
4642
|
+
message.style.color = "#666";
|
|
4643
|
+
return message;
|
|
4644
|
+
}
|
|
4645
|
+
const container = document.createElement("div");
|
|
4646
|
+
container.className = "ht-aesthetic-settings-content";
|
|
4508
4647
|
if (this.state.scaleType === "color") {
|
|
4509
|
-
|
|
4648
|
+
const paletteEditor = this.createColorPaletteEditor(controlHeight);
|
|
4649
|
+
if (paletteEditor) {
|
|
4650
|
+
container.appendChild(paletteEditor);
|
|
4651
|
+
}
|
|
4652
|
+
if (this.state.isCategorical) {
|
|
4653
|
+
const maxCategoriesGroup = createControlGroup();
|
|
4654
|
+
const maxCategoriesLabel = createLabel("Max colors:", controlHeight);
|
|
4655
|
+
maxCategoriesGroup.appendChild(maxCategoriesLabel);
|
|
4656
|
+
const maxCategoriesInput = createNumberInput(
|
|
4657
|
+
this.state.maxCategories || 7,
|
|
4658
|
+
2,
|
|
4659
|
+
100,
|
|
4660
|
+
1,
|
|
4661
|
+
controlHeight
|
|
4662
|
+
);
|
|
4663
|
+
maxCategoriesInput.addEventListener("input", (e) => {
|
|
4664
|
+
const value = parseInt(e.target.value);
|
|
4665
|
+
if (isNaN(value) || value < 1) return;
|
|
4666
|
+
this.updateState({ maxCategories: value });
|
|
4667
|
+
this.updateScale(this.values);
|
|
4668
|
+
if (this.treeState) {
|
|
4669
|
+
this.treeState.state.treeData.tree.each((node) => {
|
|
4670
|
+
const columnValue = node[columnId];
|
|
4671
|
+
if (columnValue !== void 0 && columnValue !== null) {
|
|
4672
|
+
node.tipLabelColor = this.getValue(columnValue);
|
|
4673
|
+
}
|
|
4674
|
+
});
|
|
4675
|
+
this.treeState.updateCoordinates();
|
|
4676
|
+
this.treeState.notify("legendsChange");
|
|
4677
|
+
}
|
|
4678
|
+
});
|
|
4679
|
+
maxCategoriesGroup.appendChild(maxCategoriesInput);
|
|
4680
|
+
container.appendChild(maxCategoriesGroup);
|
|
4681
|
+
}
|
|
4682
|
+
const titleGroup = createControlGroup();
|
|
4683
|
+
const titleLabel = createLabel("Legend title:", controlHeight);
|
|
4684
|
+
titleGroup.appendChild(titleLabel);
|
|
4685
|
+
const titleInput = document.createElement("input");
|
|
4686
|
+
titleInput.type = "text";
|
|
4687
|
+
titleInput.className = "ht-text-input";
|
|
4688
|
+
titleInput.style.height = `${controlHeight}px`;
|
|
4689
|
+
titleInput.style.flex = "1";
|
|
4690
|
+
titleInput.value = this.state.title || "";
|
|
4691
|
+
titleInput.placeholder = "Enter legend title";
|
|
4692
|
+
titleInput.addEventListener("input", (e) => {
|
|
4693
|
+
this.updateState({ title: e.target.value });
|
|
4694
|
+
if (this.treeState) {
|
|
4695
|
+
this.treeState.notify("legendsChange");
|
|
4696
|
+
}
|
|
4697
|
+
});
|
|
4698
|
+
titleGroup.appendChild(titleInput);
|
|
4699
|
+
container.appendChild(titleGroup);
|
|
4700
|
+
if (!this.state.isCategorical) {
|
|
4701
|
+
const unitsGroup = createControlGroup();
|
|
4702
|
+
const unitsLabel = createLabel("Units label:", controlHeight);
|
|
4703
|
+
unitsGroup.appendChild(unitsLabel);
|
|
4704
|
+
const unitsInput = document.createElement("input");
|
|
4705
|
+
unitsInput.type = "text";
|
|
4706
|
+
unitsInput.className = "ht-text-input";
|
|
4707
|
+
unitsInput.style.height = `${controlHeight}px`;
|
|
4708
|
+
unitsInput.style.flex = "1";
|
|
4709
|
+
unitsInput.value = this.state.inputUnits || "";
|
|
4710
|
+
unitsInput.placeholder = "Enter units (e.g., °C, km)";
|
|
4711
|
+
unitsInput.addEventListener("input", (e) => {
|
|
4712
|
+
this.updateState({ inputUnits: e.target.value });
|
|
4713
|
+
if (this.treeState) {
|
|
4714
|
+
this.treeState.notify("legendsChange");
|
|
4715
|
+
}
|
|
4716
|
+
});
|
|
4717
|
+
unitsGroup.appendChild(unitsInput);
|
|
4718
|
+
container.appendChild(unitsGroup);
|
|
4719
|
+
}
|
|
4510
4720
|
}
|
|
4511
|
-
|
|
4721
|
+
if (container.children.length === 0) {
|
|
4722
|
+
const placeholder = document.createElement("div");
|
|
4723
|
+
placeholder.textContent = "No settings available for this aesthetic";
|
|
4724
|
+
placeholder.style.padding = "10px";
|
|
4725
|
+
placeholder.style.color = "#666";
|
|
4726
|
+
container.appendChild(placeholder);
|
|
4727
|
+
}
|
|
4728
|
+
return container;
|
|
4512
4729
|
}
|
|
4513
4730
|
/**
|
|
4514
4731
|
* Create a color palette editor widget
|
|
@@ -4733,27 +4950,18 @@ class Aesthetic extends Subscribable {
|
|
|
4733
4950
|
nullPickerContainer.style.top = `${rect.bottom + 5}px`;
|
|
4734
4951
|
nullPickerContainer.style.display = "block";
|
|
4735
4952
|
});
|
|
4736
|
-
const
|
|
4737
|
-
|
|
4738
|
-
resetContainer.title = "Reset to default missing data color";
|
|
4953
|
+
const missingDataXContainer = document.createElement("div");
|
|
4954
|
+
missingDataXContainer.className = "missing-data-x-container";
|
|
4739
4955
|
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);
|
|
4956
|
+
resetIndicator.className = "missing-data-x-container-triangle";
|
|
4957
|
+
const missingDataX = document.createElement("div");
|
|
4958
|
+
missingDataX.className = "missing-data-x";
|
|
4959
|
+
missingDataX.textContent = "✕";
|
|
4960
|
+
missingDataXContainer.appendChild(resetIndicator);
|
|
4961
|
+
missingDataXContainer.appendChild(missingDataX);
|
|
4754
4962
|
nullColorColumn.appendChild(nullColorSquareContainer);
|
|
4755
4963
|
nullColorColumn.appendChild(nullColorBox);
|
|
4756
|
-
nullColorColumn.appendChild(
|
|
4964
|
+
nullColorColumn.appendChild(missingDataXContainer);
|
|
4757
4965
|
gradientContainer.appendChild(gradientColumn);
|
|
4758
4966
|
gradientContainer.appendChild(nullColorColumn);
|
|
4759
4967
|
const leftButtonsContainer = document.createElement("div");
|
|
@@ -4909,9 +5117,15 @@ class TreeData extends Subscribable {
|
|
|
4909
5117
|
validIdColumns = /* @__PURE__ */ new Map();
|
|
4910
5118
|
// Map of table ID to array of column names that contain valid node IDs
|
|
4911
5119
|
#nextTableId = 0;
|
|
4912
|
-
|
|
5120
|
+
/**
|
|
5121
|
+
* Create TreeData from a parsed tree object
|
|
5122
|
+
* @param {Object} treeDataObj - Parsed tree object from parseNewick or parseNexus
|
|
5123
|
+
* @param {Array} metadataTables - Optional array of metadata table strings
|
|
5124
|
+
* @param {Array} metadataTableNames - Optional array of metadata table names
|
|
5125
|
+
*/
|
|
5126
|
+
constructor(treeDataObj, metadataTables = [], metadataTableNames = []) {
|
|
4913
5127
|
super();
|
|
4914
|
-
this.tree = this.
|
|
5128
|
+
this.tree = this.createHierarchy(treeDataObj);
|
|
4915
5129
|
if (Array.isArray(metadataTables)) {
|
|
4916
5130
|
metadataTables.forEach((tableStr, index) => {
|
|
4917
5131
|
this.addTable(tableStr, metadataTableNames[index]);
|
|
@@ -4919,12 +5133,53 @@ class TreeData extends Subscribable {
|
|
|
4919
5133
|
}
|
|
4920
5134
|
}
|
|
4921
5135
|
/**
|
|
4922
|
-
*
|
|
4923
|
-
* @param {string}
|
|
5136
|
+
* Static factory method to create TreeData from tree string (Newick or NEXUS)
|
|
5137
|
+
* @param {string} treeString - Newick or NEXUS formatted string
|
|
5138
|
+
* @param {Array} metadataTables - Optional array of metadata table strings
|
|
5139
|
+
* @param {Array} metadataTableNames - Optional array of metadata table names
|
|
5140
|
+
* @returns {TreeData} New TreeData instance
|
|
5141
|
+
*/
|
|
5142
|
+
static fromTreeString(treeString, metadataTables = [], metadataTableNames = []) {
|
|
5143
|
+
const trees = TreeData.parseTrees(treeString, "Tree");
|
|
5144
|
+
if (trees.length !== 1) {
|
|
5145
|
+
throw new Error(`Expected exactly one tree, but found ${trees.length}. Use TreeData.parseTrees() for multiple trees.`);
|
|
5146
|
+
}
|
|
5147
|
+
return new TreeData(trees[0].treeData, metadataTables, metadataTableNames);
|
|
5148
|
+
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Parse tree input (Newick or NEXUS) and return array of trees with naming
|
|
5151
|
+
* @param {string} input - Tree input string
|
|
5152
|
+
* @param {string} sourceName - Base name for the tree(s) (e.g., filename or user-provided)
|
|
5153
|
+
* @returns {Array<{name: string, treeData: Object}>} Array of parsed trees with names
|
|
5154
|
+
*/
|
|
5155
|
+
static parseTrees(input, sourceName = "Tree") {
|
|
5156
|
+
let trees;
|
|
5157
|
+
if (isNexusFormat(input)) {
|
|
5158
|
+
trees = parseNexus(input);
|
|
5159
|
+
} else {
|
|
5160
|
+
const treeData = parseNewick(input);
|
|
5161
|
+
trees = [{ treeName: null, treeData }];
|
|
5162
|
+
}
|
|
5163
|
+
const multiple = trees.length > 1;
|
|
5164
|
+
return trees.map((tree, index) => {
|
|
5165
|
+
let name;
|
|
5166
|
+
if (tree.treeName) {
|
|
5167
|
+
name = multiple ? `${sourceName} ${tree.treeName}` : `${sourceName} - ${tree.treeName}`;
|
|
5168
|
+
} else {
|
|
5169
|
+
name = multiple ? `${sourceName} ${index + 1}` : sourceName;
|
|
5170
|
+
}
|
|
5171
|
+
return {
|
|
5172
|
+
name,
|
|
5173
|
+
treeData: tree.treeData
|
|
5174
|
+
};
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* Create a D3 hierarchy from parsed tree data
|
|
5179
|
+
* @param {Object} treeData - Parsed tree object from parseNewick
|
|
4924
5180
|
* @returns {object} D3 hierarchy object
|
|
4925
5181
|
*/
|
|
4926
|
-
|
|
4927
|
-
const treeData = parseNewick(newickStr);
|
|
5182
|
+
createHierarchy(treeData) {
|
|
4928
5183
|
const root2 = hierarchy(treeData, (d) => d.children).sum((d) => d.children ? 0 : 1).each(function(d) {
|
|
4929
5184
|
d.leafCount = d.value;
|
|
4930
5185
|
delete d.value;
|
|
@@ -4938,10 +5193,10 @@ class TreeData extends Subscribable {
|
|
|
4938
5193
|
}
|
|
4939
5194
|
/**
|
|
4940
5195
|
* Parse and set the tree data
|
|
4941
|
-
* @param {
|
|
5196
|
+
* @param {Object} treeDataObj - Parsed tree object
|
|
4942
5197
|
*/
|
|
4943
|
-
setTree(
|
|
4944
|
-
this.tree = this.
|
|
5198
|
+
setTree(treeDataObj) {
|
|
5199
|
+
this.tree = this.createHierarchy(treeDataObj);
|
|
4945
5200
|
this.metadata.keys().forEach((tableId) => this.#attachTable(tableId));
|
|
4946
5201
|
this.notify("treeUpdated", this);
|
|
4947
5202
|
}
|
|
@@ -5170,15 +5425,26 @@ class TreeData extends Subscribable {
|
|
|
5170
5425
|
}
|
|
5171
5426
|
let values = [];
|
|
5172
5427
|
this.tree.each((node) => {
|
|
5173
|
-
if (
|
|
5428
|
+
if (state.subset == "tips" && node.children) {
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
if (node.metadata) {
|
|
5174
5432
|
values.push(node.metadata[columnId]);
|
|
5433
|
+
} else {
|
|
5434
|
+
values.push(void 0);
|
|
5175
5435
|
}
|
|
5176
5436
|
});
|
|
5177
5437
|
if (values.length === 0) {
|
|
5178
5438
|
console.error(`No values found for column ${columnId}`);
|
|
5179
5439
|
}
|
|
5180
5440
|
const displayName = this.columnDisplayName.get(columnId) || columnId;
|
|
5441
|
+
let scaleType = state.scaleType;
|
|
5442
|
+
if (!scaleType) {
|
|
5443
|
+
scaleType = "color";
|
|
5444
|
+
}
|
|
5181
5445
|
const aesthetic = new Aesthetic(values, {
|
|
5446
|
+
scaleType,
|
|
5447
|
+
default: state.default !== void 0 ? state.default : isCategorical ? null : 0,
|
|
5182
5448
|
isCategorical,
|
|
5183
5449
|
inputUnits: displayName,
|
|
5184
5450
|
...state
|
|
@@ -5367,7 +5633,7 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5367
5633
|
}
|
|
5368
5634
|
const minLabelScale = Math.min(...leafData.map((a) => a.labelScale));
|
|
5369
5635
|
const maxBranchX = Math.max(...leafData.map((a) => a.radius));
|
|
5370
|
-
const
|
|
5636
|
+
const meanBranchX = leafData.reduce((sum, x) => sum + x.radius, 0) / leafData.length;
|
|
5371
5637
|
const nonZeroBranches = root2.descendants().filter((a) => a.data.length > 0 && a.children);
|
|
5372
5638
|
const minBranchLength = nonZeroBranches.length > 0 ? Math.min(...nonZeroBranches.map((a) => a.data.length)) : Infinity;
|
|
5373
5639
|
applyLabelMin(options.minFontPx / minLabelScale);
|
|
@@ -5378,6 +5644,15 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5378
5644
|
))
|
|
5379
5645
|
);
|
|
5380
5646
|
}
|
|
5647
|
+
const totalAnnotationHeight = leafData.reduce((sum, a) => sum + a.height, 0);
|
|
5648
|
+
if (totalAnnotationHeight > 0 && meanBranchX > 0) {
|
|
5649
|
+
if (branchLenToPxFactor_min !== branchLenToPxFactor_max) {
|
|
5650
|
+
applyBranchMin(totalAnnotationHeight * labelSizeToPxFactor_min / (meanBranchX * 2 * Math.PI));
|
|
5651
|
+
}
|
|
5652
|
+
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5653
|
+
applyLabelMax(maxBranchX * branchLenToPxFactor_max * 2 * Math.PI / totalAnnotationHeight);
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5381
5656
|
function applyBranchViewConstraint() {
|
|
5382
5657
|
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
5658
|
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 +5675,6 @@ function calculateCircularScalingFactors(root2, options) {
|
|
|
5400
5675
|
}
|
|
5401
5676
|
applyBranchViewConstraint();
|
|
5402
5677
|
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
5678
|
if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
|
|
5416
5679
|
applyLabelMin(options.idealFontPx / minLabelScale);
|
|
5417
5680
|
}
|
|
@@ -5511,7 +5774,8 @@ class TreeState extends Subscribable {
|
|
|
5511
5774
|
default: "",
|
|
5512
5775
|
nullValue: "",
|
|
5513
5776
|
downstream: ["updateTipLabelText", "updateCoordinates"],
|
|
5514
|
-
hasLegend: false
|
|
5777
|
+
hasLegend: false,
|
|
5778
|
+
subset: "tips"
|
|
5515
5779
|
},
|
|
5516
5780
|
tipLabelColor: {
|
|
5517
5781
|
title: "Tip label color",
|
|
@@ -5520,7 +5784,8 @@ class TreeState extends Subscribable {
|
|
|
5520
5784
|
nullValue: "#808080",
|
|
5521
5785
|
otherCategory: "#555555",
|
|
5522
5786
|
downstream: [],
|
|
5523
|
-
hasLegend: true
|
|
5787
|
+
hasLegend: true,
|
|
5788
|
+
subset: "tips"
|
|
5524
5789
|
},
|
|
5525
5790
|
tipLabelSize: {
|
|
5526
5791
|
title: "Tip label size",
|
|
@@ -5530,7 +5795,8 @@ class TreeState extends Subscribable {
|
|
|
5530
5795
|
isCategorical: false,
|
|
5531
5796
|
outputRange: [0.5, 2],
|
|
5532
5797
|
downstream: ["updateCoordinates"],
|
|
5533
|
-
hasLegend: true
|
|
5798
|
+
hasLegend: true,
|
|
5799
|
+
subset: "tips"
|
|
5534
5800
|
},
|
|
5535
5801
|
tipLabelFont: {
|
|
5536
5802
|
title: "Tip label font",
|
|
@@ -5538,7 +5804,8 @@ class TreeState extends Subscribable {
|
|
|
5538
5804
|
default: "sans-serif",
|
|
5539
5805
|
nullValue: "sans-serif",
|
|
5540
5806
|
downstream: ["updateCoordinates"],
|
|
5541
|
-
hasLegend: false
|
|
5807
|
+
hasLegend: false,
|
|
5808
|
+
subset: "tips"
|
|
5542
5809
|
},
|
|
5543
5810
|
tipLabelStyle: {
|
|
5544
5811
|
title: "Tip label font style",
|
|
@@ -5548,7 +5815,8 @@ class TreeState extends Subscribable {
|
|
|
5548
5815
|
nullValue: "normal",
|
|
5549
5816
|
otherCategory: "italic",
|
|
5550
5817
|
downstream: ["updateCoordinates"],
|
|
5551
|
-
hasLegend: false
|
|
5818
|
+
hasLegend: false,
|
|
5819
|
+
subset: "tips"
|
|
5552
5820
|
},
|
|
5553
5821
|
nodeLabelText: {
|
|
5554
5822
|
title: "Node label text",
|
|
@@ -5556,7 +5824,8 @@ class TreeState extends Subscribable {
|
|
|
5556
5824
|
default: "",
|
|
5557
5825
|
nullValue: "",
|
|
5558
5826
|
downstream: ["updateNodeLabelText"],
|
|
5559
|
-
hasLegend: false
|
|
5827
|
+
hasLegend: false,
|
|
5828
|
+
subset: "all"
|
|
5560
5829
|
},
|
|
5561
5830
|
nodeLabelSize: {
|
|
5562
5831
|
title: "Node label size",
|
|
@@ -5566,7 +5835,8 @@ class TreeState extends Subscribable {
|
|
|
5566
5835
|
isCategorical: false,
|
|
5567
5836
|
outputRange: [0.5, 2],
|
|
5568
5837
|
downstream: ["updateCoordinates"],
|
|
5569
|
-
hasLegend: false
|
|
5838
|
+
hasLegend: false,
|
|
5839
|
+
subset: "all"
|
|
5570
5840
|
}
|
|
5571
5841
|
};
|
|
5572
5842
|
state = {
|
|
@@ -6396,9 +6666,11 @@ class TextColorLegend extends LegendBase {
|
|
|
6396
6666
|
let currentX = 0;
|
|
6397
6667
|
let currentY = titleHeightOffset + this.verticalSpacing + this.squareSize / 2;
|
|
6398
6668
|
let rowHeight = this.squareSize;
|
|
6399
|
-
categories.slice(0, aesthetic.state.maxCategories)
|
|
6669
|
+
const categoriesToShow = categories.slice(0, aesthetic.state.maxCategories);
|
|
6670
|
+
categoriesToShow.forEach((category, i) => {
|
|
6400
6671
|
const color2 = aesthetic.scale.getValue(category);
|
|
6401
|
-
const
|
|
6672
|
+
const labelText = category === "" ? "No data" : category;
|
|
6673
|
+
const labelSize = this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6402
6674
|
const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
|
|
6403
6675
|
if (currentX > 0 && currentX + itemWidth > maxWidth) {
|
|
6404
6676
|
currentX = 0;
|
|
@@ -6409,7 +6681,7 @@ class TextColorLegend extends LegendBase {
|
|
|
6409
6681
|
x: currentX,
|
|
6410
6682
|
y: currentY,
|
|
6411
6683
|
color: color2,
|
|
6412
|
-
label: i < aesthetic.state.maxCategories - 1 ?
|
|
6684
|
+
label: i < aesthetic.state.maxCategories - 1 || i == categories.length - 1 ? labelText : aesthetic.state.otherLabel,
|
|
6413
6685
|
squareX: currentX,
|
|
6414
6686
|
squareY: currentY - this.squareSize / 2,
|
|
6415
6687
|
labelX: currentX + this.squareSize + this.itemLabelGap,
|
|
@@ -6418,6 +6690,30 @@ class TextColorLegend extends LegendBase {
|
|
|
6418
6690
|
currentX += itemWidth + this.itemGap;
|
|
6419
6691
|
this.coordinates.width = Math.max(this.coordinates.width, currentX - this.itemGap);
|
|
6420
6692
|
});
|
|
6693
|
+
const hasNullValues = aesthetic.values.some((x) => x == void 0);
|
|
6694
|
+
if (aesthetic.state.showNullInLegend && hasNullValues) {
|
|
6695
|
+
const color2 = aesthetic.state.nullValue;
|
|
6696
|
+
const labelText = "No Data";
|
|
6697
|
+
const labelSize = this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6698
|
+
const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
|
|
6699
|
+
if (currentX > 0 && currentX + itemWidth > maxWidth) {
|
|
6700
|
+
currentX = 0;
|
|
6701
|
+
currentY += rowHeight + this.verticalSpacing;
|
|
6702
|
+
rowHeight = this.squareSize;
|
|
6703
|
+
}
|
|
6704
|
+
this.coordinates.items.push({
|
|
6705
|
+
x: currentX,
|
|
6706
|
+
y: currentY,
|
|
6707
|
+
color: color2,
|
|
6708
|
+
label: labelText,
|
|
6709
|
+
squareX: currentX,
|
|
6710
|
+
squareY: currentY - this.squareSize / 2,
|
|
6711
|
+
labelX: currentX + this.squareSize + this.itemLabelGap,
|
|
6712
|
+
labelY: currentY
|
|
6713
|
+
});
|
|
6714
|
+
currentX += itemWidth + this.itemGap;
|
|
6715
|
+
this.coordinates.width = Math.max(this.coordinates.width, currentX - this.itemGap);
|
|
6716
|
+
}
|
|
6421
6717
|
this.coordinates.height = currentY + this.squareSize / 2;
|
|
6422
6718
|
}
|
|
6423
6719
|
/**
|
|
@@ -6441,7 +6737,7 @@ class TextColorLegend extends LegendBase {
|
|
|
6441
6737
|
const ticksY = gradientY + this.gradientHeight;
|
|
6442
6738
|
const labelsY = ticksY + this.tickHeight;
|
|
6443
6739
|
const unitsSize = this.textSizeEstimator.getTextSize(aesthetic.state.inputUnits || "", this.state.labelFontSize);
|
|
6444
|
-
|
|
6740
|
+
let height = labelsY + this.state.labelFontSize + unitsSize.heightPx;
|
|
6445
6741
|
this.coordinates = {
|
|
6446
6742
|
width,
|
|
6447
6743
|
height,
|
|
@@ -6464,7 +6760,8 @@ class TextColorLegend extends LegendBase {
|
|
|
6464
6760
|
x: width / 2,
|
|
6465
6761
|
y: height,
|
|
6466
6762
|
text: aesthetic.state.inputUnits || ""
|
|
6467
|
-
}
|
|
6763
|
+
},
|
|
6764
|
+
nullItem: null
|
|
6468
6765
|
};
|
|
6469
6766
|
if (minValue === maxValue) {
|
|
6470
6767
|
const x = leftOverhang + baseWidth / 2;
|
|
@@ -6495,6 +6792,23 @@ class TextColorLegend extends LegendBase {
|
|
|
6495
6792
|
});
|
|
6496
6793
|
});
|
|
6497
6794
|
}
|
|
6795
|
+
if (aesthetic.state.showNullInLegend) {
|
|
6796
|
+
const color2 = aesthetic.state.nullValue;
|
|
6797
|
+
const labelText = "No Data";
|
|
6798
|
+
this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
|
|
6799
|
+
const nullItemY = height + this.verticalSpacing;
|
|
6800
|
+
this.coordinates.nullItem = {
|
|
6801
|
+
x: leftOverhang,
|
|
6802
|
+
y: nullItemY + this.squareSize / 2,
|
|
6803
|
+
color: color2,
|
|
6804
|
+
label: labelText,
|
|
6805
|
+
squareX: leftOverhang,
|
|
6806
|
+
squareY: nullItemY,
|
|
6807
|
+
labelX: leftOverhang + this.squareSize + this.itemLabelGap,
|
|
6808
|
+
labelY: nullItemY + this.squareSize / 2
|
|
6809
|
+
};
|
|
6810
|
+
this.coordinates.height = nullItemY + this.squareSize;
|
|
6811
|
+
}
|
|
6498
6812
|
}
|
|
6499
6813
|
/**
|
|
6500
6814
|
* Render the legend in the specified SVG element
|
|
@@ -7646,72 +7960,8 @@ function convertSvgToPng(svgString, width, height, filename) {
|
|
|
7646
7960
|
};
|
|
7647
7961
|
img.src = url;
|
|
7648
7962
|
}
|
|
7649
|
-
function createControlGroup() {
|
|
7650
|
-
const group = document.createElement("div");
|
|
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
7963
|
function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCurrentTreeView, switchToTree, addNewTree, options) {
|
|
7714
|
-
const CONTROL_HEIGHT =
|
|
7964
|
+
const CONTROL_HEIGHT = 20;
|
|
7715
7965
|
let currentTab = null;
|
|
7716
7966
|
let selectedMetadata = null;
|
|
7717
7967
|
let currentAestheticSettings = null;
|
|
@@ -7795,12 +8045,15 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
7795
8045
|
}
|
|
7796
8046
|
}
|
|
7797
8047
|
}
|
|
7798
|
-
toggleButton.addEventListener("click", () => {
|
|
8048
|
+
toggleButton.addEventListener("click", (e) => {
|
|
8049
|
+
e.preventDefault();
|
|
8050
|
+
e.stopPropagation();
|
|
7799
8051
|
controlPanelVisible = !controlPanelVisible;
|
|
7800
8052
|
if (controlPanelVisible) {
|
|
7801
8053
|
collapsiblePanel.classList.remove("ht-panel-collapsed");
|
|
7802
8054
|
toggleButton.classList.remove("collapsed");
|
|
7803
8055
|
} else {
|
|
8056
|
+
closeAestheticSettings();
|
|
7804
8057
|
collapsiblePanel.classList.add("ht-panel-collapsed");
|
|
7805
8058
|
toggleButton.classList.add("collapsed");
|
|
7806
8059
|
}
|
|
@@ -7889,14 +8142,23 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
7889
8142
|
aestheticSettingsContainer.innerHTML = "";
|
|
7890
8143
|
const treeState = getCurrentTreeState();
|
|
7891
8144
|
if (!treeState) return;
|
|
7892
|
-
|
|
7893
|
-
|
|
8145
|
+
const columnId = treeState.state.aesthetics[aestheticId];
|
|
8146
|
+
const aesthetic = treeState.aestheticsScales[aestheticId];
|
|
8147
|
+
if (!aesthetic) {
|
|
8148
|
+
const message = document.createElement("div");
|
|
8149
|
+
message.textContent = "Error: Could not find aesthetic";
|
|
8150
|
+
message.style.padding = "10px";
|
|
8151
|
+
message.style.color = "#d00";
|
|
8152
|
+
aestheticSettingsContainer.appendChild(message);
|
|
7894
8153
|
return;
|
|
7895
8154
|
}
|
|
7896
|
-
const
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
8155
|
+
const settingsWidget = aesthetic.createSettingsWidget({
|
|
8156
|
+
controlHeight: CONTROL_HEIGHT,
|
|
8157
|
+
columnId
|
|
8158
|
+
});
|
|
8159
|
+
if (settingsWidget) {
|
|
8160
|
+
aestheticSettingsContainer.appendChild(settingsWidget);
|
|
8161
|
+
}
|
|
7900
8162
|
}
|
|
7901
8163
|
function openTab(tabId) {
|
|
7902
8164
|
const tabDef = tabs.find((t) => t.id === tabId);
|
|
@@ -8012,14 +8274,13 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
|
|
|
8012
8274
|
toolbarDiv.appendChild(toggleContainer);
|
|
8013
8275
|
toolbarDiv.appendChild(collapsiblePanel);
|
|
8014
8276
|
updateTabStates();
|
|
8015
|
-
openTab(tabs[0].id);
|
|
8016
8277
|
resetSelectedMetadata();
|
|
8017
8278
|
return refreshCurrentTab;
|
|
8018
8279
|
}
|
|
8019
8280
|
function populateDataControls(container, treeDataInstances, getCurrentTreeState, switchToTree, addNewTree, getCurrentMetadataNames, getSelectedMetadata, setSelectedMetadata, resetSelectedMetadata, refreshCurrentTab, options, controlHeight) {
|
|
8020
8281
|
container.innerHTML = "";
|
|
8021
8282
|
const treeGroup = createControlGroup();
|
|
8022
|
-
const treeLabel = createLabel("
|
|
8283
|
+
const treeLabel = createLabel("Tree:", controlHeight);
|
|
8023
8284
|
treeGroup.appendChild(treeLabel);
|
|
8024
8285
|
const treeSelect = document.createElement("select");
|
|
8025
8286
|
treeSelect.className = "ht-select";
|
|
@@ -8051,24 +8312,19 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8051
8312
|
container.appendChild(treeGroup);
|
|
8052
8313
|
const treeFileInput = document.createElement("input");
|
|
8053
8314
|
treeFileInput.type = "file";
|
|
8054
|
-
treeFileInput.accept = ".nwk,.newick,.tree,.tre,.treefile";
|
|
8055
8315
|
treeFileInput.style.display = "none";
|
|
8056
8316
|
treeFileInput.addEventListener("change", async (e) => {
|
|
8057
8317
|
const file = e.target.files[0];
|
|
8058
8318
|
if (!file) return;
|
|
8059
8319
|
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);
|
|
8320
|
+
const treeString = await file.text();
|
|
8321
|
+
let treeName = file.name.replace(/\.[^/.]+$/, "");
|
|
8322
|
+
const addedNames = addNewTree(treeName, treeString);
|
|
8069
8323
|
treeFileInput.value = "";
|
|
8070
8324
|
refreshCurrentTab();
|
|
8071
|
-
|
|
8325
|
+
if (addedNames.length > 0) {
|
|
8326
|
+
switchToTree(addedNames[0]);
|
|
8327
|
+
}
|
|
8072
8328
|
} catch (error) {
|
|
8073
8329
|
console.error("Error loading tree file:", error);
|
|
8074
8330
|
alert(`Error loading tree file: ${error.message}`);
|
|
@@ -8084,7 +8340,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8084
8340
|
return;
|
|
8085
8341
|
}
|
|
8086
8342
|
const metadataGroup = createControlGroup();
|
|
8087
|
-
const metadataLabel = createLabel("
|
|
8343
|
+
const metadataLabel = createLabel("Metadata:", controlHeight);
|
|
8088
8344
|
metadataGroup.appendChild(metadataLabel);
|
|
8089
8345
|
const metadataSelect = document.createElement("select");
|
|
8090
8346
|
metadataSelect.className = "ht-select";
|
|
@@ -8169,7 +8425,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8169
8425
|
const validIdColumns = treeData.getValidIdColumns(selectedTableId);
|
|
8170
8426
|
const currentIdColumn = treeData.getNodeIdColumn(selectedTableId);
|
|
8171
8427
|
const nodeIdGroup = createControlGroup();
|
|
8172
|
-
const nodeIdLabel = createLabel("
|
|
8428
|
+
const nodeIdLabel = createLabel("ID Column:", controlHeight);
|
|
8173
8429
|
nodeIdGroup.appendChild(nodeIdLabel);
|
|
8174
8430
|
const nodeIdSelect = document.createElement("select");
|
|
8175
8431
|
nodeIdSelect.className = "ht-select";
|
|
@@ -8184,7 +8440,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
|
|
|
8184
8440
|
validIdColumns.forEach((columnName) => {
|
|
8185
8441
|
const option = document.createElement("option");
|
|
8186
8442
|
option.value = columnName;
|
|
8187
|
-
option.textContent = treeData.
|
|
8443
|
+
option.textContent = treeData.columnName.get(columnName);
|
|
8188
8444
|
if (columnName === currentIdColumn) {
|
|
8189
8445
|
option.selected = true;
|
|
8190
8446
|
}
|
|
@@ -8506,101 +8762,6 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
|
|
|
8506
8762
|
tipLabelFontGroup.appendChild(tipLabelFontSelect);
|
|
8507
8763
|
container.appendChild(tipLabelFontGroup);
|
|
8508
8764
|
}
|
|
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
8765
|
function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null, getCurrentAestheticSettings = null, populateAestheticSettings = null) {
|
|
8605
8766
|
const select2 = document.createElement("select");
|
|
8606
8767
|
select2.className = "ht-select";
|
|
@@ -8832,10 +8993,10 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8832
8993
|
const treeDataInstances = /* @__PURE__ */ new Map();
|
|
8833
8994
|
const treeConfigAesthetics = /* @__PURE__ */ new Map();
|
|
8834
8995
|
treesInput.forEach((treeConfig, index) => {
|
|
8835
|
-
if (!treeConfig.
|
|
8836
|
-
throw new Error(`Tree at index ${index} is missing
|
|
8996
|
+
if (!treeConfig.tree) {
|
|
8997
|
+
throw new Error(`Tree at index ${index} is missing tree string`);
|
|
8837
8998
|
}
|
|
8838
|
-
const
|
|
8999
|
+
const sourceName = treeConfig.name || `Tree ${index + 1}`;
|
|
8839
9000
|
let metadataTables = [];
|
|
8840
9001
|
let metadataNames = [];
|
|
8841
9002
|
if (treeConfig.metadata) {
|
|
@@ -8850,24 +9011,33 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8850
9011
|
}
|
|
8851
9012
|
});
|
|
8852
9013
|
}
|
|
8853
|
-
const
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
9014
|
+
const parsedTrees = TreeData.parseTrees(treeConfig.tree, sourceName);
|
|
9015
|
+
parsedTrees.forEach(({ name: parsedName, treeData: parsedTreeData }, treeIndex) => {
|
|
9016
|
+
let uniqueName = parsedName;
|
|
9017
|
+
let counter = 1;
|
|
9018
|
+
while (treeDataInstances.has(uniqueName)) {
|
|
9019
|
+
uniqueName = `${parsedName} (${counter})`;
|
|
9020
|
+
counter++;
|
|
9021
|
+
}
|
|
9022
|
+
const treeData = new TreeData(parsedTreeData, metadataTables, metadataNames);
|
|
9023
|
+
let treeAesthetics;
|
|
9024
|
+
if (treeConfig.aesthetics) {
|
|
9025
|
+
treeAesthetics = Object.fromEntries(
|
|
9026
|
+
Object.entries(treeConfig.aesthetics).map(([aes, col]) => {
|
|
9027
|
+
for (const [assignedColId, originalName] of treeData.columnName.entries()) {
|
|
9028
|
+
if (originalName === col) {
|
|
9029
|
+
return [aes, assignedColId];
|
|
9030
|
+
}
|
|
8861
9031
|
}
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
9032
|
+
return void 0;
|
|
9033
|
+
}).filter((entry) => entry !== void 0)
|
|
9034
|
+
);
|
|
9035
|
+
} else {
|
|
9036
|
+
treeAesthetics = void 0;
|
|
9037
|
+
}
|
|
9038
|
+
treeDataInstances.set(uniqueName, treeData);
|
|
9039
|
+
treeConfigAesthetics.set(uniqueName, treeAesthetics);
|
|
9040
|
+
});
|
|
8871
9041
|
});
|
|
8872
9042
|
const treeStateCache = /* @__PURE__ */ new Map();
|
|
8873
9043
|
const treeViewCache = /* @__PURE__ */ new Map();
|
|
@@ -8904,16 +9074,21 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
|
|
|
8904
9074
|
immediate: true
|
|
8905
9075
|
}
|
|
8906
9076
|
);
|
|
8907
|
-
function addNewTree(treeName,
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
uniqueName =
|
|
8912
|
-
counter
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
9077
|
+
function addNewTree(treeName, treeString, metadataTables = [], metadataNames = []) {
|
|
9078
|
+
const parsedTrees = TreeData.parseTrees(treeString, treeName);
|
|
9079
|
+
const addedNames = [];
|
|
9080
|
+
parsedTrees.forEach(({ name: parsedName, treeData: parsedTreeData }) => {
|
|
9081
|
+
let uniqueName = parsedName;
|
|
9082
|
+
let counter = 1;
|
|
9083
|
+
while (treeDataInstances.has(uniqueName)) {
|
|
9084
|
+
uniqueName = `${parsedName} (${counter})`;
|
|
9085
|
+
counter++;
|
|
9086
|
+
}
|
|
9087
|
+
const treeData = new TreeData(parsedTreeData, metadataTables, metadataNames);
|
|
9088
|
+
treeDataInstances.set(uniqueName, treeData);
|
|
9089
|
+
addedNames.push(uniqueName);
|
|
9090
|
+
});
|
|
9091
|
+
return addedNames;
|
|
8917
9092
|
}
|
|
8918
9093
|
function switchToTree(treeName) {
|
|
8919
9094
|
if (!treeDataInstances.has(treeName)) {
|