@grunwaldlab/heat-tree 0.1.2 → 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.
@@ -824,7 +824,7 @@ function extend(parent, definition) {
824
824
  for (var key in definition) prototype[key] = definition[key];
825
825
  return prototype;
826
826
  }
827
- function Color() {
827
+ function Color$1() {
828
828
  }
829
829
  var darker = 0.7;
830
830
  var brighter = 1 / darker;
@@ -979,7 +979,7 @@ var named = {
979
979
  yellow: 16776960,
980
980
  yellowgreen: 10145074
981
981
  };
982
- define(Color, color, {
982
+ define(Color$1, color, {
983
983
  copy(channels) {
984
984
  return Object.assign(new this.constructor(), this, channels);
985
985
  },
@@ -1019,7 +1019,7 @@ function rgba(r, g, b, a) {
1019
1019
  return new Rgb(r, g, b, a);
1020
1020
  }
1021
1021
  function rgbConvert(o) {
1022
- if (!(o instanceof Color)) o = color(o);
1022
+ if (!(o instanceof Color$1)) o = color(o);
1023
1023
  if (!o) return new Rgb();
1024
1024
  o = o.rgb();
1025
1025
  return new Rgb(o.r, o.g, o.b, o.opacity);
@@ -1033,7 +1033,7 @@ function Rgb(r, g, b, opacity) {
1033
1033
  this.b = +b;
1034
1034
  this.opacity = +opacity;
1035
1035
  }
1036
- define(Rgb, rgb, extend(Color, {
1036
+ define(Rgb, rgb, extend(Color$1, {
1037
1037
  brighter(k) {
1038
1038
  k = k == null ? brighter : Math.pow(brighter, k);
1039
1039
  return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
@@ -1086,7 +1086,7 @@ function hsla(h, s, l, a) {
1086
1086
  }
1087
1087
  function hslConvert(o) {
1088
1088
  if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);
1089
- if (!(o instanceof Color)) o = color(o);
1089
+ if (!(o instanceof Color$1)) o = color(o);
1090
1090
  if (!o) return new Hsl();
1091
1091
  if (o instanceof Hsl) return o;
1092
1092
  o = o.rgb();
@@ -1111,7 +1111,7 @@ function Hsl(h, s, l, opacity) {
1111
1111
  this.l = +l;
1112
1112
  this.opacity = +opacity;
1113
1113
  }
1114
- define(Hsl, hsl, extend(Color, {
1114
+ define(Hsl, hsl, extend(Color$1, {
1115
1115
  brighter(k) {
1116
1116
  k = k == null ? brighter : Math.pow(brighter, k);
1117
1117
  return new Hsl(this.h, this.s, this.l * k, this.opacity);
@@ -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;z-index:10;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;overflow:hidden;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}.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-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-tree{flex:1 1 auto;min-height:0;position:relative}.ht-tree svg{display:block}";
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] === ";") {
@@ -3118,67 +3173,69 @@ function parseNewick(newickStr) {
3118
3173
  }
3119
3174
  return result;
3120
3175
  }
3121
- function parseTable(tsvStr, sep = " ") {
3176
+ function parseTable(tsvStr, valid_ids, sep = " ") {
3122
3177
  let metadataMap = /* @__PURE__ */ new Map();
3123
- let metadataColumns = [];
3124
3178
  let columnTypes = /* @__PURE__ */ new Map();
3179
+ let validIdCounts = [];
3180
+ let idColumns = [];
3125
3181
  const lines = tsvStr.trim().split("\n");
3126
3182
  if (lines.length == 0) {
3127
- console.error("Empty metatdata table");
3183
+ console.error("Empty metadata table");
3128
3184
  } else {
3129
3185
  const headers = lines[0].split(sep);
3130
- const nodeIdIndex = headers.indexOf("node_id");
3131
- if (nodeIdIndex === -1) {
3132
- console.warn('Metadata table must contain a "node_id" column');
3133
- } else {
3134
- metadataColumns = headers.filter((h, i) => i !== nodeIdIndex);
3135
- const columnValues = /* @__PURE__ */ new Map();
3136
- metadataColumns.forEach((col) => columnValues.set(col, []));
3137
- for (let i = 1; i < lines.length; i++) {
3138
- const values = lines[i].split(sep);
3139
- const nodeId = values[nodeIdIndex];
3140
- const metadata = {};
3141
- for (let j = 0; j < headers.length; j++) {
3142
- if (j !== nodeIdIndex) {
3143
- const colName = headers[j];
3144
- const value = values[j];
3145
- metadata[colName] = value;
3146
- columnValues.get(colName).push(value);
3147
- }
3186
+ const columnValues = /* @__PURE__ */ new Map();
3187
+ headers.forEach((col) => columnValues.set(col, []));
3188
+ for (let i = 1; i < lines.length; i++) {
3189
+ const values = lines[i].split(sep);
3190
+ const metadata = {};
3191
+ for (let j = 0; j < headers.length; j++) {
3192
+ const colName = headers[j];
3193
+ const value = values[j] === "" ? void 0 : values[j];
3194
+ metadata[colName] = value;
3195
+ columnValues.get(colName).push(value);
3196
+ }
3197
+ metadataMap.set(i - 1, metadata);
3198
+ }
3199
+ headers.forEach((col) => {
3200
+ const values = columnValues.get(col);
3201
+ const numericValues = values.map((v) => parseFloat(v)).filter((v) => !isNaN(v));
3202
+ const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !== void 0).length;
3203
+ columnTypes.set(col, isContinuous ? "continuous" : "categorical");
3204
+ let matchCount = 0;
3205
+ for (const value of values) {
3206
+ if (value && valid_ids.has(value)) {
3207
+ matchCount++;
3148
3208
  }
3149
- metadataMap.set(nodeId, metadata);
3150
3209
  }
3151
- metadataColumns.forEach((col) => {
3152
- const values = columnValues.get(col);
3153
- const numericValues = values.map((v) => parseFloat(v)).filter((v) => !isNaN(v));
3154
- const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !== "").length;
3155
- columnTypes.set(col, isContinuous ? "continuous" : "categorical");
3156
- });
3157
- }
3210
+ if (matchCount > 0) {
3211
+ validIdCounts.push({ col, matchCount });
3212
+ }
3213
+ });
3214
+ validIdCounts = validIdCounts.sort((a, b) => b.matchCount - a.matchCount);
3215
+ idColumns = validIdCounts.map((x) => x.col);
3158
3216
  }
3159
- return {
3160
- metadataMap,
3161
- columnTypes
3162
- };
3217
+ return { metadataMap, columnTypes, idColumns };
3163
3218
  }
3164
3219
  class NullScale {
3165
- defaultValue;
3166
- constructor(defaultValue) {
3167
- if (defaultValue === void 0) {
3168
- console.error("A defualt value for a NullScale is needed.");
3169
- } else {
3170
- this.defaultValue = defaultValue;
3220
+ constructor(options = {}) {
3221
+ this.state = { ...options };
3222
+ if (this.state.default === void 0) {
3223
+ console.error("A default value for a NullScale is needed.");
3171
3224
  }
3172
3225
  }
3173
3226
  getValue() {
3174
- return this.defaultValue;
3227
+ return this.state.default;
3175
3228
  }
3176
3229
  }
3177
3230
  class IdentityScale {
3178
- constructor(defaultValue = null, validValues = null, transformFn = null) {
3179
- this.validValues = validValues ? new Set(validValues) : null;
3180
- this.transformFn = transformFn;
3181
- this.defaultValue = defaultValue;
3231
+ constructor(options = {}) {
3232
+ this.state = {
3233
+ default: null,
3234
+ outputValues: null,
3235
+ transformFn: null,
3236
+ ...options
3237
+ };
3238
+ this.validValues = this.state.outputValues ? new Set(this.state.outputValues) : null;
3182
3239
  }
3183
3240
  /**
3184
3241
  * Get the value, optionally transformed and validated
@@ -3186,23 +3243,26 @@ class IdentityScale {
3186
3243
  * @returns {*} The output value, or null if invalid
3187
3244
  */
3188
3245
  getValue(value) {
3189
- let result = this.transformFn ? this.transformFn(value) : value;
3246
+ let result = this.state.transformFn ? this.state.transformFn(value) : value;
3190
3247
  if (this.validValues !== null && !this.validValues.has(result)) {
3191
3248
  result = null;
3192
3249
  }
3193
3250
  if (result === null || result === void 0 || result === "") {
3194
- result = this.defaultValue;
3251
+ result = this.state.default;
3195
3252
  }
3196
3253
  return result;
3197
3254
  }
3198
3255
  }
3199
3256
  class CategoricalTextScale {
3200
- constructor(values, outputCategories, defaultValue) {
3257
+ constructor(values, options = {}) {
3201
3258
  if (!Array.isArray(values) || values.length === 0) {
3202
3259
  throw new Error("values must be a non-empty array");
3203
3260
  }
3204
- this.outputCategories = outputCategories;
3205
- this.defaultValue = defaultValue;
3261
+ this.state = {
3262
+ outputValues: null,
3263
+ default: null,
3264
+ ...options
3265
+ };
3206
3266
  const frequencyMap = /* @__PURE__ */ new Map();
3207
3267
  for (const category of values) {
3208
3268
  if (category === null || category === void 0 || category === "") {
@@ -3217,18 +3277,18 @@ class CategoricalTextScale {
3217
3277
  const sortedCategories = new Map(
3218
3278
  [...frequencyMap.entries()].sort((a, b) => a[0] - b[0])
3219
3279
  );
3220
- if (sortedCategories.length > outputCategories.length) {
3221
- this.otheredCategories = sortedCategories.keys().slice(outputCategories.length - 2, sortedCategories.length);
3280
+ if (sortedCategories.length > this.state.outputValues.length) {
3281
+ this.otheredCategories = sortedCategories.keys().slice(this.state.outputValues.length - 2, sortedCategories.length);
3222
3282
  } else {
3223
3283
  this.otheredCategories = [];
3224
3284
  }
3225
3285
  this.categoryMap = /* @__PURE__ */ new Map();
3226
3286
  const sortedCategoriesKeys = [...sortedCategories.keys()];
3227
3287
  for (let i = 0; i < sortedCategoriesKeys.length; i++) {
3228
- if (i < outputCategories.length) {
3229
- this.categoryMap.set(sortedCategoriesKeys[i], outputCategories[i]);
3288
+ if (i < this.state.outputValues.length) {
3289
+ this.categoryMap.set(sortedCategoriesKeys[i], this.state.outputValues[i]);
3230
3290
  } else {
3231
- this.categoryMap.set(sortedCategoriesKeys[i], outputCategories[outputCategories.length - 1]);
3291
+ this.categoryMap.set(sortedCategoriesKeys[i], this.state.outputValues[this.state.outputValues.length - 1]);
3232
3292
  }
3233
3293
  }
3234
3294
  }
@@ -3239,17 +3299,20 @@ class CategoricalTextScale {
3239
3299
  */
3240
3300
  getValue(category) {
3241
3301
  if (category === null || category === void 0 || category === "") {
3242
- return this.defaultValue;
3302
+ return this.state.default;
3243
3303
  }
3244
3304
  return this.categoryMap.get(category);
3245
3305
  }
3246
3306
  }
3247
3307
  class ContinuousSizeScale {
3248
- constructor(dataMin, dataMax, sizeMin, sizeMax) {
3308
+ constructor(dataMin, dataMax, options = {}) {
3309
+ this.state = {
3310
+ outputRange: [0.5, 2],
3311
+ nullValue: 1,
3312
+ ...options
3313
+ };
3249
3314
  this.dataMin = dataMin;
3250
3315
  this.dataMax = dataMax;
3251
- this.sizeMin = sizeMin;
3252
- this.sizeMax = sizeMax;
3253
3316
  }
3254
3317
  /**
3255
3318
  * Get the size corresponding to the given value
@@ -3257,46 +3320,53 @@ class ContinuousSizeScale {
3257
3320
  * @returns {number} The corresponding size, clamped to min/max
3258
3321
  */
3259
3322
  getValue(value) {
3323
+ if (value === null || value === void 0 || value === "") {
3324
+ return this.state.nullValue;
3325
+ }
3260
3326
  const clampedValue = Math.max(this.dataMin, Math.min(this.dataMax, value));
3261
3327
  if (this.dataMin === this.dataMax) {
3262
3328
  if (value === this.dataMax) {
3263
- return (this.sizeMin + this.sizeMax) / 2;
3329
+ return (this.state.outputRange[0] + this.state.outputRange[1]) / 2;
3264
3330
  } else if (value < this.dataMin) {
3265
- return this.sizeMin;
3331
+ return this.state.outputRange[0];
3266
3332
  } else {
3267
- return this.sizeMax;
3333
+ return this.state.outputRange[1];
3268
3334
  }
3269
3335
  }
3270
3336
  const t = (clampedValue - this.dataMin) / (this.dataMax - this.dataMin);
3271
- return this.sizeMin + t * (this.sizeMax - this.sizeMin);
3337
+ return this.state.outputRange[0] + t * (this.state.outputRange[1] - this.state.outputRange[0]);
3272
3338
  }
3273
3339
  }
3274
3340
  class ContinuousColorScale {
3275
- constructor(dataMin, dataMax, transformMin = 0, transformMax = 1, colors = null, colorPositions = null) {
3341
+ constructor(dataMin, dataMax, options = {}) {
3342
+ this.state = {
3343
+ transformMin: 0,
3344
+ transformMax: 1,
3345
+ colorPalette: null,
3346
+ colorPositions: null,
3347
+ nullValue: "#808080",
3348
+ ...options
3349
+ };
3276
3350
  this.dataMin = dataMin;
3277
3351
  this.dataMax = dataMax;
3278
- this.transformMin = transformMin;
3279
- this.transformMax = transformMax;
3280
- this.nullColor = "#808080";
3281
- if (colors === null) {
3282
- this.colors = ["#440154", "#31688e", "#35b779", "#fde724"].map((c) => this._hexToRgb(c));
3283
- } else {
3284
- if (colors.length < 1) {
3285
- throw new Error("At least 1 color is required");
3286
- }
3287
- this.colors = colors.map((c) => this._hexToRgb(c));
3352
+ if (this.state.colorPalette === null) {
3353
+ this.state.colorPalette = ["#440154", "#31688e", "#35b779", "#fde724"];
3288
3354
  }
3355
+ if (this.state.colorPalette.length < 1) {
3356
+ throw new Error("At least 1 color is required");
3357
+ }
3358
+ this.colors = this.state.colorPalette.map((c) => this._hexToRgb(c));
3289
3359
  if (this.colors.length === 1) {
3290
3360
  this.colorPositions = [0];
3291
3361
  return;
3292
3362
  }
3293
- if (colorPositions === null) {
3363
+ if (this.state.colorPositions === null) {
3294
3364
  this.colorPositions = this.colors.map((_, i) => i / (this.colors.length - 1));
3295
3365
  } else {
3296
- if (colorPositions.length !== this.colors.length) {
3366
+ if (this.state.colorPositions.length !== this.colors.length) {
3297
3367
  throw new Error("colorPositions must have the same length as colors");
3298
3368
  }
3299
- const paired = colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3369
+ const paired = this.state.colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3300
3370
  paired.sort((a, b) => a.pos - b.pos);
3301
3371
  this.colorPositions = paired.map((p) => p.pos);
3302
3372
  this.colors = paired.map((p) => p.color);
@@ -3316,7 +3386,7 @@ class ContinuousColorScale {
3316
3386
  */
3317
3387
  getValue(value) {
3318
3388
  if (value === null || value === void 0 || value === "") {
3319
- return this.nullColor;
3389
+ return this.state.nullValue;
3320
3390
  }
3321
3391
  if (this.colors.length === 1) {
3322
3392
  return this._rgbToHex(this.colors[0].r, this.colors[0].g, this.colors[0].b);
@@ -3335,7 +3405,7 @@ class ContinuousColorScale {
3335
3405
  }
3336
3406
  const clampedValue = Math.max(this.dataMin, Math.min(this.dataMax, value));
3337
3407
  const dataT = (clampedValue - this.dataMin) / (this.dataMax - this.dataMin);
3338
- const transformedT = this.transformMin + dataT * (this.transformMax - this.transformMin);
3408
+ const transformedT = this.state.transformMin + dataT * (this.state.transformMax - this.state.transformMin);
3339
3409
  const clampedT = Math.max(0, Math.min(1, transformedT));
3340
3410
  if (clampedT <= this.colorPositions[0]) {
3341
3411
  return this._rgbToHex(this.colors[0].r, this.colors[0].g, this.colors[0].b);
@@ -3397,16 +3467,24 @@ class ContinuousColorScale {
3397
3467
  }
3398
3468
  class CategoricalColorScale {
3399
3469
  defaultPalette = ["#440154", "#31688e", "#35b779", "#fde724"];
3400
- constructor(categoryData, transformMin = 0, transformMax = 1, colors = null, colorPositions = null, maxColors = 10) {
3470
+ constructor(categoryData, options = {}) {
3401
3471
  if (!Array.isArray(categoryData) || categoryData.length === 0) {
3402
3472
  throw new Error("categoryData must be a non-empty array");
3403
3473
  }
3404
- this.transformMin = transformMin;
3405
- this.transformMax = transformMax;
3406
- this.nullColor = "#808080";
3407
- this.maxColors = maxColors;
3474
+ this.state = {
3475
+ transformMin: 0,
3476
+ transformMax: 1,
3477
+ colorPalette: null,
3478
+ colorPositions: null,
3479
+ maxCategories: 10,
3480
+ nullValue: "#808080",
3481
+ ...options
3482
+ };
3408
3483
  this.frequencyMap = /* @__PURE__ */ new Map();
3409
3484
  for (const category of categoryData) {
3485
+ if (category === null || category === void 0 || category === "") {
3486
+ continue;
3487
+ }
3410
3488
  if (this.frequencyMap.has(category)) {
3411
3489
  this.frequencyMap.set(category, this.frequencyMap.get(category) + 1);
3412
3490
  } else {
@@ -3414,26 +3492,25 @@ class CategoricalColorScale {
3414
3492
  }
3415
3493
  }
3416
3494
  this.categories = [...this.frequencyMap.entries()].sort((a, b) => b[1] - a[1]).map((entry) => entry[0]);
3417
- if (colors === null) {
3418
- this.colors = this.defaultPalette.map((c) => this._hexToRgb(c));
3419
- } else {
3420
- if (colors.length < 1) {
3421
- throw new Error("At least 1 color is required");
3422
- }
3423
- this.colors = colors.map((c) => this._hexToRgb(c));
3495
+ if (this.state.colorPalette === null) {
3496
+ this.state.colorPalette = this.defaultPalette;
3497
+ }
3498
+ if (this.state.colorPalette.length < 1) {
3499
+ throw new Error("At least 1 color is required");
3424
3500
  }
3501
+ this.colors = this.state.colorPalette.map((c) => this._hexToRgb(c));
3425
3502
  if (this.colors.length === 1) {
3426
3503
  this.colorPositions = [0];
3427
3504
  this._assignColors();
3428
3505
  return;
3429
3506
  }
3430
- if (colorPositions === null) {
3507
+ if (this.state.colorPositions === null) {
3431
3508
  this.colorPositions = this.colors.map((_, i) => i / (this.colors.length - 1));
3432
3509
  } else {
3433
- if (colorPositions.length !== this.colors.length) {
3510
+ if (this.state.colorPositions.length !== this.colors.length) {
3434
3511
  throw new Error("colorPositions must have the same length as colors");
3435
3512
  }
3436
- const paired = colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3513
+ const paired = this.state.colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3437
3514
  paired.sort((a, b) => a.pos - b.pos);
3438
3515
  this.colorPositions = paired.map((p) => p.pos);
3439
3516
  this.colors = paired.map((p) => p.color);
@@ -3460,14 +3537,16 @@ class CategoricalColorScale {
3460
3537
  }
3461
3538
  return;
3462
3539
  }
3463
- if (this.categories.length === 1) {
3464
- const color2 = this._getColorAtPosition(this.transformMin);
3540
+ if (this.categories.length === 0) {
3541
+ return;
3542
+ } else if (this.categories.length === 1) {
3543
+ const color2 = this._getColorAtPosition(this.state.transformMin);
3465
3544
  this.categoryColorMap.set(this.categories[0], color2);
3466
3545
  } else {
3467
- const nCategories = this.categories.length > this.maxColors ? this.maxColors : this.categories.length;
3546
+ const nCategories = this.categories.length > this.state.maxCategories ? this.state.maxCategories : this.categories.length;
3468
3547
  for (let i = 0; i < this.categories.length; i++) {
3469
3548
  const t = i >= nCategories ? 1 : i / (nCategories - 1);
3470
- const transformedT = this.transformMin + t * (this.transformMax - this.transformMin);
3549
+ const transformedT = this.state.transformMin + t * (this.state.transformMax - this.state.transformMin);
3471
3550
  const color2 = this._getColorAtPosition(transformedT);
3472
3551
  this.categoryColorMap.set(this.categories[i], color2);
3473
3552
  }
@@ -3514,7 +3593,7 @@ class CategoricalColorScale {
3514
3593
  */
3515
3594
  getValue(category) {
3516
3595
  if (category === null || category === void 0 || category === "") {
3517
- return this.nullColor;
3596
+ return this.state.nullValue;
3518
3597
  }
3519
3598
  if (this.categoryColorMap.has(category)) {
3520
3599
  return this.categoryColorMap.get(category);
@@ -3554,25 +3633,887 @@ class CategoricalColorScale {
3554
3633
  }).join("");
3555
3634
  }
3556
3635
  }
3557
- class Aesthetic {
3636
+ /*!
3637
+ * vanilla-picker v2.12.3
3638
+ * https://vanilla-picker.js.org
3639
+ *
3640
+ * Copyright 2017-2024 Andreas Borgen (https://github.com/Sphinxxxx), Adam Brooks (https://github.com/dissimulate)
3641
+ * Released under the ISC license.
3642
+ */
3643
+ var classCallCheck = function(instance, Constructor) {
3644
+ if (!(instance instanceof Constructor)) {
3645
+ throw new TypeError("Cannot call a class as a function");
3646
+ }
3647
+ };
3648
+ var createClass = /* @__PURE__ */ (function() {
3649
+ function defineProperties(target, props) {
3650
+ for (var i = 0; i < props.length; i++) {
3651
+ var descriptor = props[i];
3652
+ descriptor.enumerable = descriptor.enumerable || false;
3653
+ descriptor.configurable = true;
3654
+ if ("value" in descriptor) descriptor.writable = true;
3655
+ Object.defineProperty(target, descriptor.key, descriptor);
3656
+ }
3657
+ }
3658
+ return function(Constructor, protoProps, staticProps) {
3659
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
3660
+ if (staticProps) defineProperties(Constructor, staticProps);
3661
+ return Constructor;
3662
+ };
3663
+ })();
3664
+ var slicedToArray = /* @__PURE__ */ (function() {
3665
+ function sliceIterator(arr, i) {
3666
+ var _arr = [];
3667
+ var _n = true;
3668
+ var _d = false;
3669
+ var _e = void 0;
3670
+ try {
3671
+ for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
3672
+ _arr.push(_s.value);
3673
+ if (i && _arr.length === i) break;
3674
+ }
3675
+ } catch (err) {
3676
+ _d = true;
3677
+ _e = err;
3678
+ } finally {
3679
+ try {
3680
+ if (!_n && _i["return"]) _i["return"]();
3681
+ } finally {
3682
+ if (_d) throw _e;
3683
+ }
3684
+ }
3685
+ return _arr;
3686
+ }
3687
+ return function(arr, i) {
3688
+ if (Array.isArray(arr)) {
3689
+ return arr;
3690
+ } else if (Symbol.iterator in Object(arr)) {
3691
+ return sliceIterator(arr, i);
3692
+ } else {
3693
+ throw new TypeError("Invalid attempt to destructure non-iterable instance");
3694
+ }
3695
+ };
3696
+ })();
3697
+ String.prototype.startsWith = String.prototype.startsWith || function(needle) {
3698
+ return this.indexOf(needle) === 0;
3699
+ };
3700
+ String.prototype.padStart = String.prototype.padStart || function(len, pad) {
3701
+ var str = this;
3702
+ while (str.length < len) {
3703
+ str = pad + str;
3704
+ }
3705
+ return str;
3706
+ };
3707
+ var colorNames = { cb: "0f8ff", tqw: "aebd7", q: "-ffff", qmrn: "7fffd4", zr: "0ffff", bg: "5f5dc", bsq: "e4c4", bck: "---", nch: "ebcd", b: "--ff", bvt: "8a2be2", brwn: "a52a2a", brw: "deb887", ctb: "5f9ea0", hrt: "7fff-", chcT: "d2691e", cr: "7f50", rnw: "6495ed", crns: "8dc", crms: "dc143c", cn: "-ffff", Db: "--8b", Dcn: "-8b8b", Dgnr: "b8860b", Dgr: "a9a9a9", Dgrn: "-64-", Dkhk: "bdb76b", Dmgn: "8b-8b", Dvgr: "556b2f", Drng: "8c-", Drch: "9932cc", Dr: "8b--", Dsmn: "e9967a", Dsgr: "8fbc8f", DsTb: "483d8b", DsTg: "2f4f4f", Dtrq: "-ced1", Dvt: "94-d3", ppnk: "1493", pskb: "-bfff", mgr: "696969", grb: "1e90ff", rbrc: "b22222", rwht: "af0", stg: "228b22", chs: "-ff", gnsb: "dcdcdc", st: "8f8ff", g: "d7-", gnr: "daa520", gr: "808080", grn: "-8-0", grnw: "adff2f", hnw: "0fff0", htpn: "69b4", nnr: "cd5c5c", ng: "4b-82", vr: "0", khk: "0e68c", vnr: "e6e6fa", nrb: "0f5", wngr: "7cfc-", mnch: "acd", Lb: "add8e6", Lcr: "08080", Lcn: "e0ffff", Lgnr: "afad2", Lgr: "d3d3d3", Lgrn: "90ee90", Lpnk: "b6c1", Lsmn: "a07a", Lsgr: "20b2aa", Lskb: "87cefa", LsTg: "778899", Lstb: "b0c4de", Lw: "e0", m: "-ff-", mgrn: "32cd32", nn: "af0e6", mgnt: "-ff", mrn: "8--0", mqm: "66cdaa", mmb: "--cd", mmrc: "ba55d3", mmpr: "9370db", msg: "3cb371", mmsT: "7b68ee", "": "-fa9a", mtr: "48d1cc", mmvt: "c71585", mnLb: "191970", ntc: "5fffa", mstr: "e4e1", mccs: "e4b5", vjw: "dead", nv: "--80", c: "df5e6", v: "808-0", vrb: "6b8e23", rng: "a5-", rngr: "45-", rch: "da70d6", pgnr: "eee8aa", pgrn: "98fb98", ptrq: "afeeee", pvtr: "db7093", ppwh: "efd5", pchp: "dab9", pr: "cd853f", pnk: "c0cb", pm: "dda0dd", pwrb: "b0e0e6", prp: "8-080", cc: "663399", r: "--", sbr: "bc8f8f", rb: "4169e1", sbrw: "8b4513", smn: "a8072", nbr: "4a460", sgrn: "2e8b57", ssh: "5ee", snn: "a0522d", svr: "c0c0c0", skb: "87ceeb", sTb: "6a5acd", sTgr: "708090", snw: "afa", n: "-ff7f", stb: "4682b4", tn: "d2b48c", t: "-8080", thst: "d8bfd8", tmT: "6347", trqs: "40e0d0", vt: "ee82ee", whT: "5deb3", wht: "", hts: "5f5f5", w: "-", wgrn: "9acd32" };
3708
+ function printNum(num) {
3709
+ var decs = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 1;
3710
+ var str = decs > 0 ? num.toFixed(decs).replace(/0+$/, "").replace(/\.$/, "") : num.toString();
3711
+ return str || "0";
3712
+ }
3713
+ var Color = (function() {
3714
+ function Color2(r, g, b, a) {
3715
+ classCallCheck(this, Color2);
3716
+ var that = this;
3717
+ function parseString(input) {
3718
+ if (input.startsWith("hsl")) {
3719
+ var _input$match$map = input.match(/([\-\d\.e]+)/g).map(Number), _input$match$map2 = slicedToArray(_input$match$map, 4), h = _input$match$map2[0], s = _input$match$map2[1], l = _input$match$map2[2], _a = _input$match$map2[3];
3720
+ if (_a === void 0) {
3721
+ _a = 1;
3722
+ }
3723
+ h /= 360;
3724
+ s /= 100;
3725
+ l /= 100;
3726
+ that.hsla = [h, s, l, _a];
3727
+ } else if (input.startsWith("rgb")) {
3728
+ var _input$match$map3 = input.match(/([\-\d\.e]+)/g).map(Number), _input$match$map4 = slicedToArray(_input$match$map3, 4), _r = _input$match$map4[0], _g = _input$match$map4[1], _b = _input$match$map4[2], _a2 = _input$match$map4[3];
3729
+ if (_a2 === void 0) {
3730
+ _a2 = 1;
3731
+ }
3732
+ that.rgba = [_r, _g, _b, _a2];
3733
+ } else {
3734
+ if (input.startsWith("#")) {
3735
+ that.rgba = Color2.hexToRgb(input);
3736
+ } else {
3737
+ that.rgba = Color2.nameToRgb(input) || Color2.hexToRgb(input);
3738
+ }
3739
+ }
3740
+ }
3741
+ if (r === void 0) ;
3742
+ else if (Array.isArray(r)) {
3743
+ this.rgba = r;
3744
+ } else if (b === void 0) {
3745
+ var color2 = r && "" + r;
3746
+ if (color2) {
3747
+ parseString(color2.toLowerCase());
3748
+ }
3749
+ } else {
3750
+ this.rgba = [r, g, b, a === void 0 ? 1 : a];
3751
+ }
3752
+ }
3753
+ createClass(Color2, [{
3754
+ key: "printRGB",
3755
+ value: function printRGB(alpha) {
3756
+ var rgb2 = alpha ? this.rgba : this.rgba.slice(0, 3), vals = rgb2.map(function(x, i) {
3757
+ return printNum(x, i === 3 ? 3 : 0);
3758
+ });
3759
+ return alpha ? "rgba(" + vals + ")" : "rgb(" + vals + ")";
3760
+ }
3761
+ }, {
3762
+ key: "printHSL",
3763
+ value: function printHSL(alpha) {
3764
+ var mults = [360, 100, 100, 1], suff = ["", "%", "%", ""];
3765
+ var hsl2 = alpha ? this.hsla : this.hsla.slice(0, 3), vals = hsl2.map(function(x, i) {
3766
+ return printNum(x * mults[i], i === 3 ? 3 : 1) + suff[i];
3767
+ });
3768
+ return alpha ? "hsla(" + vals + ")" : "hsl(" + vals + ")";
3769
+ }
3770
+ }, {
3771
+ key: "printHex",
3772
+ value: function printHex(alpha) {
3773
+ var hex2 = this.hex;
3774
+ return alpha ? hex2 : hex2.substring(0, 7);
3775
+ }
3776
+ }, {
3777
+ key: "rgba",
3778
+ get: function get2() {
3779
+ if (this._rgba) {
3780
+ return this._rgba;
3781
+ }
3782
+ if (!this._hsla) {
3783
+ throw new Error("No color is set");
3784
+ }
3785
+ return this._rgba = Color2.hslToRgb(this._hsla);
3786
+ },
3787
+ set: function set2(rgb2) {
3788
+ if (rgb2.length === 3) {
3789
+ rgb2[3] = 1;
3790
+ }
3791
+ this._rgba = rgb2;
3792
+ this._hsla = null;
3793
+ }
3794
+ }, {
3795
+ key: "rgbString",
3796
+ get: function get2() {
3797
+ return this.printRGB();
3798
+ }
3799
+ }, {
3800
+ key: "rgbaString",
3801
+ get: function get2() {
3802
+ return this.printRGB(true);
3803
+ }
3804
+ }, {
3805
+ key: "hsla",
3806
+ get: function get2() {
3807
+ if (this._hsla) {
3808
+ return this._hsla;
3809
+ }
3810
+ if (!this._rgba) {
3811
+ throw new Error("No color is set");
3812
+ }
3813
+ return this._hsla = Color2.rgbToHsl(this._rgba);
3814
+ },
3815
+ set: function set2(hsl2) {
3816
+ if (hsl2.length === 3) {
3817
+ hsl2[3] = 1;
3818
+ }
3819
+ this._hsla = hsl2;
3820
+ this._rgba = null;
3821
+ }
3822
+ }, {
3823
+ key: "hslString",
3824
+ get: function get2() {
3825
+ return this.printHSL();
3826
+ }
3827
+ }, {
3828
+ key: "hslaString",
3829
+ get: function get2() {
3830
+ return this.printHSL(true);
3831
+ }
3832
+ }, {
3833
+ key: "hex",
3834
+ get: function get2() {
3835
+ var rgb2 = this.rgba, hex2 = rgb2.map(function(x, i) {
3836
+ return i < 3 ? x.toString(16) : Math.round(x * 255).toString(16);
3837
+ });
3838
+ return "#" + hex2.map(function(x) {
3839
+ return x.padStart(2, "0");
3840
+ }).join("");
3841
+ },
3842
+ set: function set2(hex2) {
3843
+ this.rgba = Color2.hexToRgb(hex2);
3844
+ }
3845
+ }], [{
3846
+ key: "hexToRgb",
3847
+ value: function hexToRgb2(input) {
3848
+ var hex2 = (input.startsWith("#") ? input.slice(1) : input).replace(/^(\w{3})$/, "$1F").replace(/^(\w)(\w)(\w)(\w)$/, "$1$1$2$2$3$3$4$4").replace(/^(\w{6})$/, "$1FF");
3849
+ if (!hex2.match(/^([0-9a-fA-F]{8})$/)) {
3850
+ throw new Error("Unknown hex color; " + input);
3851
+ }
3852
+ var rgba2 = hex2.match(/^(\w\w)(\w\w)(\w\w)(\w\w)$/).slice(1).map(function(x) {
3853
+ return parseInt(x, 16);
3854
+ });
3855
+ rgba2[3] = rgba2[3] / 255;
3856
+ return rgba2;
3857
+ }
3858
+ }, {
3859
+ key: "nameToRgb",
3860
+ value: function nameToRgb(input) {
3861
+ var hash = input.toLowerCase().replace("at", "T").replace(/[aeiouyldf]/g, "").replace("ght", "L").replace("rk", "D").slice(-5, 4), hex2 = colorNames[hash];
3862
+ return hex2 === void 0 ? hex2 : Color2.hexToRgb(hex2.replace(/\-/g, "00").padStart(6, "f"));
3863
+ }
3864
+ }, {
3865
+ key: "rgbToHsl",
3866
+ value: function rgbToHsl(_ref) {
3867
+ var _ref2 = slicedToArray(_ref, 4), r = _ref2[0], g = _ref2[1], b = _ref2[2], a = _ref2[3];
3868
+ r /= 255;
3869
+ g /= 255;
3870
+ b /= 255;
3871
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
3872
+ var h = void 0, s = void 0, l = (max + min) / 2;
3873
+ if (max === min) {
3874
+ h = s = 0;
3875
+ } else {
3876
+ var d = max - min;
3877
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
3878
+ switch (max) {
3879
+ case r:
3880
+ h = (g - b) / d + (g < b ? 6 : 0);
3881
+ break;
3882
+ case g:
3883
+ h = (b - r) / d + 2;
3884
+ break;
3885
+ case b:
3886
+ h = (r - g) / d + 4;
3887
+ break;
3888
+ }
3889
+ h /= 6;
3890
+ }
3891
+ return [h, s, l, a];
3892
+ }
3893
+ }, {
3894
+ key: "hslToRgb",
3895
+ value: function hslToRgb(_ref3) {
3896
+ var _ref4 = slicedToArray(_ref3, 4), h = _ref4[0], s = _ref4[1], l = _ref4[2], a = _ref4[3];
3897
+ var r = void 0, g = void 0, b = void 0;
3898
+ if (s === 0) {
3899
+ r = g = b = l;
3900
+ } else {
3901
+ var hue2rgb = function hue2rgb2(p2, q2, t) {
3902
+ if (t < 0) t += 1;
3903
+ if (t > 1) t -= 1;
3904
+ if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
3905
+ if (t < 1 / 2) return q2;
3906
+ if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
3907
+ return p2;
3908
+ };
3909
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
3910
+ r = hue2rgb(p, q, h + 1 / 3);
3911
+ g = hue2rgb(p, q, h);
3912
+ b = hue2rgb(p, q, h - 1 / 3);
3913
+ }
3914
+ var rgba2 = [r * 255, g * 255, b * 255].map(Math.round);
3915
+ rgba2[3] = a;
3916
+ return rgba2;
3917
+ }
3918
+ }]);
3919
+ return Color2;
3920
+ })();
3921
+ var EventBucket = (function() {
3922
+ function EventBucket2() {
3923
+ classCallCheck(this, EventBucket2);
3924
+ this._events = [];
3925
+ }
3926
+ createClass(EventBucket2, [{
3927
+ key: "add",
3928
+ value: function add(target, type, handler) {
3929
+ target.addEventListener(type, handler, false);
3930
+ this._events.push({
3931
+ target,
3932
+ type,
3933
+ handler
3934
+ });
3935
+ }
3936
+ }, {
3937
+ key: "remove",
3938
+ value: function remove2(target, type, handler) {
3939
+ this._events = this._events.filter(function(e) {
3940
+ var isMatch = true;
3941
+ if (target && target !== e.target) {
3942
+ isMatch = false;
3943
+ }
3944
+ if (type && type !== e.type) {
3945
+ isMatch = false;
3946
+ }
3947
+ if (handler && handler !== e.handler) {
3948
+ isMatch = false;
3949
+ }
3950
+ if (isMatch) {
3951
+ EventBucket2._doRemove(e.target, e.type, e.handler);
3952
+ }
3953
+ return !isMatch;
3954
+ });
3955
+ }
3956
+ }, {
3957
+ key: "destroy",
3958
+ value: function destroy() {
3959
+ this._events.forEach(function(e) {
3960
+ return EventBucket2._doRemove(e.target, e.type, e.handler);
3961
+ });
3962
+ this._events = [];
3963
+ }
3964
+ }], [{
3965
+ key: "_doRemove",
3966
+ value: function _doRemove(target, type, handler) {
3967
+ target.removeEventListener(type, handler, false);
3968
+ }
3969
+ }]);
3970
+ return EventBucket2;
3971
+ })();
3972
+ function parseHTML(htmlString) {
3973
+ var div = document.createElement("div");
3974
+ div.innerHTML = htmlString;
3975
+ return div.firstElementChild;
3976
+ }
3977
+ function dragTrack(eventBucket, area, callback) {
3978
+ var dragging = false;
3979
+ function clamp(val, min, max) {
3980
+ return Math.max(min, Math.min(val, max));
3981
+ }
3982
+ function onMove(e, info, starting) {
3983
+ if (starting) {
3984
+ dragging = true;
3985
+ }
3986
+ if (!dragging) {
3987
+ return;
3988
+ }
3989
+ e.preventDefault();
3990
+ var bounds = area.getBoundingClientRect(), w = bounds.width, h = bounds.height, x = info.clientX, y = info.clientY;
3991
+ var relX = clamp(x - bounds.left, 0, w), relY = clamp(y - bounds.top, 0, h);
3992
+ callback(relX / w, relY / h);
3993
+ }
3994
+ function onMouse(e, starting) {
3995
+ var button = e.buttons === void 0 ? e.which : e.buttons;
3996
+ if (button === 1) {
3997
+ onMove(e, e, starting);
3998
+ } else {
3999
+ dragging = false;
4000
+ }
4001
+ }
4002
+ function onTouch(e, starting) {
4003
+ if (e.touches.length === 1) {
4004
+ onMove(e, e.touches[0], starting);
4005
+ } else {
4006
+ dragging = false;
4007
+ }
4008
+ }
4009
+ eventBucket.add(area, "mousedown", function(e) {
4010
+ onMouse(e, true);
4011
+ });
4012
+ eventBucket.add(area, "touchstart", function(e) {
4013
+ onTouch(e, true);
4014
+ });
4015
+ eventBucket.add(window, "mousemove", onMouse);
4016
+ eventBucket.add(area, "touchmove", onTouch);
4017
+ eventBucket.add(window, "mouseup", function(e) {
4018
+ dragging = false;
4019
+ });
4020
+ eventBucket.add(area, "touchend", function(e) {
4021
+ dragging = false;
4022
+ });
4023
+ eventBucket.add(area, "touchcancel", function(e) {
4024
+ dragging = false;
4025
+ });
4026
+ }
4027
+ var BG_TRANSP = "linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0 / 2em 2em,\n linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em / 2em 2em";
4028
+ var HUES = 360;
4029
+ var EVENT_KEY = "keydown", EVENT_CLICK_OUTSIDE = "mousedown", EVENT_TAB_MOVE = "focusin";
4030
+ function $(selector2, context) {
4031
+ return (context || document).querySelector(selector2);
4032
+ }
4033
+ function stopEvent(e) {
4034
+ e.preventDefault();
4035
+ e.stopPropagation();
4036
+ }
4037
+ function onKey(bucket, target, keys, handler, stop) {
4038
+ bucket.add(target, EVENT_KEY, function(e) {
4039
+ if (keys.indexOf(e.key) >= 0) {
4040
+ handler(e);
4041
+ }
4042
+ });
4043
+ }
4044
+ var Picker = (function() {
4045
+ function Picker2(options) {
4046
+ classCallCheck(this, Picker2);
4047
+ this.settings = {
4048
+ popup: "right",
4049
+ layout: "default",
4050
+ alpha: true,
4051
+ editor: true,
4052
+ editorFormat: "hex",
4053
+ cancelButton: false,
4054
+ defaultColor: "#0cf"
4055
+ };
4056
+ this._events = new EventBucket();
4057
+ this.onChange = null;
4058
+ this.onDone = null;
4059
+ this.onOpen = null;
4060
+ this.onClose = null;
4061
+ this.setOptions(options);
4062
+ }
4063
+ createClass(Picker2, [{
4064
+ key: "setOptions",
4065
+ value: function setOptions(options) {
4066
+ var _this = this;
4067
+ if (!options) {
4068
+ return;
4069
+ }
4070
+ var settings = this.settings;
4071
+ function transfer(source, target, skipKeys) {
4072
+ for (var key in source) {
4073
+ target[key] = source[key];
4074
+ }
4075
+ }
4076
+ if (options instanceof HTMLElement) {
4077
+ settings.parent = options;
4078
+ } else {
4079
+ if (settings.parent && options.parent && settings.parent !== options.parent) {
4080
+ this._events.remove(settings.parent);
4081
+ this._popupInited = false;
4082
+ }
4083
+ transfer(options, settings);
4084
+ if (options.onChange) {
4085
+ this.onChange = options.onChange;
4086
+ }
4087
+ if (options.onDone) {
4088
+ this.onDone = options.onDone;
4089
+ }
4090
+ if (options.onOpen) {
4091
+ this.onOpen = options.onOpen;
4092
+ }
4093
+ if (options.onClose) {
4094
+ this.onClose = options.onClose;
4095
+ }
4096
+ var col = options.color || options.colour;
4097
+ if (col) {
4098
+ this._setColor(col);
4099
+ }
4100
+ }
4101
+ var parent = settings.parent;
4102
+ if (parent && settings.popup && !this._popupInited) {
4103
+ var openProxy = function openProxy2(e) {
4104
+ return _this.openHandler(e);
4105
+ };
4106
+ this._events.add(parent, "click", openProxy);
4107
+ onKey(this._events, parent, [" ", "Spacebar", "Enter"], openProxy);
4108
+ this._popupInited = true;
4109
+ } else if (options.parent && !settings.popup) {
4110
+ this.show();
4111
+ }
4112
+ }
4113
+ }, {
4114
+ key: "openHandler",
4115
+ value: function openHandler(e) {
4116
+ if (this.show()) {
4117
+ e && e.preventDefault();
4118
+ this.settings.parent.style.pointerEvents = "none";
4119
+ var toFocus = e && e.type === EVENT_KEY ? this._domEdit : this.domElement;
4120
+ setTimeout(function() {
4121
+ return toFocus.focus();
4122
+ }, 100);
4123
+ if (this.onOpen) {
4124
+ this.onOpen(this.colour);
4125
+ }
4126
+ }
4127
+ }
4128
+ }, {
4129
+ key: "closeHandler",
4130
+ value: function closeHandler(e) {
4131
+ var event = e && e.type;
4132
+ var doHide = false;
4133
+ if (!e) {
4134
+ doHide = true;
4135
+ } else if (event === EVENT_CLICK_OUTSIDE || event === EVENT_TAB_MOVE) {
4136
+ var knownTime = (this.__containedEvent || 0) + 100;
4137
+ if (e.timeStamp > knownTime) {
4138
+ doHide = true;
4139
+ }
4140
+ } else {
4141
+ stopEvent(e);
4142
+ doHide = true;
4143
+ }
4144
+ if (doHide && this.hide()) {
4145
+ this.settings.parent.style.pointerEvents = "";
4146
+ if (event !== EVENT_CLICK_OUTSIDE) {
4147
+ this.settings.parent.focus();
4148
+ }
4149
+ if (this.onClose) {
4150
+ this.onClose(this.colour);
4151
+ }
4152
+ }
4153
+ }
4154
+ }, {
4155
+ key: "movePopup",
4156
+ value: function movePopup(options, open) {
4157
+ this.closeHandler();
4158
+ this.setOptions(options);
4159
+ if (open) {
4160
+ this.openHandler();
4161
+ }
4162
+ }
4163
+ }, {
4164
+ key: "setColor",
4165
+ value: function setColor(color2, silent) {
4166
+ this._setColor(color2, { silent });
4167
+ }
4168
+ }, {
4169
+ key: "_setColor",
4170
+ value: function _setColor(color2, flags) {
4171
+ if (typeof color2 === "string") {
4172
+ color2 = color2.trim();
4173
+ }
4174
+ if (!color2) {
4175
+ return;
4176
+ }
4177
+ flags = flags || {};
4178
+ var c = void 0;
4179
+ try {
4180
+ c = new Color(color2);
4181
+ } catch (ex) {
4182
+ if (flags.failSilently) {
4183
+ return;
4184
+ }
4185
+ throw ex;
4186
+ }
4187
+ if (!this.settings.alpha) {
4188
+ var hsla2 = c.hsla;
4189
+ hsla2[3] = 1;
4190
+ c.hsla = hsla2;
4191
+ }
4192
+ this.colour = this.color = c;
4193
+ this._setHSLA(null, null, null, null, flags);
4194
+ }
4195
+ }, {
4196
+ key: "setColour",
4197
+ value: function setColour(colour, silent) {
4198
+ this.setColor(colour, silent);
4199
+ }
4200
+ }, {
4201
+ key: "show",
4202
+ value: function show() {
4203
+ var parent = this.settings.parent;
4204
+ if (!parent) {
4205
+ return false;
4206
+ }
4207
+ if (this.domElement) {
4208
+ var toggled = this._toggleDOM(true);
4209
+ this._setPosition();
4210
+ return toggled;
4211
+ }
4212
+ var html = this.settings.template || '<div class="picker_wrapper" tabindex="-1"><div class="picker_arrow"></div><div class="picker_hue picker_slider"><div class="picker_selector"></div></div><div class="picker_sl"><div class="picker_selector"></div></div><div class="picker_alpha picker_slider"><div class="picker_selector"></div></div><div class="picker_editor"><input aria-label="Type a color name or hex value"/></div><div class="picker_sample"></div><div class="picker_done"><button>Ok</button></div><div class="picker_cancel"><button>Cancel</button></div></div>';
4213
+ var wrapper = parseHTML(html);
4214
+ this.domElement = wrapper;
4215
+ this._domH = $(".picker_hue", wrapper);
4216
+ this._domSL = $(".picker_sl", wrapper);
4217
+ this._domA = $(".picker_alpha", wrapper);
4218
+ this._domEdit = $(".picker_editor input", wrapper);
4219
+ this._domSample = $(".picker_sample", wrapper);
4220
+ this._domOkay = $(".picker_done button", wrapper);
4221
+ this._domCancel = $(".picker_cancel button", wrapper);
4222
+ wrapper.classList.add("layout_" + this.settings.layout);
4223
+ if (!this.settings.alpha) {
4224
+ wrapper.classList.add("no_alpha");
4225
+ }
4226
+ if (!this.settings.editor) {
4227
+ wrapper.classList.add("no_editor");
4228
+ }
4229
+ if (!this.settings.cancelButton) {
4230
+ wrapper.classList.add("no_cancel");
4231
+ }
4232
+ this._ifPopup(function() {
4233
+ return wrapper.classList.add("popup");
4234
+ });
4235
+ this._setPosition();
4236
+ if (this.colour) {
4237
+ this._updateUI();
4238
+ } else {
4239
+ this._setColor(this.settings.defaultColor);
4240
+ }
4241
+ this._bindEvents();
4242
+ return true;
4243
+ }
4244
+ }, {
4245
+ key: "hide",
4246
+ value: function hide() {
4247
+ return this._toggleDOM(false);
4248
+ }
4249
+ }, {
4250
+ key: "destroy",
4251
+ value: function destroy() {
4252
+ this._events.destroy();
4253
+ if (this.domElement) {
4254
+ this.settings.parent.removeChild(this.domElement);
4255
+ }
4256
+ }
4257
+ }, {
4258
+ key: "_bindEvents",
4259
+ value: function _bindEvents() {
4260
+ var _this2 = this;
4261
+ var that = this, dom = this.domElement, events = this._events;
4262
+ function addEvent(target, type, handler) {
4263
+ events.add(target, type, handler);
4264
+ }
4265
+ addEvent(dom, "click", function(e) {
4266
+ return e.preventDefault();
4267
+ });
4268
+ dragTrack(events, this._domH, function(x, y) {
4269
+ return that._setHSLA(x);
4270
+ });
4271
+ dragTrack(events, this._domSL, function(x, y) {
4272
+ return that._setHSLA(null, x, 1 - y);
4273
+ });
4274
+ if (this.settings.alpha) {
4275
+ dragTrack(events, this._domA, function(x, y) {
4276
+ return that._setHSLA(null, null, null, 1 - y);
4277
+ });
4278
+ }
4279
+ var editInput = this._domEdit;
4280
+ {
4281
+ addEvent(editInput, "input", function(e) {
4282
+ that._setColor(this.value, { fromEditor: true, failSilently: true });
4283
+ });
4284
+ addEvent(editInput, "focus", function(e) {
4285
+ var input = this;
4286
+ if (input.selectionStart === input.selectionEnd) {
4287
+ input.select();
4288
+ }
4289
+ });
4290
+ }
4291
+ this._ifPopup(function() {
4292
+ var popupCloseProxy = function popupCloseProxy2(e) {
4293
+ return _this2.closeHandler(e);
4294
+ };
4295
+ addEvent(window, EVENT_CLICK_OUTSIDE, popupCloseProxy);
4296
+ addEvent(window, EVENT_TAB_MOVE, popupCloseProxy);
4297
+ onKey(events, dom, ["Esc", "Escape"], popupCloseProxy);
4298
+ var timeKeeper = function timeKeeper2(e) {
4299
+ _this2.__containedEvent = e.timeStamp;
4300
+ };
4301
+ addEvent(dom, EVENT_CLICK_OUTSIDE, timeKeeper);
4302
+ addEvent(dom, EVENT_TAB_MOVE, timeKeeper);
4303
+ addEvent(_this2._domCancel, "click", popupCloseProxy);
4304
+ });
4305
+ var onDoneProxy = function onDoneProxy2(e) {
4306
+ _this2._ifPopup(function() {
4307
+ return _this2.closeHandler(e);
4308
+ });
4309
+ if (_this2.onDone) {
4310
+ _this2.onDone(_this2.colour);
4311
+ }
4312
+ };
4313
+ addEvent(this._domOkay, "click", onDoneProxy);
4314
+ onKey(events, dom, ["Enter"], onDoneProxy);
4315
+ }
4316
+ }, {
4317
+ key: "_setPosition",
4318
+ value: function _setPosition() {
4319
+ var parent = this.settings.parent, elm = this.domElement;
4320
+ if (parent !== elm.parentNode) {
4321
+ parent.appendChild(elm);
4322
+ }
4323
+ this._ifPopup(function(popup) {
4324
+ if (getComputedStyle(parent).position === "static") {
4325
+ parent.style.position = "relative";
4326
+ }
4327
+ var cssClass = popup === true ? "popup_right" : "popup_" + popup;
4328
+ ["popup_top", "popup_bottom", "popup_left", "popup_right"].forEach(function(c) {
4329
+ if (c === cssClass) {
4330
+ elm.classList.add(c);
4331
+ } else {
4332
+ elm.classList.remove(c);
4333
+ }
4334
+ });
4335
+ elm.classList.add(cssClass);
4336
+ });
4337
+ }
4338
+ }, {
4339
+ key: "_setHSLA",
4340
+ value: function _setHSLA(h, s, l, a, flags) {
4341
+ flags = flags || {};
4342
+ var col = this.colour, hsla2 = col.hsla;
4343
+ [h, s, l, a].forEach(function(x, i) {
4344
+ if (x || x === 0) {
4345
+ hsla2[i] = x;
4346
+ }
4347
+ });
4348
+ col.hsla = hsla2;
4349
+ this._updateUI(flags);
4350
+ if (this.onChange && !flags.silent) {
4351
+ this.onChange(col);
4352
+ }
4353
+ }
4354
+ }, {
4355
+ key: "_updateUI",
4356
+ value: function _updateUI(flags) {
4357
+ if (!this.domElement) {
4358
+ return;
4359
+ }
4360
+ flags = flags || {};
4361
+ var col = this.colour, hsl2 = col.hsla, cssHue = "hsl(" + hsl2[0] * HUES + ", 100%, 50%)", cssHSL = col.hslString, cssHSLA = col.hslaString;
4362
+ var uiH = this._domH, uiSL = this._domSL, uiA = this._domA, thumbH = $(".picker_selector", uiH), thumbSL = $(".picker_selector", uiSL), thumbA = $(".picker_selector", uiA);
4363
+ function posX(parent, child, relX) {
4364
+ child.style.left = relX * 100 + "%";
4365
+ }
4366
+ function posY(parent, child, relY) {
4367
+ child.style.top = relY * 100 + "%";
4368
+ }
4369
+ posX(uiH, thumbH, hsl2[0]);
4370
+ this._domSL.style.backgroundColor = this._domH.style.color = cssHue;
4371
+ posX(uiSL, thumbSL, hsl2[1]);
4372
+ posY(uiSL, thumbSL, 1 - hsl2[2]);
4373
+ uiSL.style.color = cssHSL;
4374
+ posY(uiA, thumbA, 1 - hsl2[3]);
4375
+ var opaque = cssHSL, transp = opaque.replace("hsl", "hsla").replace(")", ", 0)"), bg = "linear-gradient(" + [opaque, transp] + ")";
4376
+ this._domA.style.background = bg + ", " + BG_TRANSP;
4377
+ if (!flags.fromEditor) {
4378
+ var format = this.settings.editorFormat, alpha = this.settings.alpha;
4379
+ var value = void 0;
4380
+ switch (format) {
4381
+ case "rgb":
4382
+ value = col.printRGB(alpha);
4383
+ break;
4384
+ case "hsl":
4385
+ value = col.printHSL(alpha);
4386
+ break;
4387
+ default:
4388
+ value = col.printHex(alpha);
4389
+ }
4390
+ this._domEdit.value = value;
4391
+ }
4392
+ this._domSample.style.color = cssHSLA;
4393
+ }
4394
+ }, {
4395
+ key: "_ifPopup",
4396
+ value: function _ifPopup(actionIf, actionElse) {
4397
+ if (this.settings.parent && this.settings.popup) {
4398
+ actionIf && actionIf(this.settings.popup);
4399
+ } else {
4400
+ actionElse && actionElse();
4401
+ }
4402
+ }
4403
+ }, {
4404
+ key: "_toggleDOM",
4405
+ value: function _toggleDOM(toVisible) {
4406
+ var dom = this.domElement;
4407
+ if (!dom) {
4408
+ return false;
4409
+ }
4410
+ var displayStyle = toVisible ? "" : "none", toggle = dom.style.display !== displayStyle;
4411
+ if (toggle) {
4412
+ dom.style.display = displayStyle;
4413
+ }
4414
+ return toggle;
4415
+ }
4416
+ }]);
4417
+ return Picker2;
4418
+ })();
4419
+ {
4420
+ var style = document.createElement("style");
4421
+ 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}';
4422
+ document.documentElement.firstElementChild.appendChild(style);
4423
+ Picker.StyleElement = style;
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
+ }
4489
+ class Aesthetic extends Subscribable {
3558
4490
  state;
3559
4491
  // Object containing all configuration used to infer the scale
3560
4492
  scale;
3561
4493
  // the actual scale instance
4494
+ values;
4495
+ // Store the original values for scale recreation
4496
+ defaultPalette = ["#440154", "#365C8D", "#1FA187", "#9FDA3A"];
4497
+ treeState;
4498
+ // Reference to the tree state for updates
3562
4499
  constructor(values, options = {}) {
4500
+ super();
3563
4501
  if (!options.scaleType) {
3564
4502
  throw new Error("scaleType is required");
3565
4503
  }
3566
4504
  if (options.default === void 0) {
3567
4505
  throw new Error("default is required");
3568
4506
  }
4507
+ this.values = values;
4508
+ this.treeState = options.treeState || null;
3569
4509
  this.state = {
3570
4510
  scaleType: void 0,
3571
4511
  default: void 0,
3572
4512
  isCategorical: void 0,
3573
4513
  outputValues: null,
3574
4514
  outputRegex: null,
3575
- colorPalette: null,
4515
+ colorPalette: this.defaultPalette,
4516
+ colorPositions: this.defaultPalette.map((_, i) => i / (this.defaultPalette.length - 1)),
3576
4517
  outputRange: null,
3577
4518
  inputUnits: null,
3578
4519
  title: null,
@@ -3582,17 +4523,11 @@ class Aesthetic {
3582
4523
  transformMin: 0,
3583
4524
  transformMax: 1,
3584
4525
  transformFn: null,
3585
- colorPositions: null,
4526
+ nullValue: "#808080",
4527
+ showNullInLegend: true,
3586
4528
  ...options
3587
4529
  };
3588
- this.updateScale(values);
3589
- }
3590
- /**
3591
- * Set the scale instance for this aesthetic
3592
- * @param {object} scale - The scale instance
3593
- */
3594
- setScale(scale) {
3595
- this.scale = scale;
4530
+ this.updateScale(false);
3596
4531
  }
3597
4532
  /**
3598
4533
  * Get the output value for a given input value
@@ -3607,14 +4542,10 @@ class Aesthetic {
3607
4542
  * Update the scale based on current state
3608
4543
  * Uses the stored state to create an appropriate scale
3609
4544
  */
3610
- updateScale(values) {
4545
+ updateScale(notify = true) {
4546
+ const values = this.values;
3611
4547
  const { scaleType, isCategorical } = this.state;
3612
4548
  let scale;
3613
- if (scaleType === "null") {
3614
- scale = new NullScale(this.state.default);
3615
- this.setScale(scale);
3616
- return;
3617
- }
3618
4549
  let isAlreadyOutputFormat = true;
3619
4550
  if (isCategorical && (this.state.outputValues || this.state.outputRegex)) {
3620
4551
  if (this.state.outputValues) {
@@ -3636,77 +4567,541 @@ class Aesthetic {
3636
4567
  } else {
3637
4568
  isAlreadyOutputFormat = false;
3638
4569
  }
3639
- if (scaleType === "identity" || isAlreadyOutputFormat) {
3640
- scale = new IdentityScale(
3641
- this.state.default,
3642
- this.state.outputValues,
3643
- this.state.transformFn
3644
- );
3645
- this.setScale(scale);
3646
- return;
3647
- }
3648
- if (scaleType === "text") {
4570
+ if (scaleType === "null") {
4571
+ scale = new NullScale(this.state);
4572
+ this.scale = scale;
4573
+ } else if (scaleType === "identity" || isAlreadyOutputFormat) {
4574
+ scale = new IdentityScale(this.state);
4575
+ this.scale = scale;
4576
+ } else if (scaleType === "text") {
3649
4577
  if (!isCategorical) {
3650
4578
  throw new Error("Text scales can only be used with categorical data");
3651
4579
  }
3652
- scale = new CategoricalTextScale(values, this.state.outputValues, this.state.default);
3653
- this.setScale(scale);
3654
- return;
3655
- }
3656
- if (scaleType === "size") {
4580
+ scale = new CategoricalTextScale(values, this.state);
4581
+ this.scale = scale;
4582
+ } else if (scaleType === "size") {
3657
4583
  if (isCategorical) {
3658
4584
  throw new Error("Size scales can only be used with continuous data");
3659
4585
  }
3660
4586
  const numericValues = values.map((v) => Number(v)).filter((v) => !isNaN(v));
3661
4587
  if (numericValues.length === 0) {
3662
4588
  console.warn("No numeric values found for size scale, using NullScale");
3663
- scale = new NullScale(this.state.default);
4589
+ scale = new NullScale(this.state);
3664
4590
  } else {
3665
4591
  const min = Math.min(...numericValues);
3666
4592
  const max = Math.max(...numericValues);
3667
- const range = this.state.outputRange || [0.5, 2];
3668
- scale = new ContinuousSizeScale(min, max, range[0], range[1]);
4593
+ scale = new ContinuousSizeScale(min, max, this.state);
3669
4594
  }
3670
- this.setScale(scale);
3671
- return;
3672
- }
3673
- if (scaleType === "color") {
4595
+ this.scale = scale;
4596
+ } else if (scaleType === "color") {
3674
4597
  if (isCategorical) {
3675
- scale = new CategoricalColorScale(
3676
- values,
3677
- this.state.transformMin,
3678
- this.state.transformMax,
3679
- this.state.colorPalette,
3680
- this.state.colorPositions,
3681
- this.state.maxCategories
3682
- );
4598
+ scale = new CategoricalColorScale(values, this.state);
3683
4599
  } else {
3684
4600
  const numericValues = values.map((v) => Number(v)).filter((v) => !isNaN(v));
3685
4601
  if (numericValues.length === 0) {
3686
4602
  console.warn("No numeric values found for color scale, using NullScale");
3687
- scale = new NullScale(this.state.default);
4603
+ scale = new NullScale(this.state);
3688
4604
  } else {
3689
4605
  const min = Math.min(...numericValues);
3690
4606
  const max = Math.max(...numericValues);
3691
- scale = new ContinuousColorScale(
3692
- min,
3693
- max,
3694
- this.state.transformMin,
3695
- this.state.transformMax,
3696
- this.state.colorPalette,
3697
- this.state.colorPositions
3698
- );
4607
+ scale = new ContinuousColorScale(min, max, this.state);
3699
4608
  }
3700
4609
  }
3701
- this.setScale(scale);
3702
- return;
4610
+ this.scale = scale;
4611
+ } else {
4612
+ throw new Error(`Unknown scale type: ${scaleType}`);
4613
+ }
4614
+ if (notify) {
4615
+ this.notify("aestheticChange", this);
4616
+ }
4617
+ }
4618
+ /**
4619
+ * Update aesthetic state properties
4620
+ * @param {Object} updates - Object with properties to update
4621
+ */
4622
+ updateState(updates) {
4623
+ Object.assign(this.state, updates);
4624
+ this.updateScale(this.values);
4625
+ }
4626
+ /**
4627
+ * Create settings widget(s) for this aesthetic
4628
+ * @param {Object} options - Configuration options
4629
+ * @param {number} options.controlHeight - Height of controls
4630
+ * @param {string} options.columnId - The column ID this aesthetic is mapped to
4631
+ * @returns {HTMLElement|null} The settings widget container, or null if no settings available
4632
+ */
4633
+ createSettingsWidget(options = {}) {
4634
+ const {
4635
+ controlHeight = 20,
4636
+ columnId = null
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";
4647
+ if (this.state.scaleType === "color") {
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
+ }
4720
+ }
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);
3703
4727
  }
3704
- throw new Error(`Unknown scale type: ${scaleType}`);
4728
+ return container;
4729
+ }
4730
+ /**
4731
+ * Create a color palette editor widget
4732
+ * @param {number} controlHeight - Height of controls
4733
+ * @returns {HTMLElement} The palette editor container
4734
+ */
4735
+ createColorPaletteEditor(controlHeight) {
4736
+ if (!this.scale) {
4737
+ return null;
4738
+ }
4739
+ const container = document.createElement("div");
4740
+ container.className = "ht-color-palette-editor";
4741
+ const gradientContainer = document.createElement("div");
4742
+ gradientContainer.className = "ht-gradient-container";
4743
+ const gradientColumn = document.createElement("div");
4744
+ gradientColumn.className = "ht-gradient-column";
4745
+ const colorSquaresContainer = document.createElement("div");
4746
+ colorSquaresContainer.className = "ht-color-squares-container";
4747
+ const gradientBox = document.createElement("div");
4748
+ gradientBox.className = "ht-gradient-box";
4749
+ const updateGradientDisplay = () => {
4750
+ const gradientStops = this.state.colorPalette.map((color2, i) => {
4751
+ const pos = this.state.colorPositions[i] * 100;
4752
+ return `${color2} ${pos}%`;
4753
+ }).join(", ");
4754
+ gradientBox.style.background = `linear-gradient(to right, ${gradientStops})`;
4755
+ };
4756
+ updateGradientDisplay();
4757
+ let currentPickerParent = null;
4758
+ const pickerContainer = document.createElement("div");
4759
+ pickerContainer.style.position = "fixed";
4760
+ pickerContainer.style.zIndex = "999999";
4761
+ pickerContainer.style.pointerEvents = "auto";
4762
+ document.body.appendChild(pickerContainer);
4763
+ const nullPickerContainer = document.createElement("div");
4764
+ nullPickerContainer.style.position = "fixed";
4765
+ nullPickerContainer.style.zIndex = "999999";
4766
+ nullPickerContainer.style.pointerEvents = "auto";
4767
+ document.body.appendChild(nullPickerContainer);
4768
+ const closeGradientPicker = () => {
4769
+ pickerContainer.style.display = "none";
4770
+ currentPickerParent = null;
4771
+ };
4772
+ const closeNullPicker = () => {
4773
+ nullPickerContainer.style.display = "none";
4774
+ };
4775
+ const nullColorColumn = document.createElement("div");
4776
+ nullColorColumn.className = "ht-null-color-column";
4777
+ const nullColorSquareContainer = document.createElement("div");
4778
+ nullColorSquareContainer.className = "ht-null-color-square-container";
4779
+ const nullSquareWrapper = document.createElement("div");
4780
+ nullSquareWrapper.style.display = "flex";
4781
+ nullSquareWrapper.style.flexDirection = "column";
4782
+ nullSquareWrapper.style.alignItems = "center";
4783
+ const nullSquare = document.createElement("div");
4784
+ nullSquare.className = "ht-null-color-square";
4785
+ nullSquare.style.backgroundColor = this.state.nullValue;
4786
+ nullSquare.title = "Click to edit missing data color";
4787
+ const nullTick = document.createElement("div");
4788
+ nullTick.className = "ht-null-color-square-tick";
4789
+ nullSquareWrapper.appendChild(nullSquare);
4790
+ nullSquareWrapper.appendChild(nullTick);
4791
+ nullColorSquareContainer.appendChild(nullSquareWrapper);
4792
+ const nullColorBox = document.createElement("div");
4793
+ nullColorBox.className = "ht-null-color-box";
4794
+ nullColorBox.style.backgroundColor = this.state.nullValue;
4795
+ const sharedPicker = new Picker({
4796
+ parent: pickerContainer,
4797
+ popup: false,
4798
+ alpha: false,
4799
+ editor: true,
4800
+ color: this.state.colorPalette[0],
4801
+ onChange: (color2) => {
4802
+ if (!currentPickerParent) return;
4803
+ const colorIndex = parseInt(currentPickerParent.getAttribute("data-color-index"));
4804
+ const hexColor = color2.hex.substring(0, 7);
4805
+ this.state.colorPalette[colorIndex] = hexColor;
4806
+ currentPickerParent.style.backgroundColor = hexColor;
4807
+ updateGradientDisplay();
4808
+ const minColor2 = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4809
+ minHandle.style.backgroundColor = minColor2;
4810
+ const maxColor2 = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4811
+ maxHandle.style.backgroundColor = maxColor2;
4812
+ this.updateScale();
4813
+ },
4814
+ onDone: () => {
4815
+ closeGradientPicker();
4816
+ }
4817
+ });
4818
+ pickerContainer.style.display = "none";
4819
+ const closePickerOnClickOutside = (e) => {
4820
+ if (!pickerContainer.contains(e.target) && !e.target.closest(".ht-color-square")) {
4821
+ closeGradientPicker();
4822
+ }
4823
+ };
4824
+ document.addEventListener("click", closePickerOnClickOutside);
4825
+ const nullColorPicker = new Picker({
4826
+ parent: nullPickerContainer,
4827
+ popup: false,
4828
+ alpha: false,
4829
+ editor: true,
4830
+ color: this.state.nullValue,
4831
+ onChange: (color2) => {
4832
+ const hexColor = color2.hex.substring(0, 7);
4833
+ this.state.nullValue = hexColor;
4834
+ nullSquare.style.backgroundColor = hexColor;
4835
+ nullColorBox.style.backgroundColor = hexColor;
4836
+ this.updateScale();
4837
+ },
4838
+ onDone: () => {
4839
+ closeNullPicker();
4840
+ }
4841
+ });
4842
+ nullPickerContainer.style.display = "none";
4843
+ const closeNullPickerOnClickOutside = (e) => {
4844
+ if (!nullPickerContainer.contains(e.target) && e.target !== nullSquare) {
4845
+ closeNullPicker();
4846
+ }
4847
+ };
4848
+ document.addEventListener("click", closeNullPickerOnClickOutside);
4849
+ container.dataset.pickerCleanup = "cleanup";
4850
+ container.cleanupFunction = () => {
4851
+ document.removeEventListener("click", closePickerOnClickOutside);
4852
+ document.removeEventListener("click", closeNullPickerOnClickOutside);
4853
+ if (pickerContainer.parentElement) {
4854
+ pickerContainer.parentElement.removeChild(pickerContainer);
4855
+ }
4856
+ if (nullPickerContainer.parentElement) {
4857
+ nullPickerContainer.parentElement.removeChild(nullPickerContainer);
4858
+ }
4859
+ };
4860
+ const recreateColorSquares = () => {
4861
+ colorSquaresContainer.innerHTML = "";
4862
+ this.state.colorPalette.forEach((color2, i) => {
4863
+ const squareContainer = createColorSquareWithTick(colorSquaresContainer, color2, i, (e) => {
4864
+ e.preventDefault();
4865
+ e.stopPropagation();
4866
+ const square = e.currentTarget;
4867
+ const colorIndex = parseInt(square.getAttribute("data-color-index"));
4868
+ closeNullPicker();
4869
+ currentPickerParent = square;
4870
+ const rect = square.getBoundingClientRect();
4871
+ sharedPicker.setColor(this.state.colorPalette[colorIndex], true);
4872
+ pickerContainer.style.left = `${rect.left}px`;
4873
+ pickerContainer.style.top = `${rect.bottom + 5}px`;
4874
+ pickerContainer.style.display = "block";
4875
+ });
4876
+ squareContainer.style.left = `${this.state.colorPositions[i] * 100}%`;
4877
+ });
4878
+ };
4879
+ recreateColorSquares();
4880
+ const rangeSliderContainer = document.createElement("div");
4881
+ rangeSliderContainer.className = "ht-range-slider-container";
4882
+ const minHandle = document.createElement("div");
4883
+ minHandle.className = "ht-range-handle";
4884
+ minHandle.style.left = `${this.state.transformMin * 100}%`;
4885
+ minHandle.title = "Drag to adjust minimum";
4886
+ let minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4887
+ minHandle.style.backgroundColor = minColor;
4888
+ const minIndicator = document.createElement("div");
4889
+ minIndicator.className = "ht-range-handle-indicator";
4890
+ minHandle.appendChild(minIndicator);
4891
+ const maxHandle = document.createElement("div");
4892
+ maxHandle.className = "ht-range-handle";
4893
+ maxHandle.style.left = `${this.state.transformMax * 100}%`;
4894
+ maxHandle.title = "Drag to adjust maximum";
4895
+ let maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4896
+ maxHandle.style.backgroundColor = maxColor;
4897
+ const maxIndicator = document.createElement("div");
4898
+ maxIndicator.className = "ht-range-handle-indicator";
4899
+ maxHandle.appendChild(maxIndicator);
4900
+ let isDraggingMin = false;
4901
+ minHandle.addEventListener("mousedown", (e) => {
4902
+ isDraggingMin = true;
4903
+ e.preventDefault();
4904
+ });
4905
+ let isDraggingMax = false;
4906
+ maxHandle.addEventListener("mousedown", (e) => {
4907
+ isDraggingMax = true;
4908
+ e.preventDefault();
4909
+ });
4910
+ const handleMouseMove = (e) => {
4911
+ if (!isDraggingMin && !isDraggingMax) return;
4912
+ const rect = rangeSliderContainer.getBoundingClientRect();
4913
+ const x = e.clientX - rect.left;
4914
+ const width = rect.width;
4915
+ let newValue = Math.max(0, Math.min(1, x / width));
4916
+ if (isDraggingMin) {
4917
+ newValue = Math.min(newValue, this.state.transformMax - 0.01);
4918
+ this.state.transformMin = newValue;
4919
+ minHandle.style.left = `${newValue * 100}%`;
4920
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, newValue);
4921
+ minHandle.style.backgroundColor = minColor;
4922
+ this.updateScale();
4923
+ } else if (isDraggingMax) {
4924
+ newValue = Math.max(newValue, this.state.transformMin + 0.01);
4925
+ this.state.transformMax = newValue;
4926
+ maxHandle.style.left = `${newValue * 100}%`;
4927
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, newValue);
4928
+ maxHandle.style.backgroundColor = maxColor;
4929
+ this.updateScale();
4930
+ }
4931
+ };
4932
+ const handleMouseUp = () => {
4933
+ isDraggingMin = false;
4934
+ isDraggingMax = false;
4935
+ };
4936
+ document.addEventListener("mousemove", handleMouseMove);
4937
+ document.addEventListener("mouseup", handleMouseUp);
4938
+ rangeSliderContainer.appendChild(minHandle);
4939
+ rangeSliderContainer.appendChild(maxHandle);
4940
+ gradientColumn.appendChild(colorSquaresContainer);
4941
+ gradientColumn.appendChild(gradientBox);
4942
+ gradientColumn.appendChild(rangeSliderContainer);
4943
+ nullSquare.addEventListener("click", (e) => {
4944
+ e.preventDefault();
4945
+ e.stopPropagation();
4946
+ closeGradientPicker();
4947
+ const rect = nullSquare.getBoundingClientRect();
4948
+ nullColorPicker.setColor(this.state.nullValue, true);
4949
+ nullPickerContainer.style.left = `${rect.left}px`;
4950
+ nullPickerContainer.style.top = `${rect.bottom + 5}px`;
4951
+ nullPickerContainer.style.display = "block";
4952
+ });
4953
+ const missingDataXContainer = document.createElement("div");
4954
+ missingDataXContainer.className = "missing-data-x-container";
4955
+ const resetIndicator = document.createElement("div");
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);
4962
+ nullColorColumn.appendChild(nullColorSquareContainer);
4963
+ nullColorColumn.appendChild(nullColorBox);
4964
+ nullColorColumn.appendChild(missingDataXContainer);
4965
+ gradientContainer.appendChild(gradientColumn);
4966
+ gradientContainer.appendChild(nullColorColumn);
4967
+ const leftButtonsContainer = document.createElement("div");
4968
+ leftButtonsContainer.className = "ht-palette-buttons-container";
4969
+ const leftPlusBtn = createPaletteButton("+", "Add color to left");
4970
+ leftPlusBtn.addEventListener("click", () => {
4971
+ const newColor = this.state.colorPalette[0];
4972
+ this.state.colorPalette.unshift(newColor);
4973
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4974
+ updateGradientDisplay();
4975
+ recreateColorSquares();
4976
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4977
+ minHandle.style.backgroundColor = minColor;
4978
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4979
+ maxHandle.style.backgroundColor = maxColor;
4980
+ this.updateScale();
4981
+ });
4982
+ const leftMinusBtn = createPaletteButton("-", "Remove color from left");
4983
+ leftMinusBtn.addEventListener("click", () => {
4984
+ if (this.state.colorPalette.length <= 2) {
4985
+ console.warn("Cannot remove color: minimum 2 colors required");
4986
+ return;
4987
+ }
4988
+ this.state.colorPalette.shift();
4989
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4990
+ updateGradientDisplay();
4991
+ recreateColorSquares();
4992
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4993
+ minHandle.style.backgroundColor = minColor;
4994
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4995
+ maxHandle.style.backgroundColor = maxColor;
4996
+ this.updateScale();
4997
+ });
4998
+ leftButtonsContainer.appendChild(leftPlusBtn);
4999
+ leftButtonsContainer.appendChild(leftMinusBtn);
5000
+ const rightButtonsContainer = document.createElement("div");
5001
+ rightButtonsContainer.className = "ht-palette-buttons-container";
5002
+ const rightPlusBtn = createPaletteButton("+", "Add color to right");
5003
+ rightPlusBtn.addEventListener("click", () => {
5004
+ const newColor = this.state.colorPalette[this.state.colorPalette.length - 1];
5005
+ this.state.colorPalette.push(newColor);
5006
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
5007
+ updateGradientDisplay();
5008
+ recreateColorSquares();
5009
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
5010
+ minHandle.style.backgroundColor = minColor;
5011
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
5012
+ maxHandle.style.backgroundColor = maxColor;
5013
+ this.updateScale();
5014
+ });
5015
+ const rightMinusBtn = createPaletteButton("-", "Remove color from right");
5016
+ rightMinusBtn.addEventListener("click", () => {
5017
+ if (this.state.colorPalette.length <= 2) {
5018
+ console.warn("Cannot remove color: minimum 2 colors required");
5019
+ return;
5020
+ }
5021
+ this.state.colorPalette.pop();
5022
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
5023
+ updateGradientDisplay();
5024
+ recreateColorSquares();
5025
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
5026
+ minHandle.style.backgroundColor = minColor;
5027
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
5028
+ maxHandle.style.backgroundColor = maxColor;
5029
+ this.updateScale();
5030
+ });
5031
+ rightButtonsContainer.appendChild(rightPlusBtn);
5032
+ rightButtonsContainer.appendChild(rightMinusBtn);
5033
+ container.appendChild(leftButtonsContainer);
5034
+ container.appendChild(gradientContainer);
5035
+ container.appendChild(rightButtonsContainer);
5036
+ return container;
3705
5037
  }
3706
5038
  }
5039
+ function createPaletteButton(text, title) {
5040
+ const button = document.createElement("button");
5041
+ button.className = "ht-palette-button";
5042
+ button.textContent = text;
5043
+ button.title = title;
5044
+ return button;
5045
+ }
5046
+ function interpolateGradient(colors, positions, t) {
5047
+ t = Math.max(0, Math.min(1, t));
5048
+ if (colors.length === 1) {
5049
+ return colors[0];
5050
+ }
5051
+ let i = 0;
5052
+ while (i < positions.length - 1 && t > positions[i + 1]) {
5053
+ i++;
5054
+ }
5055
+ if (t === positions[i]) {
5056
+ return colors[i];
5057
+ }
5058
+ if (i === positions.length - 1) {
5059
+ return colors[i];
5060
+ }
5061
+ const t1 = positions[i];
5062
+ const t2 = positions[i + 1];
5063
+ const localT = (t - t1) / (t2 - t1);
5064
+ const color1 = hexToRgb(colors[i]);
5065
+ const color2 = hexToRgb(colors[i + 1]);
5066
+ const r = Math.round(color1.r + (color2.r - color1.r) * localT);
5067
+ const g = Math.round(color1.g + (color2.g - color1.g) * localT);
5068
+ const b = Math.round(color1.b + (color2.b - color1.b) * localT);
5069
+ return rgbToHex(r, g, b);
5070
+ }
5071
+ function hexToRgb(hex2) {
5072
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex2);
5073
+ return result ? {
5074
+ r: parseInt(result[1], 16),
5075
+ g: parseInt(result[2], 16),
5076
+ b: parseInt(result[3], 16)
5077
+ } : { r: 0, g: 0, b: 0 };
5078
+ }
5079
+ function rgbToHex(r, g, b) {
5080
+ return "#" + [r, g, b].map((x) => {
5081
+ const hex2 = x.toString(16);
5082
+ return hex2.length === 1 ? "0" + hex2 : hex2;
5083
+ }).join("");
5084
+ }
5085
+ function createColorSquareWithTick(parent, color2, colorIndex, clickHandler) {
5086
+ const squareContainer = document.createElement("div");
5087
+ squareContainer.className = "ht-color-square-wrapper";
5088
+ const square = document.createElement("div");
5089
+ square.className = "ht-color-square";
5090
+ square.style.backgroundColor = color2;
5091
+ square.title = "Click to edit color";
5092
+ square.setAttribute("data-color-index", colorIndex);
5093
+ square.addEventListener("click", clickHandler);
5094
+ const tick = document.createElement("div");
5095
+ tick.className = "ht-color-square-tick";
5096
+ squareContainer.appendChild(square);
5097
+ squareContainer.appendChild(tick);
5098
+ parent.appendChild(squareContainer);
5099
+ return squareContainer;
5100
+ }
3707
5101
  class TreeData extends Subscribable {
3708
5102
  tree;
3709
5103
  metadata = /* @__PURE__ */ new Map();
5104
+ // Map of table ID to array of row objects
3710
5105
  metadataTableNames = /* @__PURE__ */ new Map();
3711
5106
  // Map of table ID to display name
3712
5107
  columnType = /* @__PURE__ */ new Map();
@@ -3717,24 +5112,74 @@ class TreeData extends Subscribable {
3717
5112
  // Display-friendly column name, keyed by unique column ID
3718
5113
  columnAesthetic = /* @__PURE__ */ new Map();
3719
5114
  // Map of columnId -> Map of aestheticId -> Aesthetic
5115
+ nodeIdColumn = /* @__PURE__ */ new Map();
5116
+ // Map of table ID to the column name used for node IDs
5117
+ validIdColumns = /* @__PURE__ */ new Map();
5118
+ // Map of table ID to array of column names that contain valid node IDs
3720
5119
  #nextTableId = 0;
3721
- constructor(newickStr, metadataTables = [], metadataTableNames = []) {
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 = []) {
3722
5127
  super();
3723
- this.tree = this.parseTree(newickStr);
5128
+ this.tree = this.createHierarchy(treeDataObj);
3724
5129
  if (Array.isArray(metadataTables)) {
3725
5130
  metadataTables.forEach((tableStr, index) => {
3726
- const tableName = metadataTableNames[index] || `Metadata ${index + 1}`;
3727
- this.addTable(tableStr, tableName);
5131
+ this.addTable(tableStr, metadataTableNames[index]);
3728
5132
  });
3729
5133
  }
3730
5134
  }
3731
5135
  /**
3732
- * Parse a Newick string and create a hierarchy
3733
- * @param {string} newickStr - Newick formatted 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
3734
5180
  * @returns {object} D3 hierarchy object
3735
5181
  */
3736
- parseTree(newickStr) {
3737
- const treeData = parseNewick(newickStr);
5182
+ createHierarchy(treeData) {
3738
5183
  const root2 = hierarchy(treeData, (d) => d.children).sum((d) => d.children ? 0 : 1).each(function(d) {
3739
5184
  d.leafCount = d.value;
3740
5185
  delete d.value;
@@ -3748,11 +5193,11 @@ class TreeData extends Subscribable {
3748
5193
  }
3749
5194
  /**
3750
5195
  * Parse and set the tree data
3751
- * @param {string} newickStr - Newick formatted string or path
5196
+ * @param {Object} treeDataObj - Parsed tree object
3752
5197
  */
3753
- setTree(newickStr) {
3754
- this.tree = this.parseTree(newickStr);
3755
- this.metadata.keys().forEach(this.#attachTable);
5198
+ setTree(treeDataObj) {
5199
+ this.tree = this.createHierarchy(treeDataObj);
5200
+ this.metadata.keys().forEach((tableId) => this.#attachTable(tableId));
3756
5201
  this.notify("treeUpdated", this);
3757
5202
  }
3758
5203
  /**
@@ -3762,6 +5207,39 @@ class TreeData extends Subscribable {
3762
5207
  getMetadataTableNames() {
3763
5208
  return Array.from(this.metadataTableNames.values());
3764
5209
  }
5210
+ /**
5211
+ * Get all node names from the tree
5212
+ * @returns {Set<string>} Set of all node names in the tree
5213
+ */
5214
+ getTreeNodeNames() {
5215
+ const nodeNames = /* @__PURE__ */ new Set();
5216
+ this.tree.each((d) => {
5217
+ if (d.data.name) {
5218
+ nodeNames.add(d.data.name);
5219
+ }
5220
+ });
5221
+ return nodeNames;
5222
+ }
5223
+ /**
5224
+ * Generate a Map from node ID to row data for a given table
5225
+ * @param {string} tableId - ID of the table
5226
+ * @returns {Map} Map from node ID to row data
5227
+ */
5228
+ #generateMetadataMap(tableId) {
5229
+ const rows = this.metadata.get(tableId);
5230
+ const idColumn = this.nodeIdColumn.get(tableId);
5231
+ if (!rows || !idColumn) {
5232
+ return /* @__PURE__ */ new Map();
5233
+ }
5234
+ const metadataMap = /* @__PURE__ */ new Map();
5235
+ for (const row of rows) {
5236
+ const nodeId = row[idColumn];
5237
+ if (nodeId) {
5238
+ metadataMap.set(nodeId, row);
5239
+ }
5240
+ }
5241
+ return metadataMap;
5242
+ }
3765
5243
  /**
3766
5244
  * Add a metadata table
3767
5245
  * @param {string} tableStr - TSV formatted string or path
@@ -3770,22 +5248,29 @@ class TreeData extends Subscribable {
3770
5248
  * @returns {string} The table ID
3771
5249
  */
3772
5250
  addTable(tableStr, tableName = null, sep = " ") {
3773
- const { metadataMap, columnTypes } = parseTable(tableStr, sep);
3774
- const id2 = `table_${this.#nextTableId++}`;
5251
+ let { metadataMap, columnTypes, idColumns } = parseTable(tableStr, this.getTreeNodeNames(), sep);
5252
+ const tableId = `table_${this.#nextTableId++}`;
3775
5253
  if (!tableName) {
3776
5254
  tableName = `Metadata ${this.#nextTableId}`;
3777
5255
  }
3778
- this.metadataTableNames.set(id2, tableName);
5256
+ this.metadataTableNames.set(tableId, tableName);
3779
5257
  const columnIdMap = /* @__PURE__ */ new Map();
3780
5258
  for (const [originalName, columnType] of columnTypes) {
3781
- const uniqueId = `${id2}_${originalName}`;
5259
+ const uniqueId = `${tableId}_${originalName}`;
3782
5260
  columnIdMap.set(originalName, uniqueId);
3783
5261
  this.columnType.set(uniqueId, columnType);
3784
5262
  this.columnName.set(uniqueId, originalName);
3785
5263
  this.columnDisplayName.set(uniqueId, columnToHeader(originalName));
3786
5264
  }
3787
- const transformedMetadata = /* @__PURE__ */ new Map();
3788
- for (const [nodeName, nodeData] of metadataMap) {
5265
+ idColumns = idColumns.map((x) => columnIdMap.get(x));
5266
+ let selectedIdColumn = null;
5267
+ if (idColumns.length > 0) {
5268
+ selectedIdColumn = idColumns[0];
5269
+ } else {
5270
+ console.warn(`No valid node ID column found in table ${tableName}`);
5271
+ }
5272
+ const metadataArray = [];
5273
+ for (const nodeData of metadataMap.values()) {
3789
5274
  const transformedNodeData = {};
3790
5275
  for (const [originalColumnName, value] of Object.entries(nodeData)) {
3791
5276
  const uniqueId = columnIdMap.get(originalColumnName);
@@ -3793,15 +5278,79 @@ class TreeData extends Subscribable {
3793
5278
  transformedNodeData[uniqueId] = value;
3794
5279
  }
3795
5280
  }
3796
- transformedMetadata.set(nodeName, transformedNodeData);
5281
+ metadataArray.push(transformedNodeData);
3797
5282
  }
3798
- this.metadata.set(id2, transformedMetadata);
3799
- this.#attachTable(id2);
5283
+ this.validIdColumns.set(tableId, idColumns);
5284
+ this.nodeIdColumn.set(tableId, selectedIdColumn);
5285
+ this.metadata.set(tableId, metadataArray);
5286
+ this.#attachTable(tableId);
3800
5287
  this.notify("metadataAdded", {
3801
- tableId: id2,
3802
- columnIds: columnIdMap.values()
5288
+ tableId,
5289
+ columnIds: Array.from(columnIdMap.values())
5290
+ });
5291
+ return tableId;
5292
+ }
5293
+ /**
5294
+ * Get valid ID columns for a table
5295
+ * @param {string} tableId - ID of the table
5296
+ * @returns {Array<string>} Array of column names that contain valid node IDs
5297
+ */
5298
+ getValidIdColumns(tableId) {
5299
+ return this.validIdColumns.get(tableId) || [];
5300
+ }
5301
+ /**
5302
+ * Get the current node ID column for a table
5303
+ * @param {string} tableId - ID of the table
5304
+ * @returns {string|null} Column name used as node ID, or null if none
5305
+ */
5306
+ getNodeIdColumn(tableId) {
5307
+ return this.nodeIdColumn.get(tableId);
5308
+ }
5309
+ /**
5310
+ * Get all column IDs for a table
5311
+ * @param {string} tableId - ID of the table
5312
+ * @returns {Array<string>} Array of column IDs in the table
5313
+ */
5314
+ getTableColumnIds(tableId) {
5315
+ const table = this.metadata.get(tableId);
5316
+ if (!table || table.length === 0) {
5317
+ return [];
5318
+ }
5319
+ return Object.keys(table[0]);
5320
+ }
5321
+ /**
5322
+ * Change the node ID column for a table
5323
+ * @param {string} tableId - ID of the table
5324
+ * @param {string} newIdColumnName - Name of the new ID column to use
5325
+ */
5326
+ setNodeIdColumn(tableId, newIdColumnName) {
5327
+ const table = this.metadata.get(tableId);
5328
+ if (!table) {
5329
+ console.warn(`Table ${tableId} does not exist`);
5330
+ return;
5331
+ }
5332
+ if (!this.validIdColumns.get(tableId).includes(newIdColumnName)) {
5333
+ console.warn(`Column ${newIdColumnName} is not a valid ID column for table ${tableId}`);
5334
+ return;
5335
+ }
5336
+ const oldIdColumn = this.nodeIdColumn.get(tableId);
5337
+ if (oldIdColumn === newIdColumnName) {
5338
+ return;
5339
+ }
5340
+ const columnIds = this.getTableColumnIds(tableId);
5341
+ for (const columnId of columnIds) {
5342
+ this.columnAesthetic.delete(columnId);
5343
+ }
5344
+ this.#detachTable(tableId);
5345
+ this.nodeIdColumn.set(tableId, newIdColumnName);
5346
+ this.#attachTable(tableId);
5347
+ this.notify("metadataChanged", {
5348
+ tableId,
5349
+ oldIdColumn,
5350
+ newIdColumn: newIdColumnName,
5351
+ columnIds,
5352
+ requiresAestheticRefresh: true
3803
5353
  });
3804
- return id2;
3805
5354
  }
3806
5355
  /**
3807
5356
  * Remove a metadata table
@@ -3813,7 +5362,7 @@ class TreeData extends Subscribable {
3813
5362
  console.warn(`Table ${tableId} does not exist`);
3814
5363
  return;
3815
5364
  }
3816
- const keys = Object.keys(table.values().next().value);
5365
+ const keys = table.length > 0 ? Object.keys(table[0]) : [];
3817
5366
  for (const uniqueId of keys) {
3818
5367
  this.columnType.delete(uniqueId);
3819
5368
  this.columnName.delete(uniqueId);
@@ -3823,7 +5372,9 @@ class TreeData extends Subscribable {
3823
5372
  this.#detachTable(tableId);
3824
5373
  this.metadata.delete(tableId);
3825
5374
  this.metadataTableNames.delete(tableId);
3826
- this.notify("metadataRemoved", {
5375
+ this.nodeIdColumn.delete(tableId);
5376
+ this.validIdColumns.delete(tableId);
5377
+ this.notify("metadataChanged", {
3827
5378
  tableId,
3828
5379
  columnIds: keys
3829
5380
  });
@@ -3874,15 +5425,26 @@ class TreeData extends Subscribable {
3874
5425
  }
3875
5426
  let values = [];
3876
5427
  this.tree.each((node) => {
3877
- if (node.metadata && node.metadata[columnId] !== void 0) {
5428
+ if (state.subset == "tips" && node.children) {
5429
+ return;
5430
+ }
5431
+ if (node.metadata) {
3878
5432
  values.push(node.metadata[columnId]);
5433
+ } else {
5434
+ values.push(void 0);
3879
5435
  }
3880
5436
  });
3881
5437
  if (values.length === 0) {
3882
5438
  console.error(`No values found for column ${columnId}`);
3883
5439
  }
3884
5440
  const displayName = this.columnDisplayName.get(columnId) || columnId;
5441
+ let scaleType = state.scaleType;
5442
+ if (!scaleType) {
5443
+ scaleType = "color";
5444
+ }
3885
5445
  const aesthetic = new Aesthetic(values, {
5446
+ scaleType,
5447
+ default: state.default !== void 0 ? state.default : isCategorical ? null : 0,
3886
5448
  isCategorical,
3887
5449
  inputUnits: displayName,
3888
5450
  ...state
@@ -3893,11 +5455,11 @@ class TreeData extends Subscribable {
3893
5455
  * Add metadata to tree nodes
3894
5456
  */
3895
5457
  #attachTable(tableId) {
3896
- const table = this.metadata.get(tableId);
5458
+ const metadataMap = this.#generateMetadataMap(tableId);
3897
5459
  this.tree.each((d) => {
3898
5460
  const nodeName = d.data.name;
3899
- if (nodeName && table.has(nodeName)) {
3900
- const tableMetadata = table.get(nodeName);
5461
+ if (nodeName && metadataMap.has(nodeName)) {
5462
+ const tableMetadata = metadataMap.get(nodeName);
3901
5463
  d.metadata = { ...d.metadata, ...tableMetadata };
3902
5464
  }
3903
5465
  });
@@ -3907,11 +5469,16 @@ class TreeData extends Subscribable {
3907
5469
  */
3908
5470
  #detachTable(tableId) {
3909
5471
  const table = this.metadata.get(tableId);
3910
- const keys = Object.keys(table.values().next().value);
5472
+ if (!table || table.length === 0) {
5473
+ return;
5474
+ }
5475
+ const keys = Object.keys(table[0]);
3911
5476
  this.tree.each((d) => {
3912
- keys.forEach((key) => {
3913
- delete d[key];
3914
- });
5477
+ if (d.metadata) {
5478
+ keys.forEach((key) => {
5479
+ delete d.metadata[key];
5480
+ });
5481
+ }
3915
5482
  });
3916
5483
  }
3917
5484
  }
@@ -4066,7 +5633,7 @@ function calculateCircularScalingFactors(root2, options) {
4066
5633
  }
4067
5634
  const minLabelScale = Math.min(...leafData.map((a) => a.labelScale));
4068
5635
  const maxBranchX = Math.max(...leafData.map((a) => a.radius));
4069
- const minBranchX = Math.min(...leafData.map((a) => a.radius));
5636
+ const meanBranchX = leafData.reduce((sum, x) => sum + x.radius, 0) / leafData.length;
4070
5637
  const nonZeroBranches = root2.descendants().filter((a) => a.data.length > 0 && a.children);
4071
5638
  const minBranchLength = nonZeroBranches.length > 0 ? Math.min(...nonZeroBranches.map((a) => a.data.length)) : Infinity;
4072
5639
  applyLabelMin(options.minFontPx / minLabelScale);
@@ -4077,6 +5644,15 @@ function calculateCircularScalingFactors(root2, options) {
4077
5644
  ))
4078
5645
  );
4079
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
+ }
4080
5656
  function applyBranchViewConstraint() {
4081
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)));
4082
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)));
@@ -4099,18 +5675,6 @@ function calculateCircularScalingFactors(root2, options) {
4099
5675
  }
4100
5676
  applyBranchViewConstraint();
4101
5677
  applyLabelViewConstraint();
4102
- const totalAnnotationHeight = leafData.reduce((sum, a) => sum + a.height, 0);
4103
- if (totalAnnotationHeight > 0 && minBranchX > 0) {
4104
- if (branchLenToPxFactor_min !== branchLenToPxFactor_max) {
4105
- applyBranchMin(totalAnnotationHeight * labelSizeToPxFactor_min / (minBranchX * 2 * Math.PI));
4106
- }
4107
- if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
4108
- applyLabelMax(maxBranchX * branchLenToPxFactor_max * 2 * Math.PI / totalAnnotationHeight);
4109
- }
4110
- }
4111
- if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
4112
- applyLabelViewConstraint();
4113
- }
4114
5678
  if (labelSizeToPxFactor_min !== labelSizeToPxFactor_max) {
4115
5679
  applyLabelMin(options.idealFontPx / minLabelScale);
4116
5680
  }
@@ -4208,57 +5772,71 @@ class TreeState extends Subscribable {
4208
5772
  title: "Tip label text",
4209
5773
  scaleType: "identity",
4210
5774
  default: "",
5775
+ nullValue: "",
4211
5776
  downstream: ["updateTipLabelText", "updateCoordinates"],
4212
- hasLegend: false
5777
+ hasLegend: false,
5778
+ subset: "tips"
4213
5779
  },
4214
5780
  tipLabelColor: {
4215
5781
  title: "Tip label color",
4216
5782
  scaleType: "color",
4217
5783
  default: "#000000",
5784
+ nullValue: "#808080",
4218
5785
  otherCategory: "#555555",
4219
5786
  downstream: [],
4220
- hasLegend: true
5787
+ hasLegend: true,
5788
+ subset: "tips"
4221
5789
  },
4222
5790
  tipLabelSize: {
4223
5791
  title: "Tip label size",
4224
5792
  scaleType: "size",
4225
5793
  default: 1,
5794
+ nullValue: 1,
4226
5795
  isCategorical: false,
4227
5796
  outputRange: [0.5, 2],
4228
5797
  downstream: ["updateCoordinates"],
4229
- hasLegend: true
5798
+ hasLegend: true,
5799
+ subset: "tips"
4230
5800
  },
4231
5801
  tipLabelFont: {
4232
5802
  title: "Tip label font",
4233
5803
  scaleType: "identity",
4234
5804
  default: "sans-serif",
5805
+ nullValue: "sans-serif",
4235
5806
  downstream: ["updateCoordinates"],
4236
- hasLegend: false
5807
+ hasLegend: false,
5808
+ subset: "tips"
4237
5809
  },
4238
5810
  tipLabelStyle: {
4239
5811
  title: "Tip label font style",
4240
5812
  scaleType: "text",
4241
5813
  outputValues: ["normal", "bold", "italic", "bold italic"],
4242
5814
  default: "normal",
5815
+ nullValue: "normal",
4243
5816
  otherCategory: "italic",
4244
5817
  downstream: ["updateCoordinates"],
4245
- hasLegend: false
5818
+ hasLegend: false,
5819
+ subset: "tips"
4246
5820
  },
4247
5821
  nodeLabelText: {
4248
5822
  title: "Node label text",
4249
5823
  scaleType: "identity",
4250
5824
  default: "",
5825
+ nullValue: "",
4251
5826
  downstream: ["updateNodeLabelText"],
4252
- hasLegend: false
5827
+ hasLegend: false,
5828
+ subset: "all"
4253
5829
  },
4254
5830
  nodeLabelSize: {
4255
5831
  title: "Node label size",
4256
5832
  scaleType: "size",
4257
5833
  default: 1,
5834
+ nullValue: 1,
4258
5835
  isCategorical: false,
4259
5836
  outputRange: [0.5, 2],
4260
5837
  downstream: ["updateCoordinates"],
4261
- hasLegend: false
5838
+ hasLegend: false,
5839
+ subset: "all"
4262
5840
  }
4263
5841
  };
4264
5842
  state = {
@@ -4306,8 +5884,22 @@ class TreeState extends Subscribable {
4306
5884
  this.state.treeData.subscribe("treeUpdate", () => {
4307
5885
  this.#initalize();
4308
5886
  });
4309
- this.state.treeData.subscribe("metadataRemoved", (info) => {
4310
- this.setAesthetics(Object.fromEntries(info.columnIds.map((key) => [key, void 0])));
5887
+ this.state.treeData.subscribe("metadataChanged", (info) => {
5888
+ if (info.columnIds && Array.isArray(info.columnIds)) {
5889
+ if (info.requiresAestheticRefresh) {
5890
+ const aestheticsToRefresh = {};
5891
+ for (const [aestheticId, columnId] of Object.entries(this.state.aesthetics)) {
5892
+ if (columnId && info.columnIds.includes(columnId)) {
5893
+ aestheticsToRefresh[aestheticId] = columnId;
5894
+ }
5895
+ }
5896
+ if (Object.keys(aestheticsToRefresh).length > 0) {
5897
+ this.setAesthetics(aestheticsToRefresh, true);
5898
+ }
5899
+ } else {
5900
+ this.setAesthetics(Object.fromEntries(info.columnIds.map((key) => [key, void 0])));
5901
+ }
5902
+ }
4311
5903
  });
4312
5904
  }
4313
5905
  #initalize() {
@@ -4357,21 +5949,16 @@ class TreeState extends Subscribable {
4357
5949
  if (force || columnId !== this.state.aesthetics[aestheticId]) {
4358
5950
  this.state.aesthetics[aestheticId] = columnId;
4359
5951
  if (!columnId) {
4360
- this.aestheticsScales[aestheticId] = new NullScale(aesData.default);
5952
+ this.aestheticsScales[aestheticId] = new NullScale({ default: aesData.default });
4361
5953
  } else {
4362
5954
  this.aestheticsScales[aestheticId] = this.state.treeData.getAesthetic(columnId, aestheticId, aesData);
5955
+ this.aestheticsScales[aestheticId].subscribe("aestheticChange", () => {
5956
+ this.#updateTreeDataForAesthetic(aestheticId, columnId);
5957
+ this.#updateLegends();
5958
+ this.notify(`${aestheticId}Change`);
5959
+ });
4363
5960
  }
4364
- this.state.treeData.tree.each((d) => {
4365
- if (columnId && columnId !== null && columnId !== void 0) {
4366
- if (d.metadata && d.metadata[columnId] !== void 0) {
4367
- d[aestheticId] = this.aestheticsScales[aestheticId].getValue(d.metadata[columnId]);
4368
- } else {
4369
- d[aestheticId] = aesData.default;
4370
- }
4371
- } else {
4372
- d[aestheticId] = this.aestheticsScales[aestheticId].getValue();
4373
- }
4374
- });
5961
+ this.#updateTreeDataForAesthetic(aestheticId, columnId);
4375
5962
  for (const methodName of aesData.downstream) {
4376
5963
  downstreams.add(methodName);
4377
5964
  }
@@ -4388,6 +5975,24 @@ class TreeState extends Subscribable {
4388
5975
  this[methodName]();
4389
5976
  }
4390
5977
  }
5978
+ /**
5979
+ * Update tree data for a specific aesthetic
5980
+ * @private
5981
+ */
5982
+ #updateTreeDataForAesthetic(aestheticId, columnId) {
5983
+ const aesData = this.#AESTHETICS[aestheticId];
5984
+ this.state.treeData.tree.each((d) => {
5985
+ if (columnId && columnId !== null && columnId !== void 0) {
5986
+ if (d.metadata) {
5987
+ d[aestheticId] = this.aestheticsScales[aestheticId].getValue(d.metadata[columnId]);
5988
+ } else {
5989
+ d[aestheticId] = aesData.default;
5990
+ }
5991
+ } else {
5992
+ d[aestheticId] = this.aestheticsScales[aestheticId].getValue();
5993
+ }
5994
+ });
5995
+ }
4391
5996
  #updateLegends() {
4392
5997
  this.legends = [];
4393
5998
  for (const [aestheticId, columnId] of Object.entries(this.state.aesthetics)) {
@@ -4727,6 +6332,10 @@ const ICONS = {
4727
6332
  "m 17,4 c 0,0 9,8 0,16",
4728
6333
  "M 17,8 V 4 h 4",
4729
6334
  "M 21,20 H 17 V 16"
6335
+ ],
6336
+ edit: [
6337
+ "m 12,8 -8,8 -1,5 5,-1 8,-8 z",
6338
+ "M 21,7 18,10 14,6 17,3 Z"
4730
6339
  ]
4731
6340
  };
4732
6341
  function appendIcon(svgSel, name, size, padding = 2) {
@@ -4899,8 +6508,8 @@ class TextSizeLegend extends LegendBase {
4899
6508
  updateCoordinates() {
4900
6509
  const minValue = this.state.aesthetic.scale.dataMin;
4901
6510
  const maxValue = this.state.aesthetic.scale.dataMax;
4902
- const minSize = this.state.aesthetic.scale.sizeMin;
4903
- const maxSize = this.state.aesthetic.scale.sizeMax;
6511
+ const minSize = this.state.aesthetic.state.outputRange[0];
6512
+ const maxSize = this.state.aesthetic.state.outputRange[1];
4904
6513
  const ticks = generateNiceTicks(minValue, maxValue, 5);
4905
6514
  const maxLetterFont = maxSize * this.state.treeState.labelSizeToPxFactor * 0.7;
4906
6515
  const minLetterFont = minSize * this.state.treeState.labelSizeToPxFactor * 0.7;
@@ -4939,8 +6548,8 @@ class TextSizeLegend extends LegendBase {
4939
6548
  text: this.state.aesthetic.state.inputUnits || ""
4940
6549
  }
4941
6550
  };
4942
- ticks.forEach((tickValue, i) => {
4943
- const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6551
+ if (minValue === maxValue) {
6552
+ const x = leftOverhang + baseWidth / 2;
4944
6553
  this.coordinates.ticks.push({
4945
6554
  x1: x,
4946
6555
  y1: rampBaseY,
@@ -4950,15 +6559,37 @@ class TextSizeLegend extends LegendBase {
4950
6559
  this.coordinates.labels.push({
4951
6560
  x,
4952
6561
  y: rampBaseY + this.tickHeight,
4953
- text: formatTickLabel(tickValue, ticks)
6562
+ text: formatTickLabel(minValue, ticks)
4954
6563
  });
4955
- });
4956
- this.coordinates.polygon = [
4957
- { x: leftOverhang, y: rampBaseY },
4958
- { x: leftOverhang, y: rampBaseY - minLetterFont },
4959
- { x: leftOverhang + baseWidth, y: rampBaseY - maxLetterFont },
4960
- { x: leftOverhang + baseWidth, y: rampBaseY }
4961
- ];
6564
+ const avgLetterFont = (minLetterFont + maxLetterFont) / 2;
6565
+ this.coordinates.polygon = [
6566
+ { x: leftOverhang, y: rampBaseY },
6567
+ { x: leftOverhang, y: rampBaseY - avgLetterFont },
6568
+ { x: leftOverhang + baseWidth, y: rampBaseY - avgLetterFont },
6569
+ { x: leftOverhang + baseWidth, y: rampBaseY }
6570
+ ];
6571
+ } else {
6572
+ ticks.forEach((tickValue, i) => {
6573
+ const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6574
+ this.coordinates.ticks.push({
6575
+ x1: x,
6576
+ y1: rampBaseY,
6577
+ x2: x,
6578
+ y2: rampBaseY + this.tickHeight
6579
+ });
6580
+ this.coordinates.labels.push({
6581
+ x,
6582
+ y: rampBaseY + this.tickHeight,
6583
+ text: formatTickLabel(tickValue, ticks)
6584
+ });
6585
+ });
6586
+ this.coordinates.polygon = [
6587
+ { x: leftOverhang, y: rampBaseY },
6588
+ { x: leftOverhang, y: rampBaseY - minLetterFont },
6589
+ { x: leftOverhang + baseWidth, y: rampBaseY - maxLetterFont },
6590
+ { x: leftOverhang + baseWidth, y: rampBaseY }
6591
+ ];
6592
+ }
4962
6593
  }
4963
6594
  /**
4964
6595
  * Render the legend in the specified SVG element
@@ -5035,9 +6666,11 @@ class TextColorLegend extends LegendBase {
5035
6666
  let currentX = 0;
5036
6667
  let currentY = titleHeightOffset + this.verticalSpacing + this.squareSize / 2;
5037
6668
  let rowHeight = this.squareSize;
5038
- categories.slice(0, aesthetic.scale.maxColors).forEach((category, i) => {
6669
+ const categoriesToShow = categories.slice(0, aesthetic.state.maxCategories);
6670
+ categoriesToShow.forEach((category, i) => {
5039
6671
  const color2 = aesthetic.scale.getValue(category);
5040
- const labelSize = this.textSizeEstimator.getTextSize(category, this.state.labelFontSize);
6672
+ const labelText = category === "" ? "No data" : category;
6673
+ const labelSize = this.textSizeEstimator.getTextSize(labelText, this.state.labelFontSize);
5041
6674
  const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
5042
6675
  if (currentX > 0 && currentX + itemWidth > maxWidth) {
5043
6676
  currentX = 0;
@@ -5048,7 +6681,7 @@ class TextColorLegend extends LegendBase {
5048
6681
  x: currentX,
5049
6682
  y: currentY,
5050
6683
  color: color2,
5051
- label: i < aesthetic.scale.maxColors - 1 ? category : aesthetic.state.otherLabel,
6684
+ label: i < aesthetic.state.maxCategories - 1 || i == categories.length - 1 ? labelText : aesthetic.state.otherLabel,
5052
6685
  squareX: currentX,
5053
6686
  squareY: currentY - this.squareSize / 2,
5054
6687
  labelX: currentX + this.squareSize + this.itemLabelGap,
@@ -5057,6 +6690,30 @@ class TextColorLegend extends LegendBase {
5057
6690
  currentX += itemWidth + this.itemGap;
5058
6691
  this.coordinates.width = Math.max(this.coordinates.width, currentX - this.itemGap);
5059
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
+ }
5060
6717
  this.coordinates.height = currentY + this.squareSize / 2;
5061
6718
  }
5062
6719
  /**
@@ -5080,7 +6737,7 @@ class TextColorLegend extends LegendBase {
5080
6737
  const ticksY = gradientY + this.gradientHeight;
5081
6738
  const labelsY = ticksY + this.tickHeight;
5082
6739
  const unitsSize = this.textSizeEstimator.getTextSize(aesthetic.state.inputUnits || "", this.state.labelFontSize);
5083
- const height = labelsY + this.state.labelFontSize + unitsSize.heightPx;
6740
+ let height = labelsY + this.state.labelFontSize + unitsSize.heightPx;
5084
6741
  this.coordinates = {
5085
6742
  width,
5086
6743
  height,
@@ -5103,10 +6760,11 @@ class TextColorLegend extends LegendBase {
5103
6760
  x: width / 2,
5104
6761
  y: height,
5105
6762
  text: aesthetic.state.inputUnits || ""
5106
- }
6763
+ },
6764
+ nullItem: null
5107
6765
  };
5108
- ticks.forEach((tickValue, i) => {
5109
- const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6766
+ if (minValue === maxValue) {
6767
+ const x = leftOverhang + baseWidth / 2;
5110
6768
  this.coordinates.ticks.push({
5111
6769
  x1: x,
5112
6770
  y1: ticksY,
@@ -5116,9 +6774,41 @@ class TextColorLegend extends LegendBase {
5116
6774
  this.coordinates.labels.push({
5117
6775
  x,
5118
6776
  y: labelsY,
5119
- text: formatTickLabel(tickValue, ticks)
6777
+ text: formatTickLabel(minValue, ticks)
5120
6778
  });
5121
- });
6779
+ } else {
6780
+ ticks.forEach((tickValue, i) => {
6781
+ const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6782
+ this.coordinates.ticks.push({
6783
+ x1: x,
6784
+ y1: ticksY,
6785
+ x2: x,
6786
+ y2: ticksY + this.tickHeight
6787
+ });
6788
+ this.coordinates.labels.push({
6789
+ x,
6790
+ y: labelsY,
6791
+ text: formatTickLabel(tickValue, ticks)
6792
+ });
6793
+ });
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
+ }
5122
6812
  }
5123
6813
  /**
5124
6814
  * Render the legend in the specified SVG element
@@ -5154,11 +6844,17 @@ class TextColorLegend extends LegendBase {
5154
6844
  const gradientId = `color-gradient-${Math.random().toString(36).substr(2, 9)}`;
5155
6845
  const defs = this.group.append("defs");
5156
6846
  const gradient = defs.append("linearGradient").attr("id", gradientId).attr("x1", "0%").attr("x2", "100%").attr("y1", "0%").attr("y2", "0%");
5157
- for (let i = 0; i <= this.numGradientStops; i++) {
5158
- const t = i / this.numGradientStops;
5159
- const value = minValue + t * (maxValue - minValue);
5160
- const color2 = aesthetic.scale.getValue(value);
5161
- gradient.append("stop").attr("offset", `${t * 100}%`).attr("stop-color", color2);
6847
+ if (minValue === maxValue) {
6848
+ const color2 = aesthetic.scale.getValue(minValue);
6849
+ gradient.append("stop").attr("offset", "0%").attr("stop-color", color2);
6850
+ gradient.append("stop").attr("offset", "100%").attr("stop-color", color2);
6851
+ } else {
6852
+ for (let i = 0; i <= this.numGradientStops; i++) {
6853
+ const t = i / this.numGradientStops;
6854
+ const value = minValue + t * (maxValue - minValue);
6855
+ const color2 = aesthetic.scale.getValue(value);
6856
+ gradient.append("stop").attr("offset", `${t * 100}%`).attr("stop-color", color2);
6857
+ }
5162
6858
  }
5163
6859
  this.group.append("rect").attr("x", this.coordinates.gradient.x).attr("y", this.coordinates.gradient.y).attr("width", this.coordinates.gradient.width).attr("height", this.coordinates.gradient.height).style("fill", `url(#${gradientId})`).style("stroke", "#000").style("stroke-width", 1);
5164
6860
  this.coordinates.ticks.forEach((tick) => {
@@ -6265,9 +7961,10 @@ function convertSvgToPng(svgString, width, height, filename) {
6265
7961
  img.src = url;
6266
7962
  }
6267
7963
  function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCurrentTreeView, switchToTree, addNewTree, options) {
6268
- const CONTROL_HEIGHT = 24;
7964
+ const CONTROL_HEIGHT = 20;
6269
7965
  let currentTab = null;
6270
7966
  let selectedMetadata = null;
7967
+ let currentAestheticSettings = null;
6271
7968
  let expandSubtreesBtn = null;
6272
7969
  let expandRootBtn = null;
6273
7970
  let showHiddenBtn = null;
@@ -6295,6 +7992,8 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6295
7992
  tabsContainer.className = "ht-tabs";
6296
7993
  const controlsContainer = document.createElement("div");
6297
7994
  controlsContainer.className = "ht-controls hidden";
7995
+ const aestheticSettingsContainer = document.createElement("div");
7996
+ aestheticSettingsContainer.className = "ht-aesthetic-settings hidden";
6298
7997
  const tabs = [
6299
7998
  { id: "data", label: "Data", requiresTree: false },
6300
7999
  { id: "controls", label: "Controls", requiresTree: true },
@@ -6311,6 +8010,7 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6311
8010
  if (tabDiv.classList.contains("disabled")) {
6312
8011
  return;
6313
8012
  }
8013
+ closeAestheticSettings();
6314
8014
  if (currentTab === tab.id) {
6315
8015
  closeTab();
6316
8016
  } else {
@@ -6320,6 +8020,14 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6320
8020
  tabElements[tab.id] = tabDiv;
6321
8021
  tabsContainer.appendChild(tabDiv);
6322
8022
  });
8023
+ controlsContainer.addEventListener("click", (e) => {
8024
+ const editButton = e.target.closest(".ht-icon-button");
8025
+ const aestheticGroup = e.target.closest(".ht-control-group.ht-aesthetic-editing");
8026
+ if (editButton || aestheticGroup) {
8027
+ return;
8028
+ }
8029
+ closeAestheticSettings();
8030
+ });
6323
8031
  function updateTabStates() {
6324
8032
  const hasTree = getCurrentTreeState() !== null;
6325
8033
  tabs.forEach((tab) => {
@@ -6337,12 +8045,15 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6337
8045
  }
6338
8046
  }
6339
8047
  }
6340
- toggleButton.addEventListener("click", () => {
8048
+ toggleButton.addEventListener("click", (e) => {
8049
+ e.preventDefault();
8050
+ e.stopPropagation();
6341
8051
  controlPanelVisible = !controlPanelVisible;
6342
8052
  if (controlPanelVisible) {
6343
8053
  collapsiblePanel.classList.remove("ht-panel-collapsed");
6344
8054
  toggleButton.classList.remove("collapsed");
6345
8055
  } else {
8056
+ closeAestheticSettings();
6346
8057
  collapsiblePanel.classList.add("ht-panel-collapsed");
6347
8058
  toggleButton.classList.add("collapsed");
6348
8059
  }
@@ -6402,6 +8113,53 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6402
8113
  }
6403
8114
  return "tree";
6404
8115
  }
8116
+ function openAestheticSettings(aestheticId, aestheticGroup) {
8117
+ if (currentAestheticSettings === aestheticId) {
8118
+ closeAestheticSettings();
8119
+ return;
8120
+ }
8121
+ closeAestheticSettings();
8122
+ currentAestheticSettings = aestheticId;
8123
+ aestheticGroup.classList.add("ht-aesthetic-editing");
8124
+ aestheticSettingsContainer.classList.remove("hidden");
8125
+ populateAestheticSettings(aestheticId);
8126
+ }
8127
+ function closeAestheticSettings() {
8128
+ if (!currentAestheticSettings) return;
8129
+ const existingEditors = aestheticSettingsContainer.querySelectorAll(".ht-color-palette-editor");
8130
+ existingEditors.forEach((editor) => {
8131
+ if (editor.cleanupFunction && typeof editor.cleanupFunction === "function") {
8132
+ editor.cleanupFunction();
8133
+ }
8134
+ });
8135
+ const allGroups = controlsContainer.querySelectorAll(".ht-control-group");
8136
+ allGroups.forEach((group) => group.classList.remove("ht-aesthetic-editing"));
8137
+ aestheticSettingsContainer.classList.add("hidden");
8138
+ aestheticSettingsContainer.innerHTML = "";
8139
+ currentAestheticSettings = null;
8140
+ }
8141
+ function populateAestheticSettings(aestheticId) {
8142
+ aestheticSettingsContainer.innerHTML = "";
8143
+ const treeState = getCurrentTreeState();
8144
+ if (!treeState) return;
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);
8153
+ return;
8154
+ }
8155
+ const settingsWidget = aesthetic.createSettingsWidget({
8156
+ controlHeight: CONTROL_HEIGHT,
8157
+ columnId
8158
+ });
8159
+ if (settingsWidget) {
8160
+ aestheticSettingsContainer.appendChild(settingsWidget);
8161
+ }
8162
+ }
6405
8163
  function openTab(tabId) {
6406
8164
  const tabDef = tabs.find((t) => t.id === tabId);
6407
8165
  if (tabDef && tabDef.requiresTree && !getCurrentTreeState()) {
@@ -6425,9 +8183,11 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6425
8183
  });
6426
8184
  controlsContainer.classList.add("hidden");
6427
8185
  controlsContainer.innerHTML = "";
8186
+ closeAestheticSettings();
6428
8187
  }
6429
8188
  function populateControls(tabId) {
6430
8189
  controlsContainer.innerHTML = "";
8190
+ closeAestheticSettings();
6431
8191
  switch (tabId) {
6432
8192
  case "data":
6433
8193
  populateDataControls(
@@ -6474,7 +8234,16 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6474
8234
  );
6475
8235
  break;
6476
8236
  case "tip-label-settings":
6477
- populateTipLabelSettingsControls(controlsContainer, getCurrentTreeState, options, CONTROL_HEIGHT);
8237
+ populateTipLabelSettingsControls(
8238
+ controlsContainer,
8239
+ getCurrentTreeState,
8240
+ options,
8241
+ CONTROL_HEIGHT,
8242
+ openAestheticSettings,
8243
+ closeAestheticSettings,
8244
+ populateAestheticSettings,
8245
+ () => currentAestheticSettings
8246
+ );
6478
8247
  break;
6479
8248
  case "export":
6480
8249
  populateExportControls(controlsContainer, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, CONTROL_HEIGHT);
@@ -6501,17 +8270,17 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6501
8270
  }
6502
8271
  collapsiblePanel.appendChild(tabsContainer);
6503
8272
  collapsiblePanel.appendChild(controlsContainer);
8273
+ collapsiblePanel.appendChild(aestheticSettingsContainer);
6504
8274
  toolbarDiv.appendChild(toggleContainer);
6505
8275
  toolbarDiv.appendChild(collapsiblePanel);
6506
8276
  updateTabStates();
6507
- openTab(tabs[0].id);
6508
8277
  resetSelectedMetadata();
6509
8278
  return refreshCurrentTab;
6510
8279
  }
6511
8280
  function populateDataControls(container, treeDataInstances, getCurrentTreeState, switchToTree, addNewTree, getCurrentMetadataNames, getSelectedMetadata, setSelectedMetadata, resetSelectedMetadata, refreshCurrentTab, options, controlHeight) {
6512
8281
  container.innerHTML = "";
6513
8282
  const treeGroup = createControlGroup();
6514
- const treeLabel = createLabel("Select tree:", controlHeight);
8283
+ const treeLabel = createLabel("Tree:", controlHeight);
6515
8284
  treeGroup.appendChild(treeLabel);
6516
8285
  const treeSelect = document.createElement("select");
6517
8286
  treeSelect.className = "ht-select";
@@ -6543,24 +8312,19 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6543
8312
  container.appendChild(treeGroup);
6544
8313
  const treeFileInput = document.createElement("input");
6545
8314
  treeFileInput.type = "file";
6546
- treeFileInput.accept = ".nwk,.newick,.tree,.tre,.treefile";
6547
8315
  treeFileInput.style.display = "none";
6548
8316
  treeFileInput.addEventListener("change", async (e) => {
6549
8317
  const file = e.target.files[0];
6550
8318
  if (!file) return;
6551
8319
  try {
6552
- const newickStr = await file.text();
6553
- let treeName = file.name.replace(/\.(nwk|newick|tree|tre)$/i, "");
6554
- let uniqueName = treeName;
6555
- let counter = 1;
6556
- while (treeDataInstances.has(uniqueName)) {
6557
- uniqueName = `${treeName} (${counter})`;
6558
- counter++;
6559
- }
6560
- addNewTree(uniqueName, newickStr);
8320
+ const treeString = await file.text();
8321
+ let treeName = file.name.replace(/\.[^/.]+$/, "");
8322
+ const addedNames = addNewTree(treeName, treeString);
6561
8323
  treeFileInput.value = "";
6562
8324
  refreshCurrentTab();
6563
- switchToTree(uniqueName);
8325
+ if (addedNames.length > 0) {
8326
+ switchToTree(addedNames[0]);
8327
+ }
6564
8328
  } catch (error) {
6565
8329
  console.error("Error loading tree file:", error);
6566
8330
  alert(`Error loading tree file: ${error.message}`);
@@ -6576,7 +8340,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6576
8340
  return;
6577
8341
  }
6578
8342
  const metadataGroup = createControlGroup();
6579
- const metadataLabel = createLabel("Available metadata:", controlHeight);
8343
+ const metadataLabel = createLabel("Metadata:", controlHeight);
6580
8344
  metadataGroup.appendChild(metadataLabel);
6581
8345
  const metadataSelect = document.createElement("select");
6582
8346
  metadataSelect.className = "ht-select";
@@ -6601,6 +8365,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6601
8365
  });
6602
8366
  metadataSelect.addEventListener("change", (e) => {
6603
8367
  setSelectedMetadata(e.target.value);
8368
+ refreshCurrentTab();
6604
8369
  });
6605
8370
  }
6606
8371
  metadataGroup.appendChild(metadataSelect);
@@ -6647,6 +8412,51 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6647
8412
  metadataFileInput.click();
6648
8413
  });
6649
8414
  container.appendChild(addMetadataBtn);
8415
+ if (selectedMetadata) {
8416
+ const treeData = currentTreeState.state.treeData;
8417
+ let selectedTableId = null;
8418
+ for (const [tableId, tableName] of treeData.metadataTableNames.entries()) {
8419
+ if (tableName === selectedMetadata) {
8420
+ selectedTableId = tableId;
8421
+ break;
8422
+ }
8423
+ }
8424
+ if (selectedTableId) {
8425
+ const validIdColumns = treeData.getValidIdColumns(selectedTableId);
8426
+ const currentIdColumn = treeData.getNodeIdColumn(selectedTableId);
8427
+ const nodeIdGroup = createControlGroup();
8428
+ const nodeIdLabel = createLabel("ID Column:", controlHeight);
8429
+ nodeIdGroup.appendChild(nodeIdLabel);
8430
+ const nodeIdSelect = document.createElement("select");
8431
+ nodeIdSelect.className = "ht-select";
8432
+ nodeIdSelect.style.height = `${controlHeight}px`;
8433
+ if (validIdColumns.length === 0) {
8434
+ const option = document.createElement("option");
8435
+ option.textContent = "No ID column found";
8436
+ option.value = "";
8437
+ nodeIdSelect.appendChild(option);
8438
+ nodeIdSelect.disabled = true;
8439
+ } else {
8440
+ validIdColumns.forEach((columnName) => {
8441
+ const option = document.createElement("option");
8442
+ option.value = columnName;
8443
+ option.textContent = treeData.columnName.get(columnName);
8444
+ if (columnName === currentIdColumn) {
8445
+ option.selected = true;
8446
+ }
8447
+ nodeIdSelect.appendChild(option);
8448
+ });
8449
+ nodeIdSelect.addEventListener("change", (e) => {
8450
+ const newIdColumn = e.target.value;
8451
+ treeData.setNodeIdColumn(selectedTableId, newIdColumn);
8452
+ currentTreeState.updateCoordinates();
8453
+ refreshCurrentTab();
8454
+ });
8455
+ }
8456
+ nodeIdGroup.appendChild(nodeIdSelect);
8457
+ container.appendChild(nodeIdGroup);
8458
+ }
8459
+ }
6650
8460
  }
6651
8461
  function populateControlsTab(container, getCurrentTreeState, getCurrentTreeView, options, controlHeight) {
6652
8462
  container.innerHTML = "";
@@ -6845,13 +8655,19 @@ function populateTreeManipulationControls(container, getCurrentTreeState, refres
6845
8655
  radialLayoutGroup.appendChild(radialLayoutToggle);
6846
8656
  container.appendChild(radialLayoutGroup);
6847
8657
  }
6848
- function populateTipLabelSettingsControls(container, getCurrentTreeState, options, controlHeight) {
8658
+ function populateTipLabelSettingsControls(container, getCurrentTreeState, options, controlHeight, openAestheticSettings, closeAestheticSettings, populateAestheticSettings, getCurrentAestheticSettings) {
6849
8659
  container.innerHTML = "";
6850
8660
  const treeState = getCurrentTreeState();
6851
8661
  if (!treeState) {
6852
8662
  container.textContent = "No tree selected";
6853
8663
  return;
6854
8664
  }
8665
+ const editIconSvg = `
8666
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
8667
+ <path d="M11.5 1.5L14.5 4.5L5 14H2V11L11.5 1.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
8668
+ <path d="M10 3L13 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
8669
+ </svg>
8670
+ `;
6855
8671
  const tipLabelTextGroup = createControlGroup();
6856
8672
  const tipLabelTextLabel = createLabel("Text:", controlHeight);
6857
8673
  tipLabelTextGroup.appendChild(tipLabelTextLabel);
@@ -6861,20 +8677,30 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6861
8677
  "Default",
6862
8678
  controlHeight,
6863
8679
  true,
6864
- null
8680
+ null,
8681
+ getCurrentAestheticSettings,
8682
+ populateAestheticSettings
6865
8683
  );
6866
8684
  tipLabelTextGroup.appendChild(tipLabelTextSelect);
6867
8685
  container.appendChild(tipLabelTextGroup);
6868
8686
  const tipLabelColorGroup = createControlGroup();
6869
8687
  const tipLabelColorLabel = createLabel("Color:", controlHeight);
6870
8688
  tipLabelColorGroup.appendChild(tipLabelColorLabel);
8689
+ const tipLabelColorEditBtn = createIconButton(editIconSvg, "Edit color settings", controlHeight);
8690
+ tipLabelColorEditBtn.addEventListener("click", (e) => {
8691
+ e.stopPropagation();
8692
+ openAestheticSettings("tipLabelColor", tipLabelColorGroup);
8693
+ });
8694
+ tipLabelColorGroup.appendChild(tipLabelColorEditBtn);
6871
8695
  const tipLabelColorSelect = createMetadataColumnSelect(
6872
8696
  treeState,
6873
8697
  "tipLabelColor",
6874
8698
  "Default",
6875
8699
  controlHeight,
6876
8700
  false,
6877
- null
8701
+ null,
8702
+ getCurrentAestheticSettings,
8703
+ populateAestheticSettings
6878
8704
  );
6879
8705
  tipLabelColorGroup.appendChild(tipLabelColorSelect);
6880
8706
  container.appendChild(tipLabelColorGroup);
@@ -6887,7 +8713,9 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6887
8713
  "Default",
6888
8714
  controlHeight,
6889
8715
  false,
6890
- true
8716
+ true,
8717
+ getCurrentAestheticSettings,
8718
+ populateAestheticSettings
6891
8719
  );
6892
8720
  tipLabelSizeGroup.appendChild(tipLabelSizeSelect);
6893
8721
  container.appendChild(tipLabelSizeGroup);
@@ -6900,7 +8728,9 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6900
8728
  "Default",
6901
8729
  controlHeight,
6902
8730
  false,
6903
- false
8731
+ false,
8732
+ getCurrentAestheticSettings,
8733
+ populateAestheticSettings
6904
8734
  );
6905
8735
  tipLabelStyleGroup.appendChild(tipLabelStyleSelect);
6906
8736
  container.appendChild(tipLabelStyleGroup);
@@ -6932,7 +8762,7 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6932
8762
  tipLabelFontGroup.appendChild(tipLabelFontSelect);
6933
8763
  container.appendChild(tipLabelFontGroup);
6934
8764
  }
6935
- function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null) {
8765
+ function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null, getCurrentAestheticSettings = null, populateAestheticSettings = null) {
6936
8766
  const select2 = document.createElement("select");
6937
8767
  select2.className = "ht-select";
6938
8768
  select2.style.height = `${controlHeight}px`;
@@ -6986,6 +8816,12 @@ function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlH
6986
8816
  const aestheticUpdate = {};
6987
8817
  aestheticUpdate[aesthetic] = columnId;
6988
8818
  treeState.setAesthetics(aestheticUpdate);
8819
+ if (getCurrentAestheticSettings && populateAestheticSettings) {
8820
+ const currentAestheticSettings = getCurrentAestheticSettings();
8821
+ if (currentAestheticSettings === aesthetic) {
8822
+ populateAestheticSettings(aesthetic);
8823
+ }
8824
+ }
6989
8825
  });
6990
8826
  return select2;
6991
8827
  }
@@ -7137,61 +8973,6 @@ function populateExportControls(container, getCurrentTreeState, getCurrentTreeVi
7137
8973
  marginGroup.appendChild(marginInput);
7138
8974
  container.appendChild(marginGroup);
7139
8975
  }
7140
- function createControlGroup() {
7141
- const group = document.createElement("div");
7142
- group.className = "ht-control-group";
7143
- return group;
7144
- }
7145
- function createLabel(text, height) {
7146
- const label = document.createElement("label");
7147
- label.className = "ht-control-label";
7148
- label.textContent = text;
7149
- label.style.height = `${height}px`;
7150
- return label;
7151
- }
7152
- function createButton(text, title = "", height) {
7153
- const button = document.createElement("button");
7154
- button.className = "ht-button";
7155
- button.textContent = text;
7156
- button.title = title;
7157
- button.style.height = `${height}px`;
7158
- return button;
7159
- }
7160
- function createSlider(min, max, value, step, height) {
7161
- const slider = document.createElement("input");
7162
- slider.type = "range";
7163
- slider.className = "ht-slider";
7164
- slider.min = min;
7165
- slider.max = max;
7166
- slider.value = value;
7167
- slider.step = step;
7168
- slider.style.height = `${height}px`;
7169
- return slider;
7170
- }
7171
- function createToggle(initialState, height) {
7172
- const toggleHeight = Math.min(24, height - 4);
7173
- const knobSize = toggleHeight - 4;
7174
- const toggle = document.createElement("div");
7175
- toggle.className = initialState ? "ht-toggle active" : "ht-toggle";
7176
- toggle.style.height = `${toggleHeight}px`;
7177
- const knob = document.createElement("div");
7178
- knob.className = "ht-toggle-knob";
7179
- knob.style.width = `${knobSize}px`;
7180
- knob.style.height = `${knobSize}px`;
7181
- toggle.appendChild(knob);
7182
- return toggle;
7183
- }
7184
- function createNumberInput(value, min, max, step, height) {
7185
- const input = document.createElement("input");
7186
- input.type = "number";
7187
- input.className = "ht-number-input";
7188
- input.value = value;
7189
- input.min = min;
7190
- input.max = max;
7191
- input.step = step;
7192
- input.style.height = `${height}px`;
7193
- return input;
7194
- }
7195
8976
  function heatTree(containerSelector, treesInput = [], options = {}) {
7196
8977
  if (treesInput && !Array.isArray(treesInput)) {
7197
8978
  treesInput = [treesInput];
@@ -7212,10 +8993,10 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
7212
8993
  const treeDataInstances = /* @__PURE__ */ new Map();
7213
8994
  const treeConfigAesthetics = /* @__PURE__ */ new Map();
7214
8995
  treesInput.forEach((treeConfig, index) => {
7215
- if (!treeConfig.newick) {
7216
- throw new Error(`Tree at index ${index} is missing newick string`);
8996
+ if (!treeConfig.tree) {
8997
+ throw new Error(`Tree at index ${index} is missing tree string`);
7217
8998
  }
7218
- const treeName = treeConfig.name || `Tree ${index + 1}`;
8999
+ const sourceName = treeConfig.name || `Tree ${index + 1}`;
7219
9000
  let metadataTables = [];
7220
9001
  let metadataNames = [];
7221
9002
  if (treeConfig.metadata) {
@@ -7230,24 +9011,33 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
7230
9011
  }
7231
9012
  });
7232
9013
  }
7233
- const treeData = new TreeData(treeConfig.newick, metadataTables, metadataNames);
7234
- let treeAesthetics;
7235
- if (treeConfig.aesthetics) {
7236
- treeAesthetics = Object.fromEntries(
7237
- Object.entries(treeConfig.aesthetics).map(([aes, col]) => {
7238
- for (const [assignedColId, originalName] of treeData.columnName.entries()) {
7239
- if (originalName === col) {
7240
- return [aes, assignedColId];
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
+ }
7241
9031
  }
7242
- }
7243
- return void 0;
7244
- })
7245
- );
7246
- } else {
7247
- treeAesthetics = void 0;
7248
- }
7249
- treeDataInstances.set(treeName, treeData);
7250
- treeConfigAesthetics.set(treeName, treeAesthetics);
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
+ });
7251
9041
  });
7252
9042
  const treeStateCache = /* @__PURE__ */ new Map();
7253
9043
  const treeViewCache = /* @__PURE__ */ new Map();
@@ -7284,16 +9074,21 @@ function heatTree(containerSelector, treesInput = [], options = {}) {
7284
9074
  immediate: true
7285
9075
  }
7286
9076
  );
7287
- function addNewTree(treeName, newickStr, metadataTables = [], metadataNames = []) {
7288
- let uniqueName = treeName;
7289
- let counter = 1;
7290
- while (treeDataInstances.has(uniqueName)) {
7291
- uniqueName = `${treeName} (${counter})`;
7292
- counter++;
7293
- }
7294
- const treeData = new TreeData(newickStr, metadataTables, metadataNames);
7295
- treeDataInstances.set(uniqueName, treeData);
7296
- return uniqueName;
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;
7297
9092
  }
7298
9093
  function switchToTree(treeName) {
7299
9094
  if (!treeDataInstances.has(treeName)) {