@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.
@@ -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;pointer-events:none}.ht-control-panel-toggle{background-color:transparent;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;gap:4px;transition:opacity .2s;pointer-events:auto}.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;transition:max-height .3s ease-in-out}.ht-collapsible-panel.ht-panel-collapsed{max-height:0}.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-tab{cursor:pointer;padding:2px 4px;font-family:sans-serif;font-size:14px;color:#333;border-bottom:2px solid transparent;transition:all .2s}.ht-tab:hover{color:#666}.ht-tab.active{color:#000;font-weight:700;border-bottom-color:#007bff}.ht-tab.active:hover{color:#000}.ht-tab.disabled{color:#999;cursor:not-allowed;opacity:.5}.ht-tab.disabled:hover{color:#999}.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:26px}.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-control-label{font-family:sans-serif;font-size:14px;color:#333;white-space:nowrap;display:flex;align-items:center}.ht-button{font-size:14px;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:14px;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:14px;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:14px;border:1px solid #ccc;border-radius:4px;width:80px}.ht-text-input{font-size:14px;border:1px solid #ccc;border-radius:4px;padding:2px 4px;width:100px}.ht-aesthetic-settings{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;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:24px;height:24px;font-size:14px;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:24px;border:1px solid #ccc;border-radius:4px;position:relative}.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:24px;border:1px solid #ccc;border-radius:4px;position:relative}.ht-null-color-reset-container{position:relative;display:flex;align-items:center;justify-content:center}.ht-null-color-reset-indicator{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}.ht-null-color-reset{font-size:14px;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
- function injectStyles() {
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
- if (!document.getElementById(styleId)) {
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.head.appendChild(styleElement);
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 !== "").length;
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 === 1) {
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
- var style = document.createElement("style");
4361
- style.textContent = '.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, yellow, lime, cyan, blue, magenta, red);box-shadow:0 0 0 1px silver}.picker_sl{position:relative;box-shadow:0 0 0 1px silver;background-image:linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%),linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%),linear-gradient(90deg, #808080, rgba(128, 128, 128, 0))}.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, lightgrey 25%, white 25%, white 75%, lightgrey 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 rgba(0,0,0,.4)}.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) scale(1, -1)}.popup.popup_left{top:0;right:100%}.popup.popup_left .picker_arrow{top:0;right:0;-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.popup.popup_right{top:0;left:100%}.popup.popup_right .picker_arrow{top:0;left:0}';
4362
- document.documentElement.firstElementChild.appendChild(style);
4363
- Picker.StyleElement = style;
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", "#31688e", "#35b779", "#fde724"];
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: null,
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 = 24
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
- return this.createColorPaletteEditor(controlHeight);
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
- return null;
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
- document.body.appendChild(pickerContainer);
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
- document.body.appendChild(nullPickerContainer);
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
- document.addEventListener("click", closePickerOnClickOutside);
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
- document.addEventListener("click", closeNullPickerOnClickOutside);
4851
+ clickTarget.addEventListener("click", closeNullPickerOnClickOutside);
4632
4852
  container.dataset.pickerCleanup = "cleanup";
4633
4853
  container.cleanupFunction = () => {
4634
- document.removeEventListener("click", closePickerOnClickOutside);
4635
- document.removeEventListener("click", closeNullPickerOnClickOutside);
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 resetContainer = document.createElement("div");
4737
- resetContainer.className = "ht-null-color-reset-container";
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 = "ht-null-color-reset-indicator";
4741
- const resetX = document.createElement("div");
4742
- resetX.className = "ht-null-color-reset";
4743
- resetX.textContent = "✕";
4744
- resetX.addEventListener("click", () => {
4745
- const defaultNullColor = "#808080";
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(resetContainer);
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
- constructor(newickStr, metadataTables = [], metadataTableNames = []) {
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.parseTree(newickStr);
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
- * Parse a Newick string and create a hierarchy
4923
- * @param {string} newickStr - Newick formatted 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
- parseTree(newickStr) {
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 {string} newickStr - Newick formatted string or path
5199
+ * @param {Object} treeDataObj - Parsed tree object
4942
5200
  */
4943
- setTree(newickStr) {
4944
- this.tree = this.parseTree(newickStr);
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 (node.metadata && node.metadata[columnId] !== void 0) {
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 minBranchX = Math.min(...leafData.map((a) => a.radius));
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).forEach((category, i) => {
6672
+ const categoriesToShow = categories.slice(0, aesthetic.state.maxCategories);
6673
+ categoriesToShow.forEach((category, i) => {
6400
6674
  const color2 = aesthetic.scale.getValue(category);
6401
- const labelSize = this.textSizeEstimator.getTextSize(category, this.state.labelFontSize);
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 ? category : aesthetic.state.otherLabel,
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
- const height = labelsY + this.state.labelFontSize + unitsSize.heightPx;
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 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
- 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
- if (aestheticId === "tipLabelColor") {
7893
- populateTipLabelColorSettings(aestheticSettingsContainer, treeState, CONTROL_HEIGHT);
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 placeholder = document.createElement("div");
7897
- placeholder.textContent = `Settings for ${aestheticId} (coming soon)`;
7898
- placeholder.style.padding = "10px";
7899
- aestheticSettingsContainer.appendChild(placeholder);
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("Select tree:", controlHeight);
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 newickStr = await file.text();
8061
- let treeName = file.name.replace(/\.(nwk|newick|tree|tre)$/i, "");
8062
- let uniqueName = treeName;
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
- switchToTree(uniqueName);
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("Available metadata:", controlHeight);
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("Node/Tip ID Column:", controlHeight);
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.columnDisplayName.get(columnName);
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("Branch length:", controlHeight);
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("Tree height:", controlHeight);
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 layout:", controlHeight);
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("Output format:", controlHeight);
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(containerSelector, treesInput = [], options = {}) {
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.newick) {
8836
- throw new Error(`Tree at index ${index} is missing newick string`);
9002
+ if (!treeConfig.tree) {
9003
+ throw new Error(`Tree at index ${index} is missing tree string`);
8837
9004
  }
8838
- const treeName = treeConfig.name || `Tree ${index + 1}`;
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 treeData = new TreeData(treeConfig.newick, metadataTables, metadataNames);
8854
- let treeAesthetics;
8855
- if (treeConfig.aesthetics) {
8856
- treeAesthetics = Object.fromEntries(
8857
- Object.entries(treeConfig.aesthetics).map(([aes, col]) => {
8858
- for (const [assignedColId, originalName] of treeData.columnName.entries()) {
8859
- if (originalName === col) {
8860
- return [aes, assignedColId];
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
- return void 0;
8864
- })
8865
- );
8866
- } else {
8867
- treeAesthetics = void 0;
8868
- }
8869
- treeDataInstances.set(treeName, treeData);
8870
- treeConfigAesthetics.set(treeName, treeAesthetics);
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
- const container = document.querySelector(containerSelector);
8875
- if (!container) {
8876
- throw new Error(`Container element not found: ${containerSelector}`);
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
- container.appendChild(widgetDiv);
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, newickStr, metadataTables = [], metadataNames = []) {
8908
- let uniqueName = treeName;
8909
- let counter = 1;
8910
- while (treeDataInstances.has(uniqueName)) {
8911
- uniqueName = `${treeName} (${counter})`;
8912
- counter++;
8913
- }
8914
- const treeData = new TreeData(newickStr, metadataTables, metadataNames);
8915
- treeDataInstances.set(uniqueName, treeData);
8916
- return uniqueName;
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 {