@grunwaldlab/heat-tree 0.1.1 → 0.2.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{display:flex;flex-direction:column;width:100%;height:100%}.ht-toolbar{flex:0 0 auto;margin-bottom:4px;display:flex;flex-direction:column;position:relative}.ht-toggle-container{position:absolute;top:0;right:0;padding:4px 8px;pointer-events:none}.ht-control-panel-toggle{background-color:transparent;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;gap:4px;transition:opacity .2s;pointer-events:auto}.ht-control-panel-toggle:hover{opacity:.7}.ht-control-panel-toggle .ht-toggle-arrow{transition:transform .3s}.ht-control-panel-toggle.collapsed .ht-toggle-arrow{transform:rotate(180deg)}.ht-control-panel-toggle .ht-hamburger-icon{color:#333}.ht-collapsible-panel{max-height:1000px;transition:max-height .3s ease-in-out}.ht-collapsible-panel.ht-panel-collapsed{max-height:0}.ht-tabs{display:flex;gap:20px;padding:4px 8px;background-color:#f5f5f5;border-bottom:2px solid #ddd;-webkit-user-select:none;user-select:none}.ht-tab{cursor:pointer;padding:2px 4px;font-family:sans-serif;font-size:14px;color:#333;border-bottom:2px solid transparent;transition:all .2s}.ht-tab:hover{color:#666}.ht-tab.active{color:#000;font-weight:700;border-bottom-color:#007bff}.ht-tab.active:hover{color:#000}.ht-tab.disabled{color:#999;cursor:not-allowed;opacity:.5}.ht-tab.disabled:hover{color:#999}.ht-controls{padding:2px 8px;background-color:#fafafa;border-bottom:1px solid #ddd;display:flex;flex-wrap:wrap;column-gap:8px;row-gap:2px;align-items:center;min-height:26px}.ht-controls.hidden{display:none}.ht-control-group{display:flex;align-items:center;gap:4px;white-space:nowrap;padding:2px 0;border-bottom:2px solid transparent;transition:border-bottom-color .2s}.ht-control-group.ht-aesthetic-editing{border-bottom-color:#007bff}.ht-control-label{font-family:sans-serif;font-size:14px;color:#333;white-space:nowrap;display:flex;align-items:center}.ht-button{font-size:14px;font-family:sans-serif;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s;white-space:nowrap}.ht-button:hover{background-color:#d0d0d0}.ht-button:disabled{background-color:#f0f0f0;color:#999;cursor:not-allowed}.ht-button.primary{background-color:#007bff;color:#fff;font-weight:700}.ht-button.primary:hover{background-color:#0056b3}.ht-icon-button{font-size:14px;font-family:sans-serif;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s;display:flex;align-items:center;justify-content:center;padding:2px}.ht-icon-button:hover{background-color:#d0d0d0}.ht-icon-button svg{display:block}.ht-select{padding:1px 2px;font-size:14px;border:1px solid #ccc;border-radius:4px}.ht-slider{cursor:pointer;width:150px}.ht-toggle{width:50px;background-color:#ccc;border-radius:12px;position:relative;cursor:pointer;transition:background-color .3s;flex-shrink:0}.ht-toggle.active{background-color:#007bff}.ht-toggle-knob{background-color:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:left .3s}.ht-toggle.active .ht-toggle-knob{left:calc(100% - 2px);transform:translate(-100%)}.ht-number-input{font-size:14px;border:1px solid #ccc;border-radius:4px;width:80px}.ht-text-input{font-size:14px;border:1px solid #ccc;border-radius:4px;padding:2px 4px;width:100px}.ht-aesthetic-settings{padding:2px 8px;background-color:#f0f0f0;border-bottom:1px solid #ddd;display:flex;flex-wrap:wrap;column-gap:8px;row-gap:2px;align-items:center;max-height:1000px;transition:max-height .3s ease-in-out,padding .3s ease-in-out}.ht-aesthetic-settings.hidden{max-height:0;padding-top:0;padding-bottom:0;border-bottom:none}.ht-color-palette-editor{display:flex;gap:8px;align-items:center}.ht-palette-buttons-container{display:flex;flex-direction:column;gap:2px}.ht-palette-button{width:24px;height:24px;font-size:14px;font-weight:700;background-color:#e0e0e0;border:1px solid #ccc;border-radius:4px;cursor:pointer;transition:background-color .2s}.ht-palette-button:hover{background-color:#d0d0d0}.ht-gradient-container{display:flex;gap:8px}.ht-gradient-column{display:flex;flex-direction:column;gap:4px}.ht-color-squares-container{display:flex;position:relative;height:16px;width:200px}.ht-color-square-wrapper{position:absolute;display:flex;flex-direction:column;align-items:center;transform:translate(-50%)}.ht-color-square{width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:pointer}.ht-color-square-tick{width:2px;height:8px;background-color:#333}.ht-gradient-box{width:200px;height:24px;border:1px solid #ccc;border-radius:4px;position:relative}.ht-range-slider-container{width:200px;height:12px;position:relative;border:1px solid #ccc;border-radius:4px;background:#f0f0f0;display:flex;align-items:center}.ht-range-handle{position:absolute;width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:ew-resize;transform:translate(-50%)}.ht-range-handle-indicator{position:absolute;bottom:90%;left:50%;transform:translate(-50%) rotate(180deg);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid #333;border-radius:2px;margin-bottom:2px}.ht-null-color-column{display:flex;flex-direction:column;gap:4px}.ht-null-color-square-container{display:flex;position:relative;height:16px;justify-content:center}.ht-null-color-square{width:12px;height:12px;border:2px solid #333;border-radius:4px;cursor:pointer}.ht-null-color-square-tick{width:2px;height:6px;background-color:#333;position:absolute;bottom:-6px;left:50%;transform:translate(-50%)}.ht-null-color-box{width:16px;height:24px;border:1px solid #ccc;border-radius:4px;position:relative}.ht-null-color-reset-container{position:relative;display:flex;align-items:center;justify-content:center}.ht-null-color-reset-indicator{position:absolute;bottom:85%;left:50%;transform:translate(-50%) rotate(180deg);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid #333;border-radius:2px;margin-bottom:2px}.ht-null-color-reset{font-size:14px;color:#d00;font-weight:700;line-height:1;-webkit-user-select:none;user-select:none;cursor:pointer}.ht-tree{flex:1 1 auto;min-height:0;position:relative}.ht-tree svg{display:block}.picker_wrapper,.picker_wrapper.popup{z-index:999999!important;position:fixed!important}.picker_wrapper *,.picker_selector,.picker_slider,.picker_editor,.picker_sample,.picker_done,.picker_cancel{pointer-events:auto!important}";
2873
2873
  function injectStyles() {
2874
2874
  const styleId = "heat-tree-styles";
2875
2875
  if (!document.getElementById(styleId)) {
@@ -3118,67 +3118,69 @@ function parseNewick(newickStr) {
3118
3118
  }
3119
3119
  return result;
3120
3120
  }
3121
- function parseTable(tsvStr, sep = " ") {
3121
+ function parseTable(tsvStr, valid_ids, sep = " ") {
3122
3122
  let metadataMap = /* @__PURE__ */ new Map();
3123
- let metadataColumns = [];
3124
3123
  let columnTypes = /* @__PURE__ */ new Map();
3124
+ let validIdCounts = [];
3125
+ let idColumns = [];
3125
3126
  const lines = tsvStr.trim().split("\n");
3126
3127
  if (lines.length == 0) {
3127
- console.error("Empty metatdata table");
3128
+ console.error("Empty metadata table");
3128
3129
  } else {
3129
3130
  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
- }
3131
+ const columnValues = /* @__PURE__ */ new Map();
3132
+ headers.forEach((col) => columnValues.set(col, []));
3133
+ for (let i = 1; i < lines.length; i++) {
3134
+ const values = lines[i].split(sep);
3135
+ const metadata = {};
3136
+ for (let j = 0; j < headers.length; j++) {
3137
+ const colName = headers[j];
3138
+ const value = values[j];
3139
+ metadata[colName] = value;
3140
+ columnValues.get(colName).push(value);
3141
+ }
3142
+ metadataMap.set(i - 1, metadata);
3143
+ }
3144
+ headers.forEach((col) => {
3145
+ const values = columnValues.get(col);
3146
+ const numericValues = values.map((v) => parseFloat(v)).filter((v) => !isNaN(v));
3147
+ const isContinuous = numericValues.length > 0 && numericValues.length === values.filter((v) => v !== "").length;
3148
+ columnTypes.set(col, isContinuous ? "continuous" : "categorical");
3149
+ let matchCount = 0;
3150
+ for (const value of values) {
3151
+ if (value && valid_ids.has(value)) {
3152
+ matchCount++;
3148
3153
  }
3149
- metadataMap.set(nodeId, metadata);
3150
3154
  }
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
- }
3155
+ if (matchCount > 0) {
3156
+ validIdCounts.push({ col, matchCount });
3157
+ }
3158
+ });
3159
+ validIdCounts = validIdCounts.sort((a, b) => b.matchCount - a.matchCount);
3160
+ idColumns = validIdCounts.map((x) => x.col);
3158
3161
  }
3159
- return {
3160
- metadataMap,
3161
- columnTypes
3162
- };
3162
+ return { metadataMap, columnTypes, idColumns };
3163
3163
  }
3164
3164
  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;
3165
+ constructor(options = {}) {
3166
+ this.state = { ...options };
3167
+ if (this.state.default === void 0) {
3168
+ console.error("A default value for a NullScale is needed.");
3171
3169
  }
3172
3170
  }
3173
3171
  getValue() {
3174
- return this.defaultValue;
3172
+ return this.state.default;
3175
3173
  }
3176
3174
  }
3177
3175
  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;
3176
+ constructor(options = {}) {
3177
+ this.state = {
3178
+ default: null,
3179
+ outputValues: null,
3180
+ transformFn: null,
3181
+ ...options
3182
+ };
3183
+ this.validValues = this.state.outputValues ? new Set(this.state.outputValues) : null;
3182
3184
  }
3183
3185
  /**
3184
3186
  * Get the value, optionally transformed and validated
@@ -3186,23 +3188,26 @@ class IdentityScale {
3186
3188
  * @returns {*} The output value, or null if invalid
3187
3189
  */
3188
3190
  getValue(value) {
3189
- let result = this.transformFn ? this.transformFn(value) : value;
3191
+ let result = this.state.transformFn ? this.state.transformFn(value) : value;
3190
3192
  if (this.validValues !== null && !this.validValues.has(result)) {
3191
3193
  result = null;
3192
3194
  }
3193
3195
  if (result === null || result === void 0 || result === "") {
3194
- result = this.defaultValue;
3196
+ result = this.state.default;
3195
3197
  }
3196
3198
  return result;
3197
3199
  }
3198
3200
  }
3199
3201
  class CategoricalTextScale {
3200
- constructor(values, outputCategories, defaultValue) {
3202
+ constructor(values, options = {}) {
3201
3203
  if (!Array.isArray(values) || values.length === 0) {
3202
3204
  throw new Error("values must be a non-empty array");
3203
3205
  }
3204
- this.outputCategories = outputCategories;
3205
- this.defaultValue = defaultValue;
3206
+ this.state = {
3207
+ outputValues: null,
3208
+ default: null,
3209
+ ...options
3210
+ };
3206
3211
  const frequencyMap = /* @__PURE__ */ new Map();
3207
3212
  for (const category of values) {
3208
3213
  if (category === null || category === void 0 || category === "") {
@@ -3217,18 +3222,18 @@ class CategoricalTextScale {
3217
3222
  const sortedCategories = new Map(
3218
3223
  [...frequencyMap.entries()].sort((a, b) => a[0] - b[0])
3219
3224
  );
3220
- if (sortedCategories.length > outputCategories.length) {
3221
- this.otheredCategories = sortedCategories.keys().slice(outputCategories.length - 2, sortedCategories.length);
3225
+ if (sortedCategories.length > this.state.outputValues.length) {
3226
+ this.otheredCategories = sortedCategories.keys().slice(this.state.outputValues.length - 2, sortedCategories.length);
3222
3227
  } else {
3223
3228
  this.otheredCategories = [];
3224
3229
  }
3225
3230
  this.categoryMap = /* @__PURE__ */ new Map();
3226
3231
  const sortedCategoriesKeys = [...sortedCategories.keys()];
3227
3232
  for (let i = 0; i < sortedCategoriesKeys.length; i++) {
3228
- if (i < outputCategories.length) {
3229
- this.categoryMap.set(sortedCategoriesKeys[i], outputCategories[i]);
3233
+ if (i < this.state.outputValues.length) {
3234
+ this.categoryMap.set(sortedCategoriesKeys[i], this.state.outputValues[i]);
3230
3235
  } else {
3231
- this.categoryMap.set(sortedCategoriesKeys[i], outputCategories[outputCategories.length - 1]);
3236
+ this.categoryMap.set(sortedCategoriesKeys[i], this.state.outputValues[this.state.outputValues.length - 1]);
3232
3237
  }
3233
3238
  }
3234
3239
  }
@@ -3239,17 +3244,20 @@ class CategoricalTextScale {
3239
3244
  */
3240
3245
  getValue(category) {
3241
3246
  if (category === null || category === void 0 || category === "") {
3242
- return this.defaultValue;
3247
+ return this.state.default;
3243
3248
  }
3244
3249
  return this.categoryMap.get(category);
3245
3250
  }
3246
3251
  }
3247
3252
  class ContinuousSizeScale {
3248
- constructor(dataMin, dataMax, sizeMin, sizeMax) {
3253
+ constructor(dataMin, dataMax, options = {}) {
3254
+ this.state = {
3255
+ outputRange: [0.5, 2],
3256
+ nullValue: 1,
3257
+ ...options
3258
+ };
3249
3259
  this.dataMin = dataMin;
3250
3260
  this.dataMax = dataMax;
3251
- this.sizeMin = sizeMin;
3252
- this.sizeMax = sizeMax;
3253
3261
  }
3254
3262
  /**
3255
3263
  * Get the size corresponding to the given value
@@ -3257,46 +3265,53 @@ class ContinuousSizeScale {
3257
3265
  * @returns {number} The corresponding size, clamped to min/max
3258
3266
  */
3259
3267
  getValue(value) {
3268
+ if (value === null || value === void 0 || value === "") {
3269
+ return this.state.nullValue;
3270
+ }
3260
3271
  const clampedValue = Math.max(this.dataMin, Math.min(this.dataMax, value));
3261
3272
  if (this.dataMin === this.dataMax) {
3262
3273
  if (value === this.dataMax) {
3263
- return (this.sizeMin + this.sizeMax) / 2;
3274
+ return (this.state.outputRange[0] + this.state.outputRange[1]) / 2;
3264
3275
  } else if (value < this.dataMin) {
3265
- return this.sizeMin;
3276
+ return this.state.outputRange[0];
3266
3277
  } else {
3267
- return this.sizeMax;
3278
+ return this.state.outputRange[1];
3268
3279
  }
3269
3280
  }
3270
3281
  const t = (clampedValue - this.dataMin) / (this.dataMax - this.dataMin);
3271
- return this.sizeMin + t * (this.sizeMax - this.sizeMin);
3282
+ return this.state.outputRange[0] + t * (this.state.outputRange[1] - this.state.outputRange[0]);
3272
3283
  }
3273
3284
  }
3274
3285
  class ContinuousColorScale {
3275
- constructor(dataMin, dataMax, transformMin = 0, transformMax = 1, colors = null, colorPositions = null) {
3286
+ constructor(dataMin, dataMax, options = {}) {
3287
+ this.state = {
3288
+ transformMin: 0,
3289
+ transformMax: 1,
3290
+ colorPalette: null,
3291
+ colorPositions: null,
3292
+ nullValue: "#808080",
3293
+ ...options
3294
+ };
3276
3295
  this.dataMin = dataMin;
3277
3296
  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));
3297
+ if (this.state.colorPalette === null) {
3298
+ this.state.colorPalette = ["#440154", "#31688e", "#35b779", "#fde724"];
3299
+ }
3300
+ if (this.state.colorPalette.length < 1) {
3301
+ throw new Error("At least 1 color is required");
3288
3302
  }
3303
+ this.colors = this.state.colorPalette.map((c) => this._hexToRgb(c));
3289
3304
  if (this.colors.length === 1) {
3290
3305
  this.colorPositions = [0];
3291
3306
  return;
3292
3307
  }
3293
- if (colorPositions === null) {
3308
+ if (this.state.colorPositions === null) {
3294
3309
  this.colorPositions = this.colors.map((_, i) => i / (this.colors.length - 1));
3295
3310
  } else {
3296
- if (colorPositions.length !== this.colors.length) {
3311
+ if (this.state.colorPositions.length !== this.colors.length) {
3297
3312
  throw new Error("colorPositions must have the same length as colors");
3298
3313
  }
3299
- const paired = colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3314
+ const paired = this.state.colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3300
3315
  paired.sort((a, b) => a.pos - b.pos);
3301
3316
  this.colorPositions = paired.map((p) => p.pos);
3302
3317
  this.colors = paired.map((p) => p.color);
@@ -3316,7 +3331,7 @@ class ContinuousColorScale {
3316
3331
  */
3317
3332
  getValue(value) {
3318
3333
  if (value === null || value === void 0 || value === "") {
3319
- return this.nullColor;
3334
+ return this.state.nullValue;
3320
3335
  }
3321
3336
  if (this.colors.length === 1) {
3322
3337
  return this._rgbToHex(this.colors[0].r, this.colors[0].g, this.colors[0].b);
@@ -3335,7 +3350,7 @@ class ContinuousColorScale {
3335
3350
  }
3336
3351
  const clampedValue = Math.max(this.dataMin, Math.min(this.dataMax, value));
3337
3352
  const dataT = (clampedValue - this.dataMin) / (this.dataMax - this.dataMin);
3338
- const transformedT = this.transformMin + dataT * (this.transformMax - this.transformMin);
3353
+ const transformedT = this.state.transformMin + dataT * (this.state.transformMax - this.state.transformMin);
3339
3354
  const clampedT = Math.max(0, Math.min(1, transformedT));
3340
3355
  if (clampedT <= this.colorPositions[0]) {
3341
3356
  return this._rgbToHex(this.colors[0].r, this.colors[0].g, this.colors[0].b);
@@ -3397,14 +3412,19 @@ class ContinuousColorScale {
3397
3412
  }
3398
3413
  class CategoricalColorScale {
3399
3414
  defaultPalette = ["#440154", "#31688e", "#35b779", "#fde724"];
3400
- constructor(categoryData, transformMin = 0, transformMax = 1, colors = null, colorPositions = null, maxColors = 10) {
3415
+ constructor(categoryData, options = {}) {
3401
3416
  if (!Array.isArray(categoryData) || categoryData.length === 0) {
3402
3417
  throw new Error("categoryData must be a non-empty array");
3403
3418
  }
3404
- this.transformMin = transformMin;
3405
- this.transformMax = transformMax;
3406
- this.nullColor = "#808080";
3407
- this.maxColors = maxColors;
3419
+ this.state = {
3420
+ transformMin: 0,
3421
+ transformMax: 1,
3422
+ colorPalette: null,
3423
+ colorPositions: null,
3424
+ maxCategories: 10,
3425
+ nullValue: "#808080",
3426
+ ...options
3427
+ };
3408
3428
  this.frequencyMap = /* @__PURE__ */ new Map();
3409
3429
  for (const category of categoryData) {
3410
3430
  if (this.frequencyMap.has(category)) {
@@ -3414,26 +3434,25 @@ class CategoricalColorScale {
3414
3434
  }
3415
3435
  }
3416
3436
  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));
3437
+ if (this.state.colorPalette === null) {
3438
+ this.state.colorPalette = this.defaultPalette;
3439
+ }
3440
+ if (this.state.colorPalette.length < 1) {
3441
+ throw new Error("At least 1 color is required");
3424
3442
  }
3443
+ this.colors = this.state.colorPalette.map((c) => this._hexToRgb(c));
3425
3444
  if (this.colors.length === 1) {
3426
3445
  this.colorPositions = [0];
3427
3446
  this._assignColors();
3428
3447
  return;
3429
3448
  }
3430
- if (colorPositions === null) {
3449
+ if (this.state.colorPositions === null) {
3431
3450
  this.colorPositions = this.colors.map((_, i) => i / (this.colors.length - 1));
3432
3451
  } else {
3433
- if (colorPositions.length !== this.colors.length) {
3452
+ if (this.state.colorPositions.length !== this.colors.length) {
3434
3453
  throw new Error("colorPositions must have the same length as colors");
3435
3454
  }
3436
- const paired = colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3455
+ const paired = this.state.colorPositions.map((pos, i) => ({ pos, color: this.colors[i] }));
3437
3456
  paired.sort((a, b) => a.pos - b.pos);
3438
3457
  this.colorPositions = paired.map((p) => p.pos);
3439
3458
  this.colors = paired.map((p) => p.color);
@@ -3461,13 +3480,13 @@ class CategoricalColorScale {
3461
3480
  return;
3462
3481
  }
3463
3482
  if (this.categories.length === 1) {
3464
- const color2 = this._getColorAtPosition(this.transformMin);
3483
+ const color2 = this._getColorAtPosition(this.state.transformMin);
3465
3484
  this.categoryColorMap.set(this.categories[0], color2);
3466
3485
  } else {
3467
- const nCategories = this.categories.length > this.maxColors ? this.maxColors : this.categories.length;
3486
+ const nCategories = this.categories.length > this.state.maxCategories ? this.state.maxCategories : this.categories.length;
3468
3487
  for (let i = 0; i < this.categories.length; i++) {
3469
3488
  const t = i >= nCategories ? 1 : i / (nCategories - 1);
3470
- const transformedT = this.transformMin + t * (this.transformMax - this.transformMin);
3489
+ const transformedT = this.state.transformMin + t * (this.state.transformMax - this.state.transformMin);
3471
3490
  const color2 = this._getColorAtPosition(transformedT);
3472
3491
  this.categoryColorMap.set(this.categories[i], color2);
3473
3492
  }
@@ -3514,7 +3533,7 @@ class CategoricalColorScale {
3514
3533
  */
3515
3534
  getValue(category) {
3516
3535
  if (category === null || category === void 0 || category === "") {
3517
- return this.nullColor;
3536
+ return this.state.nullValue;
3518
3537
  }
3519
3538
  if (this.categoryColorMap.has(category)) {
3520
3539
  return this.categoryColorMap.get(category);
@@ -3554,25 +3573,820 @@ class CategoricalColorScale {
3554
3573
  }).join("");
3555
3574
  }
3556
3575
  }
3557
- class Aesthetic {
3576
+ /*!
3577
+ * vanilla-picker v2.12.3
3578
+ * https://vanilla-picker.js.org
3579
+ *
3580
+ * Copyright 2017-2024 Andreas Borgen (https://github.com/Sphinxxxx), Adam Brooks (https://github.com/dissimulate)
3581
+ * Released under the ISC license.
3582
+ */
3583
+ var classCallCheck = function(instance, Constructor) {
3584
+ if (!(instance instanceof Constructor)) {
3585
+ throw new TypeError("Cannot call a class as a function");
3586
+ }
3587
+ };
3588
+ var createClass = /* @__PURE__ */ (function() {
3589
+ function defineProperties(target, props) {
3590
+ for (var i = 0; i < props.length; i++) {
3591
+ var descriptor = props[i];
3592
+ descriptor.enumerable = descriptor.enumerable || false;
3593
+ descriptor.configurable = true;
3594
+ if ("value" in descriptor) descriptor.writable = true;
3595
+ Object.defineProperty(target, descriptor.key, descriptor);
3596
+ }
3597
+ }
3598
+ return function(Constructor, protoProps, staticProps) {
3599
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
3600
+ if (staticProps) defineProperties(Constructor, staticProps);
3601
+ return Constructor;
3602
+ };
3603
+ })();
3604
+ var slicedToArray = /* @__PURE__ */ (function() {
3605
+ function sliceIterator(arr, i) {
3606
+ var _arr = [];
3607
+ var _n = true;
3608
+ var _d = false;
3609
+ var _e = void 0;
3610
+ try {
3611
+ for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
3612
+ _arr.push(_s.value);
3613
+ if (i && _arr.length === i) break;
3614
+ }
3615
+ } catch (err) {
3616
+ _d = true;
3617
+ _e = err;
3618
+ } finally {
3619
+ try {
3620
+ if (!_n && _i["return"]) _i["return"]();
3621
+ } finally {
3622
+ if (_d) throw _e;
3623
+ }
3624
+ }
3625
+ return _arr;
3626
+ }
3627
+ return function(arr, i) {
3628
+ if (Array.isArray(arr)) {
3629
+ return arr;
3630
+ } else if (Symbol.iterator in Object(arr)) {
3631
+ return sliceIterator(arr, i);
3632
+ } else {
3633
+ throw new TypeError("Invalid attempt to destructure non-iterable instance");
3634
+ }
3635
+ };
3636
+ })();
3637
+ String.prototype.startsWith = String.prototype.startsWith || function(needle) {
3638
+ return this.indexOf(needle) === 0;
3639
+ };
3640
+ String.prototype.padStart = String.prototype.padStart || function(len, pad) {
3641
+ var str = this;
3642
+ while (str.length < len) {
3643
+ str = pad + str;
3644
+ }
3645
+ return str;
3646
+ };
3647
+ 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" };
3648
+ function printNum(num) {
3649
+ var decs = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 1;
3650
+ var str = decs > 0 ? num.toFixed(decs).replace(/0+$/, "").replace(/\.$/, "") : num.toString();
3651
+ return str || "0";
3652
+ }
3653
+ var Color = (function() {
3654
+ function Color2(r, g, b, a) {
3655
+ classCallCheck(this, Color2);
3656
+ var that = this;
3657
+ function parseString(input) {
3658
+ if (input.startsWith("hsl")) {
3659
+ 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];
3660
+ if (_a === void 0) {
3661
+ _a = 1;
3662
+ }
3663
+ h /= 360;
3664
+ s /= 100;
3665
+ l /= 100;
3666
+ that.hsla = [h, s, l, _a];
3667
+ } else if (input.startsWith("rgb")) {
3668
+ 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];
3669
+ if (_a2 === void 0) {
3670
+ _a2 = 1;
3671
+ }
3672
+ that.rgba = [_r, _g, _b, _a2];
3673
+ } else {
3674
+ if (input.startsWith("#")) {
3675
+ that.rgba = Color2.hexToRgb(input);
3676
+ } else {
3677
+ that.rgba = Color2.nameToRgb(input) || Color2.hexToRgb(input);
3678
+ }
3679
+ }
3680
+ }
3681
+ if (r === void 0) ;
3682
+ else if (Array.isArray(r)) {
3683
+ this.rgba = r;
3684
+ } else if (b === void 0) {
3685
+ var color2 = r && "" + r;
3686
+ if (color2) {
3687
+ parseString(color2.toLowerCase());
3688
+ }
3689
+ } else {
3690
+ this.rgba = [r, g, b, a === void 0 ? 1 : a];
3691
+ }
3692
+ }
3693
+ createClass(Color2, [{
3694
+ key: "printRGB",
3695
+ value: function printRGB(alpha) {
3696
+ var rgb2 = alpha ? this.rgba : this.rgba.slice(0, 3), vals = rgb2.map(function(x, i) {
3697
+ return printNum(x, i === 3 ? 3 : 0);
3698
+ });
3699
+ return alpha ? "rgba(" + vals + ")" : "rgb(" + vals + ")";
3700
+ }
3701
+ }, {
3702
+ key: "printHSL",
3703
+ value: function printHSL(alpha) {
3704
+ var mults = [360, 100, 100, 1], suff = ["", "%", "%", ""];
3705
+ var hsl2 = alpha ? this.hsla : this.hsla.slice(0, 3), vals = hsl2.map(function(x, i) {
3706
+ return printNum(x * mults[i], i === 3 ? 3 : 1) + suff[i];
3707
+ });
3708
+ return alpha ? "hsla(" + vals + ")" : "hsl(" + vals + ")";
3709
+ }
3710
+ }, {
3711
+ key: "printHex",
3712
+ value: function printHex(alpha) {
3713
+ var hex2 = this.hex;
3714
+ return alpha ? hex2 : hex2.substring(0, 7);
3715
+ }
3716
+ }, {
3717
+ key: "rgba",
3718
+ get: function get2() {
3719
+ if (this._rgba) {
3720
+ return this._rgba;
3721
+ }
3722
+ if (!this._hsla) {
3723
+ throw new Error("No color is set");
3724
+ }
3725
+ return this._rgba = Color2.hslToRgb(this._hsla);
3726
+ },
3727
+ set: function set2(rgb2) {
3728
+ if (rgb2.length === 3) {
3729
+ rgb2[3] = 1;
3730
+ }
3731
+ this._rgba = rgb2;
3732
+ this._hsla = null;
3733
+ }
3734
+ }, {
3735
+ key: "rgbString",
3736
+ get: function get2() {
3737
+ return this.printRGB();
3738
+ }
3739
+ }, {
3740
+ key: "rgbaString",
3741
+ get: function get2() {
3742
+ return this.printRGB(true);
3743
+ }
3744
+ }, {
3745
+ key: "hsla",
3746
+ get: function get2() {
3747
+ if (this._hsla) {
3748
+ return this._hsla;
3749
+ }
3750
+ if (!this._rgba) {
3751
+ throw new Error("No color is set");
3752
+ }
3753
+ return this._hsla = Color2.rgbToHsl(this._rgba);
3754
+ },
3755
+ set: function set2(hsl2) {
3756
+ if (hsl2.length === 3) {
3757
+ hsl2[3] = 1;
3758
+ }
3759
+ this._hsla = hsl2;
3760
+ this._rgba = null;
3761
+ }
3762
+ }, {
3763
+ key: "hslString",
3764
+ get: function get2() {
3765
+ return this.printHSL();
3766
+ }
3767
+ }, {
3768
+ key: "hslaString",
3769
+ get: function get2() {
3770
+ return this.printHSL(true);
3771
+ }
3772
+ }, {
3773
+ key: "hex",
3774
+ get: function get2() {
3775
+ var rgb2 = this.rgba, hex2 = rgb2.map(function(x, i) {
3776
+ return i < 3 ? x.toString(16) : Math.round(x * 255).toString(16);
3777
+ });
3778
+ return "#" + hex2.map(function(x) {
3779
+ return x.padStart(2, "0");
3780
+ }).join("");
3781
+ },
3782
+ set: function set2(hex2) {
3783
+ this.rgba = Color2.hexToRgb(hex2);
3784
+ }
3785
+ }], [{
3786
+ key: "hexToRgb",
3787
+ value: function hexToRgb2(input) {
3788
+ 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");
3789
+ if (!hex2.match(/^([0-9a-fA-F]{8})$/)) {
3790
+ throw new Error("Unknown hex color; " + input);
3791
+ }
3792
+ var rgba2 = hex2.match(/^(\w\w)(\w\w)(\w\w)(\w\w)$/).slice(1).map(function(x) {
3793
+ return parseInt(x, 16);
3794
+ });
3795
+ rgba2[3] = rgba2[3] / 255;
3796
+ return rgba2;
3797
+ }
3798
+ }, {
3799
+ key: "nameToRgb",
3800
+ value: function nameToRgb(input) {
3801
+ var hash = input.toLowerCase().replace("at", "T").replace(/[aeiouyldf]/g, "").replace("ght", "L").replace("rk", "D").slice(-5, 4), hex2 = colorNames[hash];
3802
+ return hex2 === void 0 ? hex2 : Color2.hexToRgb(hex2.replace(/\-/g, "00").padStart(6, "f"));
3803
+ }
3804
+ }, {
3805
+ key: "rgbToHsl",
3806
+ value: function rgbToHsl(_ref) {
3807
+ var _ref2 = slicedToArray(_ref, 4), r = _ref2[0], g = _ref2[1], b = _ref2[2], a = _ref2[3];
3808
+ r /= 255;
3809
+ g /= 255;
3810
+ b /= 255;
3811
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
3812
+ var h = void 0, s = void 0, l = (max + min) / 2;
3813
+ if (max === min) {
3814
+ h = s = 0;
3815
+ } else {
3816
+ var d = max - min;
3817
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
3818
+ switch (max) {
3819
+ case r:
3820
+ h = (g - b) / d + (g < b ? 6 : 0);
3821
+ break;
3822
+ case g:
3823
+ h = (b - r) / d + 2;
3824
+ break;
3825
+ case b:
3826
+ h = (r - g) / d + 4;
3827
+ break;
3828
+ }
3829
+ h /= 6;
3830
+ }
3831
+ return [h, s, l, a];
3832
+ }
3833
+ }, {
3834
+ key: "hslToRgb",
3835
+ value: function hslToRgb(_ref3) {
3836
+ var _ref4 = slicedToArray(_ref3, 4), h = _ref4[0], s = _ref4[1], l = _ref4[2], a = _ref4[3];
3837
+ var r = void 0, g = void 0, b = void 0;
3838
+ if (s === 0) {
3839
+ r = g = b = l;
3840
+ } else {
3841
+ var hue2rgb = function hue2rgb2(p2, q2, t) {
3842
+ if (t < 0) t += 1;
3843
+ if (t > 1) t -= 1;
3844
+ if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
3845
+ if (t < 1 / 2) return q2;
3846
+ if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
3847
+ return p2;
3848
+ };
3849
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
3850
+ r = hue2rgb(p, q, h + 1 / 3);
3851
+ g = hue2rgb(p, q, h);
3852
+ b = hue2rgb(p, q, h - 1 / 3);
3853
+ }
3854
+ var rgba2 = [r * 255, g * 255, b * 255].map(Math.round);
3855
+ rgba2[3] = a;
3856
+ return rgba2;
3857
+ }
3858
+ }]);
3859
+ return Color2;
3860
+ })();
3861
+ var EventBucket = (function() {
3862
+ function EventBucket2() {
3863
+ classCallCheck(this, EventBucket2);
3864
+ this._events = [];
3865
+ }
3866
+ createClass(EventBucket2, [{
3867
+ key: "add",
3868
+ value: function add(target, type, handler) {
3869
+ target.addEventListener(type, handler, false);
3870
+ this._events.push({
3871
+ target,
3872
+ type,
3873
+ handler
3874
+ });
3875
+ }
3876
+ }, {
3877
+ key: "remove",
3878
+ value: function remove2(target, type, handler) {
3879
+ this._events = this._events.filter(function(e) {
3880
+ var isMatch = true;
3881
+ if (target && target !== e.target) {
3882
+ isMatch = false;
3883
+ }
3884
+ if (type && type !== e.type) {
3885
+ isMatch = false;
3886
+ }
3887
+ if (handler && handler !== e.handler) {
3888
+ isMatch = false;
3889
+ }
3890
+ if (isMatch) {
3891
+ EventBucket2._doRemove(e.target, e.type, e.handler);
3892
+ }
3893
+ return !isMatch;
3894
+ });
3895
+ }
3896
+ }, {
3897
+ key: "destroy",
3898
+ value: function destroy() {
3899
+ this._events.forEach(function(e) {
3900
+ return EventBucket2._doRemove(e.target, e.type, e.handler);
3901
+ });
3902
+ this._events = [];
3903
+ }
3904
+ }], [{
3905
+ key: "_doRemove",
3906
+ value: function _doRemove(target, type, handler) {
3907
+ target.removeEventListener(type, handler, false);
3908
+ }
3909
+ }]);
3910
+ return EventBucket2;
3911
+ })();
3912
+ function parseHTML(htmlString) {
3913
+ var div = document.createElement("div");
3914
+ div.innerHTML = htmlString;
3915
+ return div.firstElementChild;
3916
+ }
3917
+ function dragTrack(eventBucket, area, callback) {
3918
+ var dragging = false;
3919
+ function clamp(val, min, max) {
3920
+ return Math.max(min, Math.min(val, max));
3921
+ }
3922
+ function onMove(e, info, starting) {
3923
+ if (starting) {
3924
+ dragging = true;
3925
+ }
3926
+ if (!dragging) {
3927
+ return;
3928
+ }
3929
+ e.preventDefault();
3930
+ var bounds = area.getBoundingClientRect(), w = bounds.width, h = bounds.height, x = info.clientX, y = info.clientY;
3931
+ var relX = clamp(x - bounds.left, 0, w), relY = clamp(y - bounds.top, 0, h);
3932
+ callback(relX / w, relY / h);
3933
+ }
3934
+ function onMouse(e, starting) {
3935
+ var button = e.buttons === void 0 ? e.which : e.buttons;
3936
+ if (button === 1) {
3937
+ onMove(e, e, starting);
3938
+ } else {
3939
+ dragging = false;
3940
+ }
3941
+ }
3942
+ function onTouch(e, starting) {
3943
+ if (e.touches.length === 1) {
3944
+ onMove(e, e.touches[0], starting);
3945
+ } else {
3946
+ dragging = false;
3947
+ }
3948
+ }
3949
+ eventBucket.add(area, "mousedown", function(e) {
3950
+ onMouse(e, true);
3951
+ });
3952
+ eventBucket.add(area, "touchstart", function(e) {
3953
+ onTouch(e, true);
3954
+ });
3955
+ eventBucket.add(window, "mousemove", onMouse);
3956
+ eventBucket.add(area, "touchmove", onTouch);
3957
+ eventBucket.add(window, "mouseup", function(e) {
3958
+ dragging = false;
3959
+ });
3960
+ eventBucket.add(area, "touchend", function(e) {
3961
+ dragging = false;
3962
+ });
3963
+ eventBucket.add(area, "touchcancel", function(e) {
3964
+ dragging = false;
3965
+ });
3966
+ }
3967
+ 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";
3968
+ var HUES = 360;
3969
+ var EVENT_KEY = "keydown", EVENT_CLICK_OUTSIDE = "mousedown", EVENT_TAB_MOVE = "focusin";
3970
+ function $(selector2, context) {
3971
+ return (context || document).querySelector(selector2);
3972
+ }
3973
+ function stopEvent(e) {
3974
+ e.preventDefault();
3975
+ e.stopPropagation();
3976
+ }
3977
+ function onKey(bucket, target, keys, handler, stop) {
3978
+ bucket.add(target, EVENT_KEY, function(e) {
3979
+ if (keys.indexOf(e.key) >= 0) {
3980
+ handler(e);
3981
+ }
3982
+ });
3983
+ }
3984
+ var Picker = (function() {
3985
+ function Picker2(options) {
3986
+ classCallCheck(this, Picker2);
3987
+ this.settings = {
3988
+ popup: "right",
3989
+ layout: "default",
3990
+ alpha: true,
3991
+ editor: true,
3992
+ editorFormat: "hex",
3993
+ cancelButton: false,
3994
+ defaultColor: "#0cf"
3995
+ };
3996
+ this._events = new EventBucket();
3997
+ this.onChange = null;
3998
+ this.onDone = null;
3999
+ this.onOpen = null;
4000
+ this.onClose = null;
4001
+ this.setOptions(options);
4002
+ }
4003
+ createClass(Picker2, [{
4004
+ key: "setOptions",
4005
+ value: function setOptions(options) {
4006
+ var _this = this;
4007
+ if (!options) {
4008
+ return;
4009
+ }
4010
+ var settings = this.settings;
4011
+ function transfer(source, target, skipKeys) {
4012
+ for (var key in source) {
4013
+ target[key] = source[key];
4014
+ }
4015
+ }
4016
+ if (options instanceof HTMLElement) {
4017
+ settings.parent = options;
4018
+ } else {
4019
+ if (settings.parent && options.parent && settings.parent !== options.parent) {
4020
+ this._events.remove(settings.parent);
4021
+ this._popupInited = false;
4022
+ }
4023
+ transfer(options, settings);
4024
+ if (options.onChange) {
4025
+ this.onChange = options.onChange;
4026
+ }
4027
+ if (options.onDone) {
4028
+ this.onDone = options.onDone;
4029
+ }
4030
+ if (options.onOpen) {
4031
+ this.onOpen = options.onOpen;
4032
+ }
4033
+ if (options.onClose) {
4034
+ this.onClose = options.onClose;
4035
+ }
4036
+ var col = options.color || options.colour;
4037
+ if (col) {
4038
+ this._setColor(col);
4039
+ }
4040
+ }
4041
+ var parent = settings.parent;
4042
+ if (parent && settings.popup && !this._popupInited) {
4043
+ var openProxy = function openProxy2(e) {
4044
+ return _this.openHandler(e);
4045
+ };
4046
+ this._events.add(parent, "click", openProxy);
4047
+ onKey(this._events, parent, [" ", "Spacebar", "Enter"], openProxy);
4048
+ this._popupInited = true;
4049
+ } else if (options.parent && !settings.popup) {
4050
+ this.show();
4051
+ }
4052
+ }
4053
+ }, {
4054
+ key: "openHandler",
4055
+ value: function openHandler(e) {
4056
+ if (this.show()) {
4057
+ e && e.preventDefault();
4058
+ this.settings.parent.style.pointerEvents = "none";
4059
+ var toFocus = e && e.type === EVENT_KEY ? this._domEdit : this.domElement;
4060
+ setTimeout(function() {
4061
+ return toFocus.focus();
4062
+ }, 100);
4063
+ if (this.onOpen) {
4064
+ this.onOpen(this.colour);
4065
+ }
4066
+ }
4067
+ }
4068
+ }, {
4069
+ key: "closeHandler",
4070
+ value: function closeHandler(e) {
4071
+ var event = e && e.type;
4072
+ var doHide = false;
4073
+ if (!e) {
4074
+ doHide = true;
4075
+ } else if (event === EVENT_CLICK_OUTSIDE || event === EVENT_TAB_MOVE) {
4076
+ var knownTime = (this.__containedEvent || 0) + 100;
4077
+ if (e.timeStamp > knownTime) {
4078
+ doHide = true;
4079
+ }
4080
+ } else {
4081
+ stopEvent(e);
4082
+ doHide = true;
4083
+ }
4084
+ if (doHide && this.hide()) {
4085
+ this.settings.parent.style.pointerEvents = "";
4086
+ if (event !== EVENT_CLICK_OUTSIDE) {
4087
+ this.settings.parent.focus();
4088
+ }
4089
+ if (this.onClose) {
4090
+ this.onClose(this.colour);
4091
+ }
4092
+ }
4093
+ }
4094
+ }, {
4095
+ key: "movePopup",
4096
+ value: function movePopup(options, open) {
4097
+ this.closeHandler();
4098
+ this.setOptions(options);
4099
+ if (open) {
4100
+ this.openHandler();
4101
+ }
4102
+ }
4103
+ }, {
4104
+ key: "setColor",
4105
+ value: function setColor(color2, silent) {
4106
+ this._setColor(color2, { silent });
4107
+ }
4108
+ }, {
4109
+ key: "_setColor",
4110
+ value: function _setColor(color2, flags) {
4111
+ if (typeof color2 === "string") {
4112
+ color2 = color2.trim();
4113
+ }
4114
+ if (!color2) {
4115
+ return;
4116
+ }
4117
+ flags = flags || {};
4118
+ var c = void 0;
4119
+ try {
4120
+ c = new Color(color2);
4121
+ } catch (ex) {
4122
+ if (flags.failSilently) {
4123
+ return;
4124
+ }
4125
+ throw ex;
4126
+ }
4127
+ if (!this.settings.alpha) {
4128
+ var hsla2 = c.hsla;
4129
+ hsla2[3] = 1;
4130
+ c.hsla = hsla2;
4131
+ }
4132
+ this.colour = this.color = c;
4133
+ this._setHSLA(null, null, null, null, flags);
4134
+ }
4135
+ }, {
4136
+ key: "setColour",
4137
+ value: function setColour(colour, silent) {
4138
+ this.setColor(colour, silent);
4139
+ }
4140
+ }, {
4141
+ key: "show",
4142
+ value: function show() {
4143
+ var parent = this.settings.parent;
4144
+ if (!parent) {
4145
+ return false;
4146
+ }
4147
+ if (this.domElement) {
4148
+ var toggled = this._toggleDOM(true);
4149
+ this._setPosition();
4150
+ return toggled;
4151
+ }
4152
+ 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>';
4153
+ var wrapper = parseHTML(html);
4154
+ this.domElement = wrapper;
4155
+ this._domH = $(".picker_hue", wrapper);
4156
+ this._domSL = $(".picker_sl", wrapper);
4157
+ this._domA = $(".picker_alpha", wrapper);
4158
+ this._domEdit = $(".picker_editor input", wrapper);
4159
+ this._domSample = $(".picker_sample", wrapper);
4160
+ this._domOkay = $(".picker_done button", wrapper);
4161
+ this._domCancel = $(".picker_cancel button", wrapper);
4162
+ wrapper.classList.add("layout_" + this.settings.layout);
4163
+ if (!this.settings.alpha) {
4164
+ wrapper.classList.add("no_alpha");
4165
+ }
4166
+ if (!this.settings.editor) {
4167
+ wrapper.classList.add("no_editor");
4168
+ }
4169
+ if (!this.settings.cancelButton) {
4170
+ wrapper.classList.add("no_cancel");
4171
+ }
4172
+ this._ifPopup(function() {
4173
+ return wrapper.classList.add("popup");
4174
+ });
4175
+ this._setPosition();
4176
+ if (this.colour) {
4177
+ this._updateUI();
4178
+ } else {
4179
+ this._setColor(this.settings.defaultColor);
4180
+ }
4181
+ this._bindEvents();
4182
+ return true;
4183
+ }
4184
+ }, {
4185
+ key: "hide",
4186
+ value: function hide() {
4187
+ return this._toggleDOM(false);
4188
+ }
4189
+ }, {
4190
+ key: "destroy",
4191
+ value: function destroy() {
4192
+ this._events.destroy();
4193
+ if (this.domElement) {
4194
+ this.settings.parent.removeChild(this.domElement);
4195
+ }
4196
+ }
4197
+ }, {
4198
+ key: "_bindEvents",
4199
+ value: function _bindEvents() {
4200
+ var _this2 = this;
4201
+ var that = this, dom = this.domElement, events = this._events;
4202
+ function addEvent(target, type, handler) {
4203
+ events.add(target, type, handler);
4204
+ }
4205
+ addEvent(dom, "click", function(e) {
4206
+ return e.preventDefault();
4207
+ });
4208
+ dragTrack(events, this._domH, function(x, y) {
4209
+ return that._setHSLA(x);
4210
+ });
4211
+ dragTrack(events, this._domSL, function(x, y) {
4212
+ return that._setHSLA(null, x, 1 - y);
4213
+ });
4214
+ if (this.settings.alpha) {
4215
+ dragTrack(events, this._domA, function(x, y) {
4216
+ return that._setHSLA(null, null, null, 1 - y);
4217
+ });
4218
+ }
4219
+ var editInput = this._domEdit;
4220
+ {
4221
+ addEvent(editInput, "input", function(e) {
4222
+ that._setColor(this.value, { fromEditor: true, failSilently: true });
4223
+ });
4224
+ addEvent(editInput, "focus", function(e) {
4225
+ var input = this;
4226
+ if (input.selectionStart === input.selectionEnd) {
4227
+ input.select();
4228
+ }
4229
+ });
4230
+ }
4231
+ this._ifPopup(function() {
4232
+ var popupCloseProxy = function popupCloseProxy2(e) {
4233
+ return _this2.closeHandler(e);
4234
+ };
4235
+ addEvent(window, EVENT_CLICK_OUTSIDE, popupCloseProxy);
4236
+ addEvent(window, EVENT_TAB_MOVE, popupCloseProxy);
4237
+ onKey(events, dom, ["Esc", "Escape"], popupCloseProxy);
4238
+ var timeKeeper = function timeKeeper2(e) {
4239
+ _this2.__containedEvent = e.timeStamp;
4240
+ };
4241
+ addEvent(dom, EVENT_CLICK_OUTSIDE, timeKeeper);
4242
+ addEvent(dom, EVENT_TAB_MOVE, timeKeeper);
4243
+ addEvent(_this2._domCancel, "click", popupCloseProxy);
4244
+ });
4245
+ var onDoneProxy = function onDoneProxy2(e) {
4246
+ _this2._ifPopup(function() {
4247
+ return _this2.closeHandler(e);
4248
+ });
4249
+ if (_this2.onDone) {
4250
+ _this2.onDone(_this2.colour);
4251
+ }
4252
+ };
4253
+ addEvent(this._domOkay, "click", onDoneProxy);
4254
+ onKey(events, dom, ["Enter"], onDoneProxy);
4255
+ }
4256
+ }, {
4257
+ key: "_setPosition",
4258
+ value: function _setPosition() {
4259
+ var parent = this.settings.parent, elm = this.domElement;
4260
+ if (parent !== elm.parentNode) {
4261
+ parent.appendChild(elm);
4262
+ }
4263
+ this._ifPopup(function(popup) {
4264
+ if (getComputedStyle(parent).position === "static") {
4265
+ parent.style.position = "relative";
4266
+ }
4267
+ var cssClass = popup === true ? "popup_right" : "popup_" + popup;
4268
+ ["popup_top", "popup_bottom", "popup_left", "popup_right"].forEach(function(c) {
4269
+ if (c === cssClass) {
4270
+ elm.classList.add(c);
4271
+ } else {
4272
+ elm.classList.remove(c);
4273
+ }
4274
+ });
4275
+ elm.classList.add(cssClass);
4276
+ });
4277
+ }
4278
+ }, {
4279
+ key: "_setHSLA",
4280
+ value: function _setHSLA(h, s, l, a, flags) {
4281
+ flags = flags || {};
4282
+ var col = this.colour, hsla2 = col.hsla;
4283
+ [h, s, l, a].forEach(function(x, i) {
4284
+ if (x || x === 0) {
4285
+ hsla2[i] = x;
4286
+ }
4287
+ });
4288
+ col.hsla = hsla2;
4289
+ this._updateUI(flags);
4290
+ if (this.onChange && !flags.silent) {
4291
+ this.onChange(col);
4292
+ }
4293
+ }
4294
+ }, {
4295
+ key: "_updateUI",
4296
+ value: function _updateUI(flags) {
4297
+ if (!this.domElement) {
4298
+ return;
4299
+ }
4300
+ flags = flags || {};
4301
+ var col = this.colour, hsl2 = col.hsla, cssHue = "hsl(" + hsl2[0] * HUES + ", 100%, 50%)", cssHSL = col.hslString, cssHSLA = col.hslaString;
4302
+ var uiH = this._domH, uiSL = this._domSL, uiA = this._domA, thumbH = $(".picker_selector", uiH), thumbSL = $(".picker_selector", uiSL), thumbA = $(".picker_selector", uiA);
4303
+ function posX(parent, child, relX) {
4304
+ child.style.left = relX * 100 + "%";
4305
+ }
4306
+ function posY(parent, child, relY) {
4307
+ child.style.top = relY * 100 + "%";
4308
+ }
4309
+ posX(uiH, thumbH, hsl2[0]);
4310
+ this._domSL.style.backgroundColor = this._domH.style.color = cssHue;
4311
+ posX(uiSL, thumbSL, hsl2[1]);
4312
+ posY(uiSL, thumbSL, 1 - hsl2[2]);
4313
+ uiSL.style.color = cssHSL;
4314
+ posY(uiA, thumbA, 1 - hsl2[3]);
4315
+ var opaque = cssHSL, transp = opaque.replace("hsl", "hsla").replace(")", ", 0)"), bg = "linear-gradient(" + [opaque, transp] + ")";
4316
+ this._domA.style.background = bg + ", " + BG_TRANSP;
4317
+ if (!flags.fromEditor) {
4318
+ var format = this.settings.editorFormat, alpha = this.settings.alpha;
4319
+ var value = void 0;
4320
+ switch (format) {
4321
+ case "rgb":
4322
+ value = col.printRGB(alpha);
4323
+ break;
4324
+ case "hsl":
4325
+ value = col.printHSL(alpha);
4326
+ break;
4327
+ default:
4328
+ value = col.printHex(alpha);
4329
+ }
4330
+ this._domEdit.value = value;
4331
+ }
4332
+ this._domSample.style.color = cssHSLA;
4333
+ }
4334
+ }, {
4335
+ key: "_ifPopup",
4336
+ value: function _ifPopup(actionIf, actionElse) {
4337
+ if (this.settings.parent && this.settings.popup) {
4338
+ actionIf && actionIf(this.settings.popup);
4339
+ } else {
4340
+ actionElse && actionElse();
4341
+ }
4342
+ }
4343
+ }, {
4344
+ key: "_toggleDOM",
4345
+ value: function _toggleDOM(toVisible) {
4346
+ var dom = this.domElement;
4347
+ if (!dom) {
4348
+ return false;
4349
+ }
4350
+ var displayStyle = toVisible ? "" : "none", toggle = dom.style.display !== displayStyle;
4351
+ if (toggle) {
4352
+ dom.style.display = displayStyle;
4353
+ }
4354
+ return toggle;
4355
+ }
4356
+ }]);
4357
+ return Picker2;
4358
+ })();
4359
+ {
4360
+ var style = document.createElement("style");
4361
+ style.textContent = '.picker_wrapper.no_alpha .picker_alpha{display:none}.picker_wrapper.no_editor .picker_editor{position:absolute;z-index:-1;opacity:0}.picker_wrapper.no_cancel .picker_cancel{display:none}.layout_default.picker_wrapper{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:stretch;font-size:10px;width:25em;padding:.5em}.layout_default.picker_wrapper input,.layout_default.picker_wrapper button{font-size:1rem}.layout_default.picker_wrapper>*{margin:.5em}.layout_default.picker_wrapper::before{content:"";display:block;width:100%;height:0;order:1}.layout_default .picker_slider,.layout_default .picker_selector{padding:1em}.layout_default .picker_hue{width:100%}.layout_default .picker_sl{flex:1 1 auto}.layout_default .picker_sl::before{content:"";display:block;padding-bottom:100%}.layout_default .picker_editor{order:1;width:6.5rem}.layout_default .picker_editor input{width:100%;height:100%}.layout_default .picker_sample{order:1;flex:1 1 auto}.layout_default .picker_done,.layout_default .picker_cancel{order:1}.picker_wrapper{box-sizing:border-box;background:#f2f2f2;box-shadow:0 0 0 1px silver;cursor:default;font-family:sans-serif;color:#444;pointer-events:auto}.picker_wrapper:focus{outline:none}.picker_wrapper button,.picker_wrapper input{box-sizing:border-box;border:none;box-shadow:0 0 0 1px silver;outline:none}.picker_wrapper button:focus,.picker_wrapper button:active,.picker_wrapper input:focus,.picker_wrapper input:active{box-shadow:0 0 2px 1px #1e90ff}.picker_wrapper button{padding:.4em .6em;cursor:pointer;background-color:#f5f5f5;background-image:linear-gradient(0deg, gainsboro, transparent)}.picker_wrapper button:active{background-image:linear-gradient(0deg, transparent, gainsboro)}.picker_wrapper button:hover{background-color:#fff}.picker_selector{position:absolute;z-index:1;display:block;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);border:2px solid #fff;border-radius:100%;box-shadow:0 0 3px 1px #67b9ff;background:currentColor;cursor:pointer}.picker_slider .picker_selector{border-radius:2px}.picker_hue{position:relative;background-image:linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);box-shadow:0 0 0 1px silver}.picker_sl{position:relative;box-shadow:0 0 0 1px silver;background-image:linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%),linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%),linear-gradient(90deg, #808080, rgba(128, 128, 128, 0))}.picker_alpha,.picker_sample{position:relative;background:linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em,linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;box-shadow:0 0 0 1px silver}.picker_alpha .picker_selector,.picker_sample .picker_selector{background:none}.picker_editor input{font-family:monospace;padding:.2em .4em}.picker_sample::before{content:"";position:absolute;display:block;width:100%;height:100%;background:currentColor}.picker_arrow{position:absolute;z-index:-1}.picker_wrapper.popup{position:absolute;z-index:2;margin:1.5em}.picker_wrapper.popup,.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{background:#f2f2f2;box-shadow:0 0 10px 1px rgba(0,0,0,.4)}.picker_wrapper.popup .picker_arrow{width:3em;height:3em;margin:0}.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{content:"";display:block;position:absolute;top:0;left:0;z-index:-99}.picker_wrapper.popup .picker_arrow::before{width:100%;height:100%;-webkit-transform:skew(45deg);transform:skew(45deg);-webkit-transform-origin:0 100%;transform-origin:0 100%}.picker_wrapper.popup .picker_arrow::after{width:150%;height:150%;box-shadow:none}.popup.popup_top{bottom:100%;left:0}.popup.popup_top .picker_arrow{bottom:0;left:0;-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.popup.popup_bottom{top:100%;left:0}.popup.popup_bottom .picker_arrow{top:0;left:0;-webkit-transform:rotate(90deg) scale(1, -1);transform:rotate(90deg) scale(1, -1)}.popup.popup_left{top:0;right:100%}.popup.popup_left .picker_arrow{top:0;right:0;-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.popup.popup_right{top:0;left:100%}.popup.popup_right .picker_arrow{top:0;left:0}';
4362
+ document.documentElement.firstElementChild.appendChild(style);
4363
+ Picker.StyleElement = style;
4364
+ }
4365
+ class Aesthetic extends Subscribable {
3558
4366
  state;
3559
4367
  // Object containing all configuration used to infer the scale
3560
4368
  scale;
3561
4369
  // the actual scale instance
4370
+ values;
4371
+ // Store the original values for scale recreation
4372
+ defaultPalette = ["#440154", "#31688e", "#35b779", "#fde724"];
3562
4373
  constructor(values, options = {}) {
4374
+ super();
3563
4375
  if (!options.scaleType) {
3564
4376
  throw new Error("scaleType is required");
3565
4377
  }
3566
4378
  if (options.default === void 0) {
3567
4379
  throw new Error("default is required");
3568
4380
  }
4381
+ this.values = values;
3569
4382
  this.state = {
3570
4383
  scaleType: void 0,
3571
4384
  default: void 0,
3572
4385
  isCategorical: void 0,
3573
4386
  outputValues: null,
3574
4387
  outputRegex: null,
3575
- colorPalette: null,
4388
+ colorPalette: this.defaultPalette,
4389
+ colorPositions: this.defaultPalette.map((_, i) => i / (this.defaultPalette.length - 1)),
3576
4390
  outputRange: null,
3577
4391
  inputUnits: null,
3578
4392
  title: null,
@@ -3582,17 +4396,10 @@ class Aesthetic {
3582
4396
  transformMin: 0,
3583
4397
  transformMax: 1,
3584
4398
  transformFn: null,
3585
- colorPositions: null,
4399
+ nullValue: null,
3586
4400
  ...options
3587
4401
  };
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;
4402
+ this.updateScale(false);
3596
4403
  }
3597
4404
  /**
3598
4405
  * Get the output value for a given input value
@@ -3607,14 +4414,10 @@ class Aesthetic {
3607
4414
  * Update the scale based on current state
3608
4415
  * Uses the stored state to create an appropriate scale
3609
4416
  */
3610
- updateScale(values) {
4417
+ updateScale(notify = true) {
4418
+ const values = this.values;
3611
4419
  const { scaleType, isCategorical } = this.state;
3612
4420
  let scale;
3613
- if (scaleType === "null") {
3614
- scale = new NullScale(this.state.default);
3615
- this.setScale(scale);
3616
- return;
3617
- }
3618
4421
  let isAlreadyOutputFormat = true;
3619
4422
  if (isCategorical && (this.state.outputValues || this.state.outputRegex)) {
3620
4423
  if (this.state.outputValues) {
@@ -3636,77 +4439,461 @@ class Aesthetic {
3636
4439
  } else {
3637
4440
  isAlreadyOutputFormat = false;
3638
4441
  }
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") {
4442
+ if (scaleType === "null") {
4443
+ scale = new NullScale(this.state);
4444
+ this.scale = scale;
4445
+ } else if (scaleType === "identity" || isAlreadyOutputFormat) {
4446
+ scale = new IdentityScale(this.state);
4447
+ this.scale = scale;
4448
+ } else if (scaleType === "text") {
3649
4449
  if (!isCategorical) {
3650
4450
  throw new Error("Text scales can only be used with categorical data");
3651
4451
  }
3652
- scale = new CategoricalTextScale(values, this.state.outputValues, this.state.default);
3653
- this.setScale(scale);
3654
- return;
3655
- }
3656
- if (scaleType === "size") {
4452
+ scale = new CategoricalTextScale(values, this.state);
4453
+ this.scale = scale;
4454
+ } else if (scaleType === "size") {
3657
4455
  if (isCategorical) {
3658
4456
  throw new Error("Size scales can only be used with continuous data");
3659
4457
  }
3660
4458
  const numericValues = values.map((v) => Number(v)).filter((v) => !isNaN(v));
3661
4459
  if (numericValues.length === 0) {
3662
4460
  console.warn("No numeric values found for size scale, using NullScale");
3663
- scale = new NullScale(this.state.default);
4461
+ scale = new NullScale(this.state);
3664
4462
  } else {
3665
4463
  const min = Math.min(...numericValues);
3666
4464
  const max = Math.max(...numericValues);
3667
- const range = this.state.outputRange || [0.5, 2];
3668
- scale = new ContinuousSizeScale(min, max, range[0], range[1]);
4465
+ scale = new ContinuousSizeScale(min, max, this.state);
3669
4466
  }
3670
- this.setScale(scale);
3671
- return;
3672
- }
3673
- if (scaleType === "color") {
4467
+ this.scale = scale;
4468
+ } else if (scaleType === "color") {
3674
4469
  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
- );
4470
+ scale = new CategoricalColorScale(values, this.state);
3683
4471
  } else {
3684
4472
  const numericValues = values.map((v) => Number(v)).filter((v) => !isNaN(v));
3685
4473
  if (numericValues.length === 0) {
3686
4474
  console.warn("No numeric values found for color scale, using NullScale");
3687
- scale = new NullScale(this.state.default);
4475
+ scale = new NullScale(this.state);
3688
4476
  } else {
3689
4477
  const min = Math.min(...numericValues);
3690
4478
  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
- );
4479
+ scale = new ContinuousColorScale(min, max, this.state);
3699
4480
  }
3700
4481
  }
3701
- this.setScale(scale);
3702
- return;
4482
+ this.scale = scale;
4483
+ } else {
4484
+ throw new Error(`Unknown scale type: ${scaleType}`);
4485
+ }
4486
+ if (notify) {
4487
+ this.notify("aestheticChange", this);
4488
+ }
4489
+ }
4490
+ /**
4491
+ * Update aesthetic state properties
4492
+ * @param {Object} updates - Object with properties to update
4493
+ */
4494
+ updateState(updates) {
4495
+ Object.assign(this.state, updates);
4496
+ this.updateScale(this.values);
4497
+ }
4498
+ /**
4499
+ * Create settings widget(s) for this aesthetic
4500
+ * @param {Object} options - Configuration options
4501
+ * @param {number} options.controlHeight - Height of controls
4502
+ * @returns {HTMLElement|null} The settings widget container, or null if no settings available
4503
+ */
4504
+ createSettingsWidget(options = {}) {
4505
+ const {
4506
+ controlHeight = 24
4507
+ } = options;
4508
+ if (this.state.scaleType === "color") {
4509
+ return this.createColorPaletteEditor(controlHeight);
3703
4510
  }
3704
- throw new Error(`Unknown scale type: ${scaleType}`);
4511
+ return null;
3705
4512
  }
4513
+ /**
4514
+ * Create a color palette editor widget
4515
+ * @param {number} controlHeight - Height of controls
4516
+ * @returns {HTMLElement} The palette editor container
4517
+ */
4518
+ createColorPaletteEditor(controlHeight) {
4519
+ if (!this.scale) {
4520
+ return null;
4521
+ }
4522
+ const container = document.createElement("div");
4523
+ container.className = "ht-color-palette-editor";
4524
+ const gradientContainer = document.createElement("div");
4525
+ gradientContainer.className = "ht-gradient-container";
4526
+ const gradientColumn = document.createElement("div");
4527
+ gradientColumn.className = "ht-gradient-column";
4528
+ const colorSquaresContainer = document.createElement("div");
4529
+ colorSquaresContainer.className = "ht-color-squares-container";
4530
+ const gradientBox = document.createElement("div");
4531
+ gradientBox.className = "ht-gradient-box";
4532
+ const updateGradientDisplay = () => {
4533
+ const gradientStops = this.state.colorPalette.map((color2, i) => {
4534
+ const pos = this.state.colorPositions[i] * 100;
4535
+ return `${color2} ${pos}%`;
4536
+ }).join(", ");
4537
+ gradientBox.style.background = `linear-gradient(to right, ${gradientStops})`;
4538
+ };
4539
+ updateGradientDisplay();
4540
+ let currentPickerParent = null;
4541
+ const pickerContainer = document.createElement("div");
4542
+ pickerContainer.style.position = "fixed";
4543
+ pickerContainer.style.zIndex = "999999";
4544
+ pickerContainer.style.pointerEvents = "auto";
4545
+ document.body.appendChild(pickerContainer);
4546
+ const nullPickerContainer = document.createElement("div");
4547
+ nullPickerContainer.style.position = "fixed";
4548
+ nullPickerContainer.style.zIndex = "999999";
4549
+ nullPickerContainer.style.pointerEvents = "auto";
4550
+ document.body.appendChild(nullPickerContainer);
4551
+ const closeGradientPicker = () => {
4552
+ pickerContainer.style.display = "none";
4553
+ currentPickerParent = null;
4554
+ };
4555
+ const closeNullPicker = () => {
4556
+ nullPickerContainer.style.display = "none";
4557
+ };
4558
+ const nullColorColumn = document.createElement("div");
4559
+ nullColorColumn.className = "ht-null-color-column";
4560
+ const nullColorSquareContainer = document.createElement("div");
4561
+ nullColorSquareContainer.className = "ht-null-color-square-container";
4562
+ const nullSquareWrapper = document.createElement("div");
4563
+ nullSquareWrapper.style.display = "flex";
4564
+ nullSquareWrapper.style.flexDirection = "column";
4565
+ nullSquareWrapper.style.alignItems = "center";
4566
+ const nullSquare = document.createElement("div");
4567
+ nullSquare.className = "ht-null-color-square";
4568
+ nullSquare.style.backgroundColor = this.state.nullValue;
4569
+ nullSquare.title = "Click to edit missing data color";
4570
+ const nullTick = document.createElement("div");
4571
+ nullTick.className = "ht-null-color-square-tick";
4572
+ nullSquareWrapper.appendChild(nullSquare);
4573
+ nullSquareWrapper.appendChild(nullTick);
4574
+ nullColorSquareContainer.appendChild(nullSquareWrapper);
4575
+ const nullColorBox = document.createElement("div");
4576
+ nullColorBox.className = "ht-null-color-box";
4577
+ nullColorBox.style.backgroundColor = this.state.nullValue;
4578
+ const sharedPicker = new Picker({
4579
+ parent: pickerContainer,
4580
+ popup: false,
4581
+ alpha: false,
4582
+ editor: true,
4583
+ color: this.state.colorPalette[0],
4584
+ onChange: (color2) => {
4585
+ if (!currentPickerParent) return;
4586
+ const colorIndex = parseInt(currentPickerParent.getAttribute("data-color-index"));
4587
+ const hexColor = color2.hex.substring(0, 7);
4588
+ this.state.colorPalette[colorIndex] = hexColor;
4589
+ currentPickerParent.style.backgroundColor = hexColor;
4590
+ updateGradientDisplay();
4591
+ const minColor2 = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4592
+ minHandle.style.backgroundColor = minColor2;
4593
+ const maxColor2 = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4594
+ maxHandle.style.backgroundColor = maxColor2;
4595
+ this.updateScale();
4596
+ },
4597
+ onDone: () => {
4598
+ closeGradientPicker();
4599
+ }
4600
+ });
4601
+ pickerContainer.style.display = "none";
4602
+ const closePickerOnClickOutside = (e) => {
4603
+ if (!pickerContainer.contains(e.target) && !e.target.closest(".ht-color-square")) {
4604
+ closeGradientPicker();
4605
+ }
4606
+ };
4607
+ document.addEventListener("click", closePickerOnClickOutside);
4608
+ const nullColorPicker = new Picker({
4609
+ parent: nullPickerContainer,
4610
+ popup: false,
4611
+ alpha: false,
4612
+ editor: true,
4613
+ color: this.state.nullValue,
4614
+ onChange: (color2) => {
4615
+ const hexColor = color2.hex.substring(0, 7);
4616
+ this.state.nullValue = hexColor;
4617
+ nullSquare.style.backgroundColor = hexColor;
4618
+ nullColorBox.style.backgroundColor = hexColor;
4619
+ this.updateScale();
4620
+ },
4621
+ onDone: () => {
4622
+ closeNullPicker();
4623
+ }
4624
+ });
4625
+ nullPickerContainer.style.display = "none";
4626
+ const closeNullPickerOnClickOutside = (e) => {
4627
+ if (!nullPickerContainer.contains(e.target) && e.target !== nullSquare) {
4628
+ closeNullPicker();
4629
+ }
4630
+ };
4631
+ document.addEventListener("click", closeNullPickerOnClickOutside);
4632
+ container.dataset.pickerCleanup = "cleanup";
4633
+ container.cleanupFunction = () => {
4634
+ document.removeEventListener("click", closePickerOnClickOutside);
4635
+ document.removeEventListener("click", closeNullPickerOnClickOutside);
4636
+ if (pickerContainer.parentElement) {
4637
+ pickerContainer.parentElement.removeChild(pickerContainer);
4638
+ }
4639
+ if (nullPickerContainer.parentElement) {
4640
+ nullPickerContainer.parentElement.removeChild(nullPickerContainer);
4641
+ }
4642
+ };
4643
+ const recreateColorSquares = () => {
4644
+ colorSquaresContainer.innerHTML = "";
4645
+ this.state.colorPalette.forEach((color2, i) => {
4646
+ const squareContainer = createColorSquareWithTick(colorSquaresContainer, color2, i, (e) => {
4647
+ e.preventDefault();
4648
+ e.stopPropagation();
4649
+ const square = e.currentTarget;
4650
+ const colorIndex = parseInt(square.getAttribute("data-color-index"));
4651
+ closeNullPicker();
4652
+ currentPickerParent = square;
4653
+ const rect = square.getBoundingClientRect();
4654
+ sharedPicker.setColor(this.state.colorPalette[colorIndex], true);
4655
+ pickerContainer.style.left = `${rect.left}px`;
4656
+ pickerContainer.style.top = `${rect.bottom + 5}px`;
4657
+ pickerContainer.style.display = "block";
4658
+ });
4659
+ squareContainer.style.left = `${this.state.colorPositions[i] * 100}%`;
4660
+ });
4661
+ };
4662
+ recreateColorSquares();
4663
+ const rangeSliderContainer = document.createElement("div");
4664
+ rangeSliderContainer.className = "ht-range-slider-container";
4665
+ const minHandle = document.createElement("div");
4666
+ minHandle.className = "ht-range-handle";
4667
+ minHandle.style.left = `${this.state.transformMin * 100}%`;
4668
+ minHandle.title = "Drag to adjust minimum";
4669
+ let minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4670
+ minHandle.style.backgroundColor = minColor;
4671
+ const minIndicator = document.createElement("div");
4672
+ minIndicator.className = "ht-range-handle-indicator";
4673
+ minHandle.appendChild(minIndicator);
4674
+ const maxHandle = document.createElement("div");
4675
+ maxHandle.className = "ht-range-handle";
4676
+ maxHandle.style.left = `${this.state.transformMax * 100}%`;
4677
+ maxHandle.title = "Drag to adjust maximum";
4678
+ let maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4679
+ maxHandle.style.backgroundColor = maxColor;
4680
+ const maxIndicator = document.createElement("div");
4681
+ maxIndicator.className = "ht-range-handle-indicator";
4682
+ maxHandle.appendChild(maxIndicator);
4683
+ let isDraggingMin = false;
4684
+ minHandle.addEventListener("mousedown", (e) => {
4685
+ isDraggingMin = true;
4686
+ e.preventDefault();
4687
+ });
4688
+ let isDraggingMax = false;
4689
+ maxHandle.addEventListener("mousedown", (e) => {
4690
+ isDraggingMax = true;
4691
+ e.preventDefault();
4692
+ });
4693
+ const handleMouseMove = (e) => {
4694
+ if (!isDraggingMin && !isDraggingMax) return;
4695
+ const rect = rangeSliderContainer.getBoundingClientRect();
4696
+ const x = e.clientX - rect.left;
4697
+ const width = rect.width;
4698
+ let newValue = Math.max(0, Math.min(1, x / width));
4699
+ if (isDraggingMin) {
4700
+ newValue = Math.min(newValue, this.state.transformMax - 0.01);
4701
+ this.state.transformMin = newValue;
4702
+ minHandle.style.left = `${newValue * 100}%`;
4703
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, newValue);
4704
+ minHandle.style.backgroundColor = minColor;
4705
+ this.updateScale();
4706
+ } else if (isDraggingMax) {
4707
+ newValue = Math.max(newValue, this.state.transformMin + 0.01);
4708
+ this.state.transformMax = newValue;
4709
+ maxHandle.style.left = `${newValue * 100}%`;
4710
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, newValue);
4711
+ maxHandle.style.backgroundColor = maxColor;
4712
+ this.updateScale();
4713
+ }
4714
+ };
4715
+ const handleMouseUp = () => {
4716
+ isDraggingMin = false;
4717
+ isDraggingMax = false;
4718
+ };
4719
+ document.addEventListener("mousemove", handleMouseMove);
4720
+ document.addEventListener("mouseup", handleMouseUp);
4721
+ rangeSliderContainer.appendChild(minHandle);
4722
+ rangeSliderContainer.appendChild(maxHandle);
4723
+ gradientColumn.appendChild(colorSquaresContainer);
4724
+ gradientColumn.appendChild(gradientBox);
4725
+ gradientColumn.appendChild(rangeSliderContainer);
4726
+ nullSquare.addEventListener("click", (e) => {
4727
+ e.preventDefault();
4728
+ e.stopPropagation();
4729
+ closeGradientPicker();
4730
+ const rect = nullSquare.getBoundingClientRect();
4731
+ nullColorPicker.setColor(this.state.nullValue, true);
4732
+ nullPickerContainer.style.left = `${rect.left}px`;
4733
+ nullPickerContainer.style.top = `${rect.bottom + 5}px`;
4734
+ nullPickerContainer.style.display = "block";
4735
+ });
4736
+ const resetContainer = document.createElement("div");
4737
+ resetContainer.className = "ht-null-color-reset-container";
4738
+ resetContainer.title = "Reset to default missing data color";
4739
+ const resetIndicator = document.createElement("div");
4740
+ resetIndicator.className = "ht-null-color-reset-indicator";
4741
+ const resetX = document.createElement("div");
4742
+ resetX.className = "ht-null-color-reset";
4743
+ resetX.textContent = "✕";
4744
+ resetX.addEventListener("click", () => {
4745
+ const defaultNullColor = "#808080";
4746
+ this.state.nullValue = defaultNullColor;
4747
+ nullSquare.style.backgroundColor = defaultNullColor;
4748
+ nullColorBox.style.backgroundColor = defaultNullColor;
4749
+ nullColorPicker.setColor(defaultNullColor, true);
4750
+ this.updateScale();
4751
+ });
4752
+ resetContainer.appendChild(resetIndicator);
4753
+ resetContainer.appendChild(resetX);
4754
+ nullColorColumn.appendChild(nullColorSquareContainer);
4755
+ nullColorColumn.appendChild(nullColorBox);
4756
+ nullColorColumn.appendChild(resetContainer);
4757
+ gradientContainer.appendChild(gradientColumn);
4758
+ gradientContainer.appendChild(nullColorColumn);
4759
+ const leftButtonsContainer = document.createElement("div");
4760
+ leftButtonsContainer.className = "ht-palette-buttons-container";
4761
+ const leftPlusBtn = createPaletteButton("+", "Add color to left");
4762
+ leftPlusBtn.addEventListener("click", () => {
4763
+ const newColor = this.state.colorPalette[0];
4764
+ this.state.colorPalette.unshift(newColor);
4765
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4766
+ updateGradientDisplay();
4767
+ recreateColorSquares();
4768
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4769
+ minHandle.style.backgroundColor = minColor;
4770
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4771
+ maxHandle.style.backgroundColor = maxColor;
4772
+ this.updateScale();
4773
+ });
4774
+ const leftMinusBtn = createPaletteButton("-", "Remove color from left");
4775
+ leftMinusBtn.addEventListener("click", () => {
4776
+ if (this.state.colorPalette.length <= 2) {
4777
+ console.warn("Cannot remove color: minimum 2 colors required");
4778
+ return;
4779
+ }
4780
+ this.state.colorPalette.shift();
4781
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4782
+ updateGradientDisplay();
4783
+ recreateColorSquares();
4784
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4785
+ minHandle.style.backgroundColor = minColor;
4786
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4787
+ maxHandle.style.backgroundColor = maxColor;
4788
+ this.updateScale();
4789
+ });
4790
+ leftButtonsContainer.appendChild(leftPlusBtn);
4791
+ leftButtonsContainer.appendChild(leftMinusBtn);
4792
+ const rightButtonsContainer = document.createElement("div");
4793
+ rightButtonsContainer.className = "ht-palette-buttons-container";
4794
+ const rightPlusBtn = createPaletteButton("+", "Add color to right");
4795
+ rightPlusBtn.addEventListener("click", () => {
4796
+ const newColor = this.state.colorPalette[this.state.colorPalette.length - 1];
4797
+ this.state.colorPalette.push(newColor);
4798
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4799
+ updateGradientDisplay();
4800
+ recreateColorSquares();
4801
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4802
+ minHandle.style.backgroundColor = minColor;
4803
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4804
+ maxHandle.style.backgroundColor = maxColor;
4805
+ this.updateScale();
4806
+ });
4807
+ const rightMinusBtn = createPaletteButton("-", "Remove color from right");
4808
+ rightMinusBtn.addEventListener("click", () => {
4809
+ if (this.state.colorPalette.length <= 2) {
4810
+ console.warn("Cannot remove color: minimum 2 colors required");
4811
+ return;
4812
+ }
4813
+ this.state.colorPalette.pop();
4814
+ this.state.colorPositions = this.state.colorPalette.map((_, i) => i / (this.state.colorPalette.length - 1));
4815
+ updateGradientDisplay();
4816
+ recreateColorSquares();
4817
+ minColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMin);
4818
+ minHandle.style.backgroundColor = minColor;
4819
+ maxColor = interpolateGradient(this.state.colorPalette, this.state.colorPositions, this.state.transformMax);
4820
+ maxHandle.style.backgroundColor = maxColor;
4821
+ this.updateScale();
4822
+ });
4823
+ rightButtonsContainer.appendChild(rightPlusBtn);
4824
+ rightButtonsContainer.appendChild(rightMinusBtn);
4825
+ container.appendChild(leftButtonsContainer);
4826
+ container.appendChild(gradientContainer);
4827
+ container.appendChild(rightButtonsContainer);
4828
+ return container;
4829
+ }
4830
+ }
4831
+ function createPaletteButton(text, title) {
4832
+ const button = document.createElement("button");
4833
+ button.className = "ht-palette-button";
4834
+ button.textContent = text;
4835
+ button.title = title;
4836
+ return button;
4837
+ }
4838
+ function interpolateGradient(colors, positions, t) {
4839
+ t = Math.max(0, Math.min(1, t));
4840
+ if (colors.length === 1) {
4841
+ return colors[0];
4842
+ }
4843
+ let i = 0;
4844
+ while (i < positions.length - 1 && t > positions[i + 1]) {
4845
+ i++;
4846
+ }
4847
+ if (t === positions[i]) {
4848
+ return colors[i];
4849
+ }
4850
+ if (i === positions.length - 1) {
4851
+ return colors[i];
4852
+ }
4853
+ const t1 = positions[i];
4854
+ const t2 = positions[i + 1];
4855
+ const localT = (t - t1) / (t2 - t1);
4856
+ const color1 = hexToRgb(colors[i]);
4857
+ const color2 = hexToRgb(colors[i + 1]);
4858
+ const r = Math.round(color1.r + (color2.r - color1.r) * localT);
4859
+ const g = Math.round(color1.g + (color2.g - color1.g) * localT);
4860
+ const b = Math.round(color1.b + (color2.b - color1.b) * localT);
4861
+ return rgbToHex(r, g, b);
4862
+ }
4863
+ function hexToRgb(hex2) {
4864
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex2);
4865
+ return result ? {
4866
+ r: parseInt(result[1], 16),
4867
+ g: parseInt(result[2], 16),
4868
+ b: parseInt(result[3], 16)
4869
+ } : { r: 0, g: 0, b: 0 };
4870
+ }
4871
+ function rgbToHex(r, g, b) {
4872
+ return "#" + [r, g, b].map((x) => {
4873
+ const hex2 = x.toString(16);
4874
+ return hex2.length === 1 ? "0" + hex2 : hex2;
4875
+ }).join("");
4876
+ }
4877
+ function createColorSquareWithTick(parent, color2, colorIndex, clickHandler) {
4878
+ const squareContainer = document.createElement("div");
4879
+ squareContainer.className = "ht-color-square-wrapper";
4880
+ const square = document.createElement("div");
4881
+ square.className = "ht-color-square";
4882
+ square.style.backgroundColor = color2;
4883
+ square.title = "Click to edit color";
4884
+ square.setAttribute("data-color-index", colorIndex);
4885
+ square.addEventListener("click", clickHandler);
4886
+ const tick = document.createElement("div");
4887
+ tick.className = "ht-color-square-tick";
4888
+ squareContainer.appendChild(square);
4889
+ squareContainer.appendChild(tick);
4890
+ parent.appendChild(squareContainer);
4891
+ return squareContainer;
3706
4892
  }
3707
4893
  class TreeData extends Subscribable {
3708
4894
  tree;
3709
4895
  metadata = /* @__PURE__ */ new Map();
4896
+ // Map of table ID to array of row objects
3710
4897
  metadataTableNames = /* @__PURE__ */ new Map();
3711
4898
  // Map of table ID to display name
3712
4899
  columnType = /* @__PURE__ */ new Map();
@@ -3717,14 +4904,17 @@ class TreeData extends Subscribable {
3717
4904
  // Display-friendly column name, keyed by unique column ID
3718
4905
  columnAesthetic = /* @__PURE__ */ new Map();
3719
4906
  // Map of columnId -> Map of aestheticId -> Aesthetic
4907
+ nodeIdColumn = /* @__PURE__ */ new Map();
4908
+ // Map of table ID to the column name used for node IDs
4909
+ validIdColumns = /* @__PURE__ */ new Map();
4910
+ // Map of table ID to array of column names that contain valid node IDs
3720
4911
  #nextTableId = 0;
3721
4912
  constructor(newickStr, metadataTables = [], metadataTableNames = []) {
3722
4913
  super();
3723
4914
  this.tree = this.parseTree(newickStr);
3724
4915
  if (Array.isArray(metadataTables)) {
3725
4916
  metadataTables.forEach((tableStr, index) => {
3726
- const tableName = metadataTableNames[index] || `Metadata ${index + 1}`;
3727
- this.addTable(tableStr, tableName);
4917
+ this.addTable(tableStr, metadataTableNames[index]);
3728
4918
  });
3729
4919
  }
3730
4920
  }
@@ -3752,7 +4942,7 @@ class TreeData extends Subscribable {
3752
4942
  */
3753
4943
  setTree(newickStr) {
3754
4944
  this.tree = this.parseTree(newickStr);
3755
- this.metadata.keys().forEach(this.#attachTable);
4945
+ this.metadata.keys().forEach((tableId) => this.#attachTable(tableId));
3756
4946
  this.notify("treeUpdated", this);
3757
4947
  }
3758
4948
  /**
@@ -3762,6 +4952,39 @@ class TreeData extends Subscribable {
3762
4952
  getMetadataTableNames() {
3763
4953
  return Array.from(this.metadataTableNames.values());
3764
4954
  }
4955
+ /**
4956
+ * Get all node names from the tree
4957
+ * @returns {Set<string>} Set of all node names in the tree
4958
+ */
4959
+ getTreeNodeNames() {
4960
+ const nodeNames = /* @__PURE__ */ new Set();
4961
+ this.tree.each((d) => {
4962
+ if (d.data.name) {
4963
+ nodeNames.add(d.data.name);
4964
+ }
4965
+ });
4966
+ return nodeNames;
4967
+ }
4968
+ /**
4969
+ * Generate a Map from node ID to row data for a given table
4970
+ * @param {string} tableId - ID of the table
4971
+ * @returns {Map} Map from node ID to row data
4972
+ */
4973
+ #generateMetadataMap(tableId) {
4974
+ const rows = this.metadata.get(tableId);
4975
+ const idColumn = this.nodeIdColumn.get(tableId);
4976
+ if (!rows || !idColumn) {
4977
+ return /* @__PURE__ */ new Map();
4978
+ }
4979
+ const metadataMap = /* @__PURE__ */ new Map();
4980
+ for (const row of rows) {
4981
+ const nodeId = row[idColumn];
4982
+ if (nodeId) {
4983
+ metadataMap.set(nodeId, row);
4984
+ }
4985
+ }
4986
+ return metadataMap;
4987
+ }
3765
4988
  /**
3766
4989
  * Add a metadata table
3767
4990
  * @param {string} tableStr - TSV formatted string or path
@@ -3770,22 +4993,29 @@ class TreeData extends Subscribable {
3770
4993
  * @returns {string} The table ID
3771
4994
  */
3772
4995
  addTable(tableStr, tableName = null, sep = " ") {
3773
- const { metadataMap, columnTypes } = parseTable(tableStr, sep);
3774
- const id2 = `table_${this.#nextTableId++}`;
4996
+ let { metadataMap, columnTypes, idColumns } = parseTable(tableStr, this.getTreeNodeNames(), sep);
4997
+ const tableId = `table_${this.#nextTableId++}`;
3775
4998
  if (!tableName) {
3776
4999
  tableName = `Metadata ${this.#nextTableId}`;
3777
5000
  }
3778
- this.metadataTableNames.set(id2, tableName);
5001
+ this.metadataTableNames.set(tableId, tableName);
3779
5002
  const columnIdMap = /* @__PURE__ */ new Map();
3780
5003
  for (const [originalName, columnType] of columnTypes) {
3781
- const uniqueId = `${id2}_${originalName}`;
5004
+ const uniqueId = `${tableId}_${originalName}`;
3782
5005
  columnIdMap.set(originalName, uniqueId);
3783
5006
  this.columnType.set(uniqueId, columnType);
3784
5007
  this.columnName.set(uniqueId, originalName);
3785
5008
  this.columnDisplayName.set(uniqueId, columnToHeader(originalName));
3786
5009
  }
3787
- const transformedMetadata = /* @__PURE__ */ new Map();
3788
- for (const [nodeName, nodeData] of metadataMap) {
5010
+ idColumns = idColumns.map((x) => columnIdMap.get(x));
5011
+ let selectedIdColumn = null;
5012
+ if (idColumns.length > 0) {
5013
+ selectedIdColumn = idColumns[0];
5014
+ } else {
5015
+ console.warn(`No valid node ID column found in table ${tableName}`);
5016
+ }
5017
+ const metadataArray = [];
5018
+ for (const nodeData of metadataMap.values()) {
3789
5019
  const transformedNodeData = {};
3790
5020
  for (const [originalColumnName, value] of Object.entries(nodeData)) {
3791
5021
  const uniqueId = columnIdMap.get(originalColumnName);
@@ -3793,15 +5023,79 @@ class TreeData extends Subscribable {
3793
5023
  transformedNodeData[uniqueId] = value;
3794
5024
  }
3795
5025
  }
3796
- transformedMetadata.set(nodeName, transformedNodeData);
5026
+ metadataArray.push(transformedNodeData);
3797
5027
  }
3798
- this.metadata.set(id2, transformedMetadata);
3799
- this.#attachTable(id2);
5028
+ this.validIdColumns.set(tableId, idColumns);
5029
+ this.nodeIdColumn.set(tableId, selectedIdColumn);
5030
+ this.metadata.set(tableId, metadataArray);
5031
+ this.#attachTable(tableId);
3800
5032
  this.notify("metadataAdded", {
3801
- tableId: id2,
3802
- columnIds: columnIdMap.values()
5033
+ tableId,
5034
+ columnIds: Array.from(columnIdMap.values())
5035
+ });
5036
+ return tableId;
5037
+ }
5038
+ /**
5039
+ * Get valid ID columns for a table
5040
+ * @param {string} tableId - ID of the table
5041
+ * @returns {Array<string>} Array of column names that contain valid node IDs
5042
+ */
5043
+ getValidIdColumns(tableId) {
5044
+ return this.validIdColumns.get(tableId) || [];
5045
+ }
5046
+ /**
5047
+ * Get the current node ID column for a table
5048
+ * @param {string} tableId - ID of the table
5049
+ * @returns {string|null} Column name used as node ID, or null if none
5050
+ */
5051
+ getNodeIdColumn(tableId) {
5052
+ return this.nodeIdColumn.get(tableId);
5053
+ }
5054
+ /**
5055
+ * Get all column IDs for a table
5056
+ * @param {string} tableId - ID of the table
5057
+ * @returns {Array<string>} Array of column IDs in the table
5058
+ */
5059
+ getTableColumnIds(tableId) {
5060
+ const table = this.metadata.get(tableId);
5061
+ if (!table || table.length === 0) {
5062
+ return [];
5063
+ }
5064
+ return Object.keys(table[0]);
5065
+ }
5066
+ /**
5067
+ * Change the node ID column for a table
5068
+ * @param {string} tableId - ID of the table
5069
+ * @param {string} newIdColumnName - Name of the new ID column to use
5070
+ */
5071
+ setNodeIdColumn(tableId, newIdColumnName) {
5072
+ const table = this.metadata.get(tableId);
5073
+ if (!table) {
5074
+ console.warn(`Table ${tableId} does not exist`);
5075
+ return;
5076
+ }
5077
+ if (!this.validIdColumns.get(tableId).includes(newIdColumnName)) {
5078
+ console.warn(`Column ${newIdColumnName} is not a valid ID column for table ${tableId}`);
5079
+ return;
5080
+ }
5081
+ const oldIdColumn = this.nodeIdColumn.get(tableId);
5082
+ if (oldIdColumn === newIdColumnName) {
5083
+ return;
5084
+ }
5085
+ const columnIds = this.getTableColumnIds(tableId);
5086
+ for (const columnId of columnIds) {
5087
+ this.columnAesthetic.delete(columnId);
5088
+ }
5089
+ this.#detachTable(tableId);
5090
+ this.nodeIdColumn.set(tableId, newIdColumnName);
5091
+ this.#attachTable(tableId);
5092
+ this.notify("metadataChanged", {
5093
+ tableId,
5094
+ oldIdColumn,
5095
+ newIdColumn: newIdColumnName,
5096
+ columnIds,
5097
+ requiresAestheticRefresh: true
3803
5098
  });
3804
- return id2;
3805
5099
  }
3806
5100
  /**
3807
5101
  * Remove a metadata table
@@ -3813,7 +5107,7 @@ class TreeData extends Subscribable {
3813
5107
  console.warn(`Table ${tableId} does not exist`);
3814
5108
  return;
3815
5109
  }
3816
- const keys = Object.keys(table.values().next().value);
5110
+ const keys = table.length > 0 ? Object.keys(table[0]) : [];
3817
5111
  for (const uniqueId of keys) {
3818
5112
  this.columnType.delete(uniqueId);
3819
5113
  this.columnName.delete(uniqueId);
@@ -3823,7 +5117,9 @@ class TreeData extends Subscribable {
3823
5117
  this.#detachTable(tableId);
3824
5118
  this.metadata.delete(tableId);
3825
5119
  this.metadataTableNames.delete(tableId);
3826
- this.notify("metadataRemoved", {
5120
+ this.nodeIdColumn.delete(tableId);
5121
+ this.validIdColumns.delete(tableId);
5122
+ this.notify("metadataChanged", {
3827
5123
  tableId,
3828
5124
  columnIds: keys
3829
5125
  });
@@ -3893,11 +5189,11 @@ class TreeData extends Subscribable {
3893
5189
  * Add metadata to tree nodes
3894
5190
  */
3895
5191
  #attachTable(tableId) {
3896
- const table = this.metadata.get(tableId);
5192
+ const metadataMap = this.#generateMetadataMap(tableId);
3897
5193
  this.tree.each((d) => {
3898
5194
  const nodeName = d.data.name;
3899
- if (nodeName && table.has(nodeName)) {
3900
- const tableMetadata = table.get(nodeName);
5195
+ if (nodeName && metadataMap.has(nodeName)) {
5196
+ const tableMetadata = metadataMap.get(nodeName);
3901
5197
  d.metadata = { ...d.metadata, ...tableMetadata };
3902
5198
  }
3903
5199
  });
@@ -3907,11 +5203,16 @@ class TreeData extends Subscribable {
3907
5203
  */
3908
5204
  #detachTable(tableId) {
3909
5205
  const table = this.metadata.get(tableId);
3910
- const keys = Object.keys(table.values().next().value);
5206
+ if (!table || table.length === 0) {
5207
+ return;
5208
+ }
5209
+ const keys = Object.keys(table[0]);
3911
5210
  this.tree.each((d) => {
3912
- keys.forEach((key) => {
3913
- delete d[key];
3914
- });
5211
+ if (d.metadata) {
5212
+ keys.forEach((key) => {
5213
+ delete d.metadata[key];
5214
+ });
5215
+ }
3915
5216
  });
3916
5217
  }
3917
5218
  }
@@ -4208,6 +5509,7 @@ class TreeState extends Subscribable {
4208
5509
  title: "Tip label text",
4209
5510
  scaleType: "identity",
4210
5511
  default: "",
5512
+ nullValue: "",
4211
5513
  downstream: ["updateTipLabelText", "updateCoordinates"],
4212
5514
  hasLegend: false
4213
5515
  },
@@ -4215,6 +5517,7 @@ class TreeState extends Subscribable {
4215
5517
  title: "Tip label color",
4216
5518
  scaleType: "color",
4217
5519
  default: "#000000",
5520
+ nullValue: "#808080",
4218
5521
  otherCategory: "#555555",
4219
5522
  downstream: [],
4220
5523
  hasLegend: true
@@ -4223,6 +5526,7 @@ class TreeState extends Subscribable {
4223
5526
  title: "Tip label size",
4224
5527
  scaleType: "size",
4225
5528
  default: 1,
5529
+ nullValue: 1,
4226
5530
  isCategorical: false,
4227
5531
  outputRange: [0.5, 2],
4228
5532
  downstream: ["updateCoordinates"],
@@ -4232,6 +5536,7 @@ class TreeState extends Subscribable {
4232
5536
  title: "Tip label font",
4233
5537
  scaleType: "identity",
4234
5538
  default: "sans-serif",
5539
+ nullValue: "sans-serif",
4235
5540
  downstream: ["updateCoordinates"],
4236
5541
  hasLegend: false
4237
5542
  },
@@ -4240,6 +5545,7 @@ class TreeState extends Subscribable {
4240
5545
  scaleType: "text",
4241
5546
  outputValues: ["normal", "bold", "italic", "bold italic"],
4242
5547
  default: "normal",
5548
+ nullValue: "normal",
4243
5549
  otherCategory: "italic",
4244
5550
  downstream: ["updateCoordinates"],
4245
5551
  hasLegend: false
@@ -4248,6 +5554,7 @@ class TreeState extends Subscribable {
4248
5554
  title: "Node label text",
4249
5555
  scaleType: "identity",
4250
5556
  default: "",
5557
+ nullValue: "",
4251
5558
  downstream: ["updateNodeLabelText"],
4252
5559
  hasLegend: false
4253
5560
  },
@@ -4255,6 +5562,7 @@ class TreeState extends Subscribable {
4255
5562
  title: "Node label size",
4256
5563
  scaleType: "size",
4257
5564
  default: 1,
5565
+ nullValue: 1,
4258
5566
  isCategorical: false,
4259
5567
  outputRange: [0.5, 2],
4260
5568
  downstream: ["updateCoordinates"],
@@ -4306,8 +5614,22 @@ class TreeState extends Subscribable {
4306
5614
  this.state.treeData.subscribe("treeUpdate", () => {
4307
5615
  this.#initalize();
4308
5616
  });
4309
- this.state.treeData.subscribe("metadataRemoved", (info) => {
4310
- this.setAesthetics(Object.fromEntries(info.columnIds.map((key) => [key, void 0])));
5617
+ this.state.treeData.subscribe("metadataChanged", (info) => {
5618
+ if (info.columnIds && Array.isArray(info.columnIds)) {
5619
+ if (info.requiresAestheticRefresh) {
5620
+ const aestheticsToRefresh = {};
5621
+ for (const [aestheticId, columnId] of Object.entries(this.state.aesthetics)) {
5622
+ if (columnId && info.columnIds.includes(columnId)) {
5623
+ aestheticsToRefresh[aestheticId] = columnId;
5624
+ }
5625
+ }
5626
+ if (Object.keys(aestheticsToRefresh).length > 0) {
5627
+ this.setAesthetics(aestheticsToRefresh, true);
5628
+ }
5629
+ } else {
5630
+ this.setAesthetics(Object.fromEntries(info.columnIds.map((key) => [key, void 0])));
5631
+ }
5632
+ }
4311
5633
  });
4312
5634
  }
4313
5635
  #initalize() {
@@ -4357,21 +5679,16 @@ class TreeState extends Subscribable {
4357
5679
  if (force || columnId !== this.state.aesthetics[aestheticId]) {
4358
5680
  this.state.aesthetics[aestheticId] = columnId;
4359
5681
  if (!columnId) {
4360
- this.aestheticsScales[aestheticId] = new NullScale(aesData.default);
5682
+ this.aestheticsScales[aestheticId] = new NullScale({ default: aesData.default });
4361
5683
  } else {
4362
5684
  this.aestheticsScales[aestheticId] = this.state.treeData.getAesthetic(columnId, aestheticId, aesData);
5685
+ this.aestheticsScales[aestheticId].subscribe("aestheticChange", () => {
5686
+ this.#updateTreeDataForAesthetic(aestheticId, columnId);
5687
+ this.#updateLegends();
5688
+ this.notify(`${aestheticId}Change`);
5689
+ });
4363
5690
  }
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
- });
5691
+ this.#updateTreeDataForAesthetic(aestheticId, columnId);
4375
5692
  for (const methodName of aesData.downstream) {
4376
5693
  downstreams.add(methodName);
4377
5694
  }
@@ -4388,6 +5705,24 @@ class TreeState extends Subscribable {
4388
5705
  this[methodName]();
4389
5706
  }
4390
5707
  }
5708
+ /**
5709
+ * Update tree data for a specific aesthetic
5710
+ * @private
5711
+ */
5712
+ #updateTreeDataForAesthetic(aestheticId, columnId) {
5713
+ const aesData = this.#AESTHETICS[aestheticId];
5714
+ this.state.treeData.tree.each((d) => {
5715
+ if (columnId && columnId !== null && columnId !== void 0) {
5716
+ if (d.metadata) {
5717
+ d[aestheticId] = this.aestheticsScales[aestheticId].getValue(d.metadata[columnId]);
5718
+ } else {
5719
+ d[aestheticId] = aesData.default;
5720
+ }
5721
+ } else {
5722
+ d[aestheticId] = this.aestheticsScales[aestheticId].getValue();
5723
+ }
5724
+ });
5725
+ }
4391
5726
  #updateLegends() {
4392
5727
  this.legends = [];
4393
5728
  for (const [aestheticId, columnId] of Object.entries(this.state.aesthetics)) {
@@ -4727,6 +6062,10 @@ const ICONS = {
4727
6062
  "m 17,4 c 0,0 9,8 0,16",
4728
6063
  "M 17,8 V 4 h 4",
4729
6064
  "M 21,20 H 17 V 16"
6065
+ ],
6066
+ edit: [
6067
+ "m 12,8 -8,8 -1,5 5,-1 8,-8 z",
6068
+ "M 21,7 18,10 14,6 17,3 Z"
4730
6069
  ]
4731
6070
  };
4732
6071
  function appendIcon(svgSel, name, size, padding = 2) {
@@ -4899,8 +6238,8 @@ class TextSizeLegend extends LegendBase {
4899
6238
  updateCoordinates() {
4900
6239
  const minValue = this.state.aesthetic.scale.dataMin;
4901
6240
  const maxValue = this.state.aesthetic.scale.dataMax;
4902
- const minSize = this.state.aesthetic.scale.sizeMin;
4903
- const maxSize = this.state.aesthetic.scale.sizeMax;
6241
+ const minSize = this.state.aesthetic.state.outputRange[0];
6242
+ const maxSize = this.state.aesthetic.state.outputRange[1];
4904
6243
  const ticks = generateNiceTicks(minValue, maxValue, 5);
4905
6244
  const maxLetterFont = maxSize * this.state.treeState.labelSizeToPxFactor * 0.7;
4906
6245
  const minLetterFont = minSize * this.state.treeState.labelSizeToPxFactor * 0.7;
@@ -4939,8 +6278,8 @@ class TextSizeLegend extends LegendBase {
4939
6278
  text: this.state.aesthetic.state.inputUnits || ""
4940
6279
  }
4941
6280
  };
4942
- ticks.forEach((tickValue, i) => {
4943
- const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6281
+ if (minValue === maxValue) {
6282
+ const x = leftOverhang + baseWidth / 2;
4944
6283
  this.coordinates.ticks.push({
4945
6284
  x1: x,
4946
6285
  y1: rampBaseY,
@@ -4950,15 +6289,37 @@ class TextSizeLegend extends LegendBase {
4950
6289
  this.coordinates.labels.push({
4951
6290
  x,
4952
6291
  y: rampBaseY + this.tickHeight,
4953
- text: formatTickLabel(tickValue, ticks)
6292
+ text: formatTickLabel(minValue, ticks)
4954
6293
  });
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
- ];
6294
+ const avgLetterFont = (minLetterFont + maxLetterFont) / 2;
6295
+ this.coordinates.polygon = [
6296
+ { x: leftOverhang, y: rampBaseY },
6297
+ { x: leftOverhang, y: rampBaseY - avgLetterFont },
6298
+ { x: leftOverhang + baseWidth, y: rampBaseY - avgLetterFont },
6299
+ { x: leftOverhang + baseWidth, y: rampBaseY }
6300
+ ];
6301
+ } else {
6302
+ ticks.forEach((tickValue, i) => {
6303
+ const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6304
+ this.coordinates.ticks.push({
6305
+ x1: x,
6306
+ y1: rampBaseY,
6307
+ x2: x,
6308
+ y2: rampBaseY + this.tickHeight
6309
+ });
6310
+ this.coordinates.labels.push({
6311
+ x,
6312
+ y: rampBaseY + this.tickHeight,
6313
+ text: formatTickLabel(tickValue, ticks)
6314
+ });
6315
+ });
6316
+ this.coordinates.polygon = [
6317
+ { x: leftOverhang, y: rampBaseY },
6318
+ { x: leftOverhang, y: rampBaseY - minLetterFont },
6319
+ { x: leftOverhang + baseWidth, y: rampBaseY - maxLetterFont },
6320
+ { x: leftOverhang + baseWidth, y: rampBaseY }
6321
+ ];
6322
+ }
4962
6323
  }
4963
6324
  /**
4964
6325
  * Render the legend in the specified SVG element
@@ -5035,7 +6396,7 @@ class TextColorLegend extends LegendBase {
5035
6396
  let currentX = 0;
5036
6397
  let currentY = titleHeightOffset + this.verticalSpacing + this.squareSize / 2;
5037
6398
  let rowHeight = this.squareSize;
5038
- categories.slice(0, aesthetic.scale.maxColors).forEach((category, i) => {
6399
+ categories.slice(0, aesthetic.state.maxCategories).forEach((category, i) => {
5039
6400
  const color2 = aesthetic.scale.getValue(category);
5040
6401
  const labelSize = this.textSizeEstimator.getTextSize(category, this.state.labelFontSize);
5041
6402
  const itemWidth = this.squareSize + this.itemLabelGap + labelSize.widthPx;
@@ -5048,7 +6409,7 @@ class TextColorLegend extends LegendBase {
5048
6409
  x: currentX,
5049
6410
  y: currentY,
5050
6411
  color: color2,
5051
- label: i < aesthetic.scale.maxColors - 1 ? category : aesthetic.state.otherLabel,
6412
+ label: i < aesthetic.state.maxCategories - 1 ? category : aesthetic.state.otherLabel,
5052
6413
  squareX: currentX,
5053
6414
  squareY: currentY - this.squareSize / 2,
5054
6415
  labelX: currentX + this.squareSize + this.itemLabelGap,
@@ -5105,8 +6466,8 @@ class TextColorLegend extends LegendBase {
5105
6466
  text: aesthetic.state.inputUnits || ""
5106
6467
  }
5107
6468
  };
5108
- ticks.forEach((tickValue, i) => {
5109
- const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6469
+ if (minValue === maxValue) {
6470
+ const x = leftOverhang + baseWidth / 2;
5110
6471
  this.coordinates.ticks.push({
5111
6472
  x1: x,
5112
6473
  y1: ticksY,
@@ -5116,9 +6477,24 @@ class TextColorLegend extends LegendBase {
5116
6477
  this.coordinates.labels.push({
5117
6478
  x,
5118
6479
  y: labelsY,
5119
- text: formatTickLabel(tickValue, ticks)
6480
+ text: formatTickLabel(minValue, ticks)
5120
6481
  });
5121
- });
6482
+ } else {
6483
+ ticks.forEach((tickValue, i) => {
6484
+ const x = leftOverhang + i / (ticks.length - 1) * baseWidth;
6485
+ this.coordinates.ticks.push({
6486
+ x1: x,
6487
+ y1: ticksY,
6488
+ x2: x,
6489
+ y2: ticksY + this.tickHeight
6490
+ });
6491
+ this.coordinates.labels.push({
6492
+ x,
6493
+ y: labelsY,
6494
+ text: formatTickLabel(tickValue, ticks)
6495
+ });
6496
+ });
6497
+ }
5122
6498
  }
5123
6499
  /**
5124
6500
  * Render the legend in the specified SVG element
@@ -5154,11 +6530,17 @@ class TextColorLegend extends LegendBase {
5154
6530
  const gradientId = `color-gradient-${Math.random().toString(36).substr(2, 9)}`;
5155
6531
  const defs = this.group.append("defs");
5156
6532
  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);
6533
+ if (minValue === maxValue) {
6534
+ const color2 = aesthetic.scale.getValue(minValue);
6535
+ gradient.append("stop").attr("offset", "0%").attr("stop-color", color2);
6536
+ gradient.append("stop").attr("offset", "100%").attr("stop-color", color2);
6537
+ } else {
6538
+ for (let i = 0; i <= this.numGradientStops; i++) {
6539
+ const t = i / this.numGradientStops;
6540
+ const value = minValue + t * (maxValue - minValue);
6541
+ const color2 = aesthetic.scale.getValue(value);
6542
+ gradient.append("stop").attr("offset", `${t * 100}%`).attr("stop-color", color2);
6543
+ }
5162
6544
  }
5163
6545
  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
6546
  this.coordinates.ticks.forEach((tick) => {
@@ -6264,10 +7646,75 @@ function convertSvgToPng(svgString, width, height, filename) {
6264
7646
  };
6265
7647
  img.src = url;
6266
7648
  }
7649
+ function createControlGroup() {
7650
+ const group = document.createElement("div");
7651
+ group.className = "ht-control-group";
7652
+ return group;
7653
+ }
7654
+ function createLabel(text, height) {
7655
+ const label = document.createElement("label");
7656
+ label.className = "ht-control-label";
7657
+ label.textContent = text;
7658
+ label.style.height = `${height}px`;
7659
+ return label;
7660
+ }
7661
+ function createButton(text, title = "", height) {
7662
+ const button = document.createElement("button");
7663
+ button.className = "ht-button";
7664
+ button.textContent = text;
7665
+ button.title = title;
7666
+ button.style.height = `${height}px`;
7667
+ return button;
7668
+ }
7669
+ function createIconButton(iconSvg, title = "", height) {
7670
+ const button = document.createElement("button");
7671
+ button.className = "ht-icon-button";
7672
+ button.innerHTML = iconSvg;
7673
+ button.title = title;
7674
+ button.style.height = `${height}px`;
7675
+ button.style.width = `${height}px`;
7676
+ return button;
7677
+ }
7678
+ function createSlider(min, max, value, step, height) {
7679
+ const slider = document.createElement("input");
7680
+ slider.type = "range";
7681
+ slider.className = "ht-slider";
7682
+ slider.min = min;
7683
+ slider.max = max;
7684
+ slider.value = value;
7685
+ slider.step = step;
7686
+ slider.style.height = `${height}px`;
7687
+ return slider;
7688
+ }
7689
+ function createToggle(initialState, height) {
7690
+ const toggleHeight = Math.min(24, height - 4);
7691
+ const knobSize = toggleHeight - 4;
7692
+ const toggle = document.createElement("div");
7693
+ toggle.className = initialState ? "ht-toggle active" : "ht-toggle";
7694
+ toggle.style.height = `${toggleHeight}px`;
7695
+ const knob = document.createElement("div");
7696
+ knob.className = "ht-toggle-knob";
7697
+ knob.style.width = `${knobSize}px`;
7698
+ knob.style.height = `${knobSize}px`;
7699
+ toggle.appendChild(knob);
7700
+ return toggle;
7701
+ }
7702
+ function createNumberInput(value, min, max, step, height) {
7703
+ const input = document.createElement("input");
7704
+ input.type = "number";
7705
+ input.className = "ht-number-input";
7706
+ input.value = value;
7707
+ input.min = min;
7708
+ input.max = max;
7709
+ input.step = step;
7710
+ input.style.height = `${height}px`;
7711
+ return input;
7712
+ }
6267
7713
  function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCurrentTreeView, switchToTree, addNewTree, options) {
6268
7714
  const CONTROL_HEIGHT = 24;
6269
7715
  let currentTab = null;
6270
7716
  let selectedMetadata = null;
7717
+ let currentAestheticSettings = null;
6271
7718
  let expandSubtreesBtn = null;
6272
7719
  let expandRootBtn = null;
6273
7720
  let showHiddenBtn = null;
@@ -6295,6 +7742,8 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6295
7742
  tabsContainer.className = "ht-tabs";
6296
7743
  const controlsContainer = document.createElement("div");
6297
7744
  controlsContainer.className = "ht-controls hidden";
7745
+ const aestheticSettingsContainer = document.createElement("div");
7746
+ aestheticSettingsContainer.className = "ht-aesthetic-settings hidden";
6298
7747
  const tabs = [
6299
7748
  { id: "data", label: "Data", requiresTree: false },
6300
7749
  { id: "controls", label: "Controls", requiresTree: true },
@@ -6311,6 +7760,7 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6311
7760
  if (tabDiv.classList.contains("disabled")) {
6312
7761
  return;
6313
7762
  }
7763
+ closeAestheticSettings();
6314
7764
  if (currentTab === tab.id) {
6315
7765
  closeTab();
6316
7766
  } else {
@@ -6320,6 +7770,14 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6320
7770
  tabElements[tab.id] = tabDiv;
6321
7771
  tabsContainer.appendChild(tabDiv);
6322
7772
  });
7773
+ controlsContainer.addEventListener("click", (e) => {
7774
+ const editButton = e.target.closest(".ht-icon-button");
7775
+ const aestheticGroup = e.target.closest(".ht-control-group.ht-aesthetic-editing");
7776
+ if (editButton || aestheticGroup) {
7777
+ return;
7778
+ }
7779
+ closeAestheticSettings();
7780
+ });
6323
7781
  function updateTabStates() {
6324
7782
  const hasTree = getCurrentTreeState() !== null;
6325
7783
  tabs.forEach((tab) => {
@@ -6402,6 +7860,44 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6402
7860
  }
6403
7861
  return "tree";
6404
7862
  }
7863
+ function openAestheticSettings(aestheticId, aestheticGroup) {
7864
+ if (currentAestheticSettings === aestheticId) {
7865
+ closeAestheticSettings();
7866
+ return;
7867
+ }
7868
+ closeAestheticSettings();
7869
+ currentAestheticSettings = aestheticId;
7870
+ aestheticGroup.classList.add("ht-aesthetic-editing");
7871
+ aestheticSettingsContainer.classList.remove("hidden");
7872
+ populateAestheticSettings(aestheticId);
7873
+ }
7874
+ function closeAestheticSettings() {
7875
+ if (!currentAestheticSettings) return;
7876
+ const existingEditors = aestheticSettingsContainer.querySelectorAll(".ht-color-palette-editor");
7877
+ existingEditors.forEach((editor) => {
7878
+ if (editor.cleanupFunction && typeof editor.cleanupFunction === "function") {
7879
+ editor.cleanupFunction();
7880
+ }
7881
+ });
7882
+ const allGroups = controlsContainer.querySelectorAll(".ht-control-group");
7883
+ allGroups.forEach((group) => group.classList.remove("ht-aesthetic-editing"));
7884
+ aestheticSettingsContainer.classList.add("hidden");
7885
+ aestheticSettingsContainer.innerHTML = "";
7886
+ currentAestheticSettings = null;
7887
+ }
7888
+ function populateAestheticSettings(aestheticId) {
7889
+ aestheticSettingsContainer.innerHTML = "";
7890
+ const treeState = getCurrentTreeState();
7891
+ if (!treeState) return;
7892
+ if (aestheticId === "tipLabelColor") {
7893
+ populateTipLabelColorSettings(aestheticSettingsContainer, treeState, CONTROL_HEIGHT);
7894
+ return;
7895
+ }
7896
+ const placeholder = document.createElement("div");
7897
+ placeholder.textContent = `Settings for ${aestheticId} (coming soon)`;
7898
+ placeholder.style.padding = "10px";
7899
+ aestheticSettingsContainer.appendChild(placeholder);
7900
+ }
6405
7901
  function openTab(tabId) {
6406
7902
  const tabDef = tabs.find((t) => t.id === tabId);
6407
7903
  if (tabDef && tabDef.requiresTree && !getCurrentTreeState()) {
@@ -6425,9 +7921,11 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6425
7921
  });
6426
7922
  controlsContainer.classList.add("hidden");
6427
7923
  controlsContainer.innerHTML = "";
7924
+ closeAestheticSettings();
6428
7925
  }
6429
7926
  function populateControls(tabId) {
6430
7927
  controlsContainer.innerHTML = "";
7928
+ closeAestheticSettings();
6431
7929
  switch (tabId) {
6432
7930
  case "data":
6433
7931
  populateDataControls(
@@ -6474,7 +7972,16 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6474
7972
  );
6475
7973
  break;
6476
7974
  case "tip-label-settings":
6477
- populateTipLabelSettingsControls(controlsContainer, getCurrentTreeState, options, CONTROL_HEIGHT);
7975
+ populateTipLabelSettingsControls(
7976
+ controlsContainer,
7977
+ getCurrentTreeState,
7978
+ options,
7979
+ CONTROL_HEIGHT,
7980
+ openAestheticSettings,
7981
+ closeAestheticSettings,
7982
+ populateAestheticSettings,
7983
+ () => currentAestheticSettings
7984
+ );
6478
7985
  break;
6479
7986
  case "export":
6480
7987
  populateExportControls(controlsContainer, getCurrentTreeState, getCurrentTreeView, getCurrentTreeName, options, CONTROL_HEIGHT);
@@ -6501,6 +8008,7 @@ function createToolbar(toolbarDiv, treeDataInstances, getCurrentTreeState, getCu
6501
8008
  }
6502
8009
  collapsiblePanel.appendChild(tabsContainer);
6503
8010
  collapsiblePanel.appendChild(controlsContainer);
8011
+ collapsiblePanel.appendChild(aestheticSettingsContainer);
6504
8012
  toolbarDiv.appendChild(toggleContainer);
6505
8013
  toolbarDiv.appendChild(collapsiblePanel);
6506
8014
  updateTabStates();
@@ -6601,6 +8109,7 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6601
8109
  });
6602
8110
  metadataSelect.addEventListener("change", (e) => {
6603
8111
  setSelectedMetadata(e.target.value);
8112
+ refreshCurrentTab();
6604
8113
  });
6605
8114
  }
6606
8115
  metadataGroup.appendChild(metadataSelect);
@@ -6647,6 +8156,51 @@ function populateDataControls(container, treeDataInstances, getCurrentTreeState,
6647
8156
  metadataFileInput.click();
6648
8157
  });
6649
8158
  container.appendChild(addMetadataBtn);
8159
+ if (selectedMetadata) {
8160
+ const treeData = currentTreeState.state.treeData;
8161
+ let selectedTableId = null;
8162
+ for (const [tableId, tableName] of treeData.metadataTableNames.entries()) {
8163
+ if (tableName === selectedMetadata) {
8164
+ selectedTableId = tableId;
8165
+ break;
8166
+ }
8167
+ }
8168
+ if (selectedTableId) {
8169
+ const validIdColumns = treeData.getValidIdColumns(selectedTableId);
8170
+ const currentIdColumn = treeData.getNodeIdColumn(selectedTableId);
8171
+ const nodeIdGroup = createControlGroup();
8172
+ const nodeIdLabel = createLabel("Node/Tip ID Column:", controlHeight);
8173
+ nodeIdGroup.appendChild(nodeIdLabel);
8174
+ const nodeIdSelect = document.createElement("select");
8175
+ nodeIdSelect.className = "ht-select";
8176
+ nodeIdSelect.style.height = `${controlHeight}px`;
8177
+ if (validIdColumns.length === 0) {
8178
+ const option = document.createElement("option");
8179
+ option.textContent = "No ID column found";
8180
+ option.value = "";
8181
+ nodeIdSelect.appendChild(option);
8182
+ nodeIdSelect.disabled = true;
8183
+ } else {
8184
+ validIdColumns.forEach((columnName) => {
8185
+ const option = document.createElement("option");
8186
+ option.value = columnName;
8187
+ option.textContent = treeData.columnDisplayName.get(columnName);
8188
+ if (columnName === currentIdColumn) {
8189
+ option.selected = true;
8190
+ }
8191
+ nodeIdSelect.appendChild(option);
8192
+ });
8193
+ nodeIdSelect.addEventListener("change", (e) => {
8194
+ const newIdColumn = e.target.value;
8195
+ treeData.setNodeIdColumn(selectedTableId, newIdColumn);
8196
+ currentTreeState.updateCoordinates();
8197
+ refreshCurrentTab();
8198
+ });
8199
+ }
8200
+ nodeIdGroup.appendChild(nodeIdSelect);
8201
+ container.appendChild(nodeIdGroup);
8202
+ }
8203
+ }
6650
8204
  }
6651
8205
  function populateControlsTab(container, getCurrentTreeState, getCurrentTreeView, options, controlHeight) {
6652
8206
  container.innerHTML = "";
@@ -6845,13 +8399,19 @@ function populateTreeManipulationControls(container, getCurrentTreeState, refres
6845
8399
  radialLayoutGroup.appendChild(radialLayoutToggle);
6846
8400
  container.appendChild(radialLayoutGroup);
6847
8401
  }
6848
- function populateTipLabelSettingsControls(container, getCurrentTreeState, options, controlHeight) {
8402
+ function populateTipLabelSettingsControls(container, getCurrentTreeState, options, controlHeight, openAestheticSettings, closeAestheticSettings, populateAestheticSettings, getCurrentAestheticSettings) {
6849
8403
  container.innerHTML = "";
6850
8404
  const treeState = getCurrentTreeState();
6851
8405
  if (!treeState) {
6852
8406
  container.textContent = "No tree selected";
6853
8407
  return;
6854
8408
  }
8409
+ const editIconSvg = `
8410
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
8411
+ <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"/>
8412
+ <path d="M10 3L13 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
8413
+ </svg>
8414
+ `;
6855
8415
  const tipLabelTextGroup = createControlGroup();
6856
8416
  const tipLabelTextLabel = createLabel("Text:", controlHeight);
6857
8417
  tipLabelTextGroup.appendChild(tipLabelTextLabel);
@@ -6861,20 +8421,30 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6861
8421
  "Default",
6862
8422
  controlHeight,
6863
8423
  true,
6864
- null
8424
+ null,
8425
+ getCurrentAestheticSettings,
8426
+ populateAestheticSettings
6865
8427
  );
6866
8428
  tipLabelTextGroup.appendChild(tipLabelTextSelect);
6867
8429
  container.appendChild(tipLabelTextGroup);
6868
8430
  const tipLabelColorGroup = createControlGroup();
6869
8431
  const tipLabelColorLabel = createLabel("Color:", controlHeight);
6870
8432
  tipLabelColorGroup.appendChild(tipLabelColorLabel);
8433
+ const tipLabelColorEditBtn = createIconButton(editIconSvg, "Edit color settings", controlHeight);
8434
+ tipLabelColorEditBtn.addEventListener("click", (e) => {
8435
+ e.stopPropagation();
8436
+ openAestheticSettings("tipLabelColor", tipLabelColorGroup);
8437
+ });
8438
+ tipLabelColorGroup.appendChild(tipLabelColorEditBtn);
6871
8439
  const tipLabelColorSelect = createMetadataColumnSelect(
6872
8440
  treeState,
6873
8441
  "tipLabelColor",
6874
8442
  "Default",
6875
8443
  controlHeight,
6876
8444
  false,
6877
- null
8445
+ null,
8446
+ getCurrentAestheticSettings,
8447
+ populateAestheticSettings
6878
8448
  );
6879
8449
  tipLabelColorGroup.appendChild(tipLabelColorSelect);
6880
8450
  container.appendChild(tipLabelColorGroup);
@@ -6887,7 +8457,9 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6887
8457
  "Default",
6888
8458
  controlHeight,
6889
8459
  false,
6890
- true
8460
+ true,
8461
+ getCurrentAestheticSettings,
8462
+ populateAestheticSettings
6891
8463
  );
6892
8464
  tipLabelSizeGroup.appendChild(tipLabelSizeSelect);
6893
8465
  container.appendChild(tipLabelSizeGroup);
@@ -6900,7 +8472,9 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6900
8472
  "Default",
6901
8473
  controlHeight,
6902
8474
  false,
6903
- false
8475
+ false,
8476
+ getCurrentAestheticSettings,
8477
+ populateAestheticSettings
6904
8478
  );
6905
8479
  tipLabelStyleGroup.appendChild(tipLabelStyleSelect);
6906
8480
  container.appendChild(tipLabelStyleGroup);
@@ -6932,7 +8506,102 @@ function populateTipLabelSettingsControls(container, getCurrentTreeState, option
6932
8506
  tipLabelFontGroup.appendChild(tipLabelFontSelect);
6933
8507
  container.appendChild(tipLabelFontGroup);
6934
8508
  }
6935
- function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null) {
8509
+ function populateTipLabelColorSettings(container, treeState, controlHeight) {
8510
+ const columnId = treeState.state.aesthetics.tipLabelColor;
8511
+ if (!columnId) {
8512
+ const message = document.createElement("div");
8513
+ message.textContent = "Select a metadata column for tip label color to edit its settings";
8514
+ message.style.padding = "10px";
8515
+ message.style.color = "#666";
8516
+ container.appendChild(message);
8517
+ return;
8518
+ }
8519
+ const aesthetic = treeState.aestheticsScales.tipLabelColor;
8520
+ if (!aesthetic) {
8521
+ const message = document.createElement("div");
8522
+ message.textContent = "Error: Could not find color aesthetic";
8523
+ message.style.padding = "10px";
8524
+ message.style.color = "#d00";
8525
+ container.appendChild(message);
8526
+ return;
8527
+ }
8528
+ const settingsWidget = aesthetic.createSettingsWidget({
8529
+ controlHeight
8530
+ });
8531
+ if (settingsWidget) {
8532
+ container.appendChild(settingsWidget);
8533
+ } else {
8534
+ const message = document.createElement("div");
8535
+ message.textContent = "No settings available for this aesthetic";
8536
+ message.style.padding = "10px";
8537
+ message.style.color = "#666";
8538
+ container.appendChild(message);
8539
+ return;
8540
+ }
8541
+ if (aesthetic.state.isCategorical) {
8542
+ const maxCategoriesGroup = createControlGroup();
8543
+ const maxCategoriesLabel = createLabel("Max colors:", controlHeight);
8544
+ maxCategoriesGroup.appendChild(maxCategoriesLabel);
8545
+ const maxCategoriesInput = createNumberInput(
8546
+ aesthetic.state.maxCategories || 7,
8547
+ 1,
8548
+ 100,
8549
+ 1,
8550
+ controlHeight
8551
+ );
8552
+ maxCategoriesInput.addEventListener("input", (e) => {
8553
+ const value = parseInt(e.target.value);
8554
+ if (isNaN(value) || value < 1) return;
8555
+ aesthetic.updateState({ maxCategories: value });
8556
+ aesthetic.updateScale(aesthetic.values);
8557
+ treeState.state.treeData.tree.each((node) => {
8558
+ const columnValue = node[columnId];
8559
+ if (columnValue !== void 0 && columnValue !== null) {
8560
+ node.tipLabelColor = aesthetic.getValue(columnValue);
8561
+ }
8562
+ });
8563
+ treeState.updateCoordinates();
8564
+ treeState.notify("legendsChange");
8565
+ });
8566
+ maxCategoriesGroup.appendChild(maxCategoriesInput);
8567
+ container.appendChild(maxCategoriesGroup);
8568
+ }
8569
+ const titleGroup = createControlGroup();
8570
+ const titleLabel = createLabel("Legend title:", controlHeight);
8571
+ titleGroup.appendChild(titleLabel);
8572
+ const titleInput = document.createElement("input");
8573
+ titleInput.type = "text";
8574
+ titleInput.className = "ht-text-input";
8575
+ titleInput.style.height = `${controlHeight}px`;
8576
+ titleInput.style.flex = "1";
8577
+ titleInput.value = aesthetic.state.title || "";
8578
+ titleInput.placeholder = "Enter legend title";
8579
+ titleInput.addEventListener("input", (e) => {
8580
+ aesthetic.updateState({ title: e.target.value });
8581
+ treeState.notify("legendsChange");
8582
+ });
8583
+ titleGroup.appendChild(titleInput);
8584
+ container.appendChild(titleGroup);
8585
+ if (!aesthetic.state.isCategorical) {
8586
+ const unitsGroup = createControlGroup();
8587
+ const unitsLabel = createLabel("Units label:", controlHeight);
8588
+ unitsGroup.appendChild(unitsLabel);
8589
+ const unitsInput = document.createElement("input");
8590
+ unitsInput.type = "text";
8591
+ unitsInput.className = "ht-text-input";
8592
+ unitsInput.style.height = `${controlHeight}px`;
8593
+ unitsInput.style.flex = "1";
8594
+ unitsInput.value = aesthetic.state.inputUnits || "";
8595
+ unitsInput.placeholder = "Enter units (e.g., °C, km)";
8596
+ unitsInput.addEventListener("input", (e) => {
8597
+ aesthetic.updateState({ inputUnits: e.target.value });
8598
+ treeState.notify("legendsChange");
8599
+ });
8600
+ unitsGroup.appendChild(unitsInput);
8601
+ container.appendChild(unitsGroup);
8602
+ }
8603
+ }
8604
+ function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlHeight, includeNone = false, continuous = null, getCurrentAestheticSettings = null, populateAestheticSettings = null) {
6936
8605
  const select2 = document.createElement("select");
6937
8606
  select2.className = "ht-select";
6938
8607
  select2.style.height = `${controlHeight}px`;
@@ -6986,6 +8655,12 @@ function createMetadataColumnSelect(treeState, aesthetic, defaultLabel, controlH
6986
8655
  const aestheticUpdate = {};
6987
8656
  aestheticUpdate[aesthetic] = columnId;
6988
8657
  treeState.setAesthetics(aestheticUpdate);
8658
+ if (getCurrentAestheticSettings && populateAestheticSettings) {
8659
+ const currentAestheticSettings = getCurrentAestheticSettings();
8660
+ if (currentAestheticSettings === aesthetic) {
8661
+ populateAestheticSettings(aesthetic);
8662
+ }
8663
+ }
6989
8664
  });
6990
8665
  return select2;
6991
8666
  }
@@ -7137,61 +8812,6 @@ function populateExportControls(container, getCurrentTreeState, getCurrentTreeVi
7137
8812
  marginGroup.appendChild(marginInput);
7138
8813
  container.appendChild(marginGroup);
7139
8814
  }
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
8815
  function heatTree(containerSelector, treesInput = [], options = {}) {
7196
8816
  if (treesInput && !Array.isArray(treesInput)) {
7197
8817
  treesInput = [treesInput];