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