archaeopteryx 3.18.0 → 3.20.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 +96 -22
- package/archaeopteryx.d.ts +18 -0
- package/archaeopteryx.js +598 -84
- package/forester.js +204 -10
- package/package.json +2 -2
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.20.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -2091,7 +2091,12 @@
|
|
|
2091
2091
|
return p.applies_to === 'node' || p.applies_to === 'clade';
|
|
2092
2092
|
};
|
|
2093
2093
|
|
|
2094
|
-
|
|
2094
|
+
// options.keepConstant: also return a NUMERIC field that has one value on
|
|
2095
|
+
// every tip. There is nothing to colour by in it, so the selection never
|
|
2096
|
+
// asks for it -- but a matrix does: a single-copy core gene is a column
|
|
2097
|
+
// of 1s, and dropping it from the heat map hid a gene the file lists.
|
|
2098
|
+
forester.visualizationCandidates = function (tree, options) {
|
|
2099
|
+
let keepConstant = !!(options && options.keepConstant);
|
|
2095
2100
|
let total = 0;
|
|
2096
2101
|
let stats = Object.create(null); // id -> {kind, ref, label, nodes, values:Set, multi}; null-proto: ids embed file refs
|
|
2097
2102
|
|
|
@@ -2255,7 +2260,7 @@
|
|
|
2255
2260
|
// The legend already carries the honest part: a "no value" row
|
|
2256
2261
|
// with the uncovered count, at reduced opacity.
|
|
2257
2262
|
let sparse = covered * VIS_MIN_COVERAGE_DEN < total * VIS_MIN_COVERAGE_NUM;
|
|
2258
|
-
if (distinct < 2) {
|
|
2263
|
+
if (distinct < 2 && !(keepConstant && numeric)) {
|
|
2259
2264
|
return;
|
|
2260
2265
|
}
|
|
2261
2266
|
let colorMode;
|
|
@@ -2883,11 +2888,18 @@
|
|
|
2883
2888
|
* no numeric per-tip property. `min`/`max` are over every value of every
|
|
2884
2889
|
* column and are `null` when there is nothing to scale.
|
|
2885
2890
|
*/
|
|
2886
|
-
forester.heatmapColumns = function (tree) {
|
|
2891
|
+
forester.heatmapColumns = function (tree, only) {
|
|
2892
|
+
let wanted = null; // the figure's columns, when a file names them
|
|
2893
|
+
if (Array.isArray(only)) {
|
|
2894
|
+
wanted = Object.create(null);
|
|
2895
|
+
only.forEach(function (ref) {
|
|
2896
|
+
wanted[ref] = true;
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2887
2899
|
let numeric = Object.create(null);
|
|
2888
2900
|
let label = Object.create(null);
|
|
2889
|
-
forester.visualizationCandidates(tree).forEach(function (c) {
|
|
2890
|
-
if (c && c.kind === 'property' && c.numeric === true && c.ref) {
|
|
2901
|
+
forester.visualizationCandidates(tree, {keepConstant: true}).forEach(function (c) {
|
|
2902
|
+
if (c && c.kind === 'property' && c.numeric === true && c.ref && (!wanted || wanted[c.ref])) {
|
|
2891
2903
|
numeric[c.ref] = true;
|
|
2892
2904
|
label[c.ref] = c.label || c.ref;
|
|
2893
2905
|
}
|
|
@@ -2933,6 +2945,179 @@
|
|
|
2933
2945
|
};
|
|
2934
2946
|
};
|
|
2935
2947
|
|
|
2948
|
+
// --------------------------------------------------------------
|
|
2949
|
+
// The desktop's figure setting (aptx:figure)
|
|
2950
|
+
// --------------------------------------------------------------
|
|
2951
|
+
// The desktop Archaeopteryx stores what a tree is to be drawn with in one
|
|
2952
|
+
// property of the PHYLOGENY: <property ref="aptx:figure" applies_to=
|
|
2953
|
+
// "phylogeny"> as a direct child of <phylogeny>. Its value (FigureSpec
|
|
2954
|
+
// on the desktop) is "v1;key=value;..." with every value escaped
|
|
2955
|
+
// (PropertyTextCodec): \\ \t \n \r, \_ a space, \p "|", \s "~",
|
|
2956
|
+
// \c ";", \e "=". columns= is a "|" list of ref~TYPE~SHAPE~normalized.
|
|
2957
|
+
// Only a direct child of <phylogeny> counts (Christian, 2026-09-30: "a
|
|
2958
|
+
// hard break"): desktops 0.11.117 to 0.11.172 wrote it on the root clade,
|
|
2959
|
+
// and such a copy is ignored here and dropped on the way out, as the
|
|
2960
|
+
// desktop does. An unknown version is no figure; unknown keys are ignored.
|
|
2961
|
+
forester.FIGURE_REF = 'aptx:figure';
|
|
2962
|
+
|
|
2963
|
+
forester.unescapeFigureValue = function (v) {
|
|
2964
|
+
const map = {'\\': '\\', 't': '\t', 'n': '\n', 'r': '\r', '_': ' ', 'p': '|', 's': '~', 'c': ';', 'e': '='};
|
|
2965
|
+
return String(v).replace(/\\(.)/g, function (all, c) {
|
|
2966
|
+
return map[c] !== undefined ? map[c] : all;
|
|
2967
|
+
});
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
// {columns: [{ref, type, shape, normalized}]} from the tree's figure, or
|
|
2971
|
+
// null when it has none (or one this version cannot read).
|
|
2972
|
+
forester.readFigure = function (tree) {
|
|
2973
|
+
let props = tree && Array.isArray(tree.properties) ? tree.properties : [];
|
|
2974
|
+
let p = props.find(function (q) {
|
|
2975
|
+
return q && q.ref === forester.FIGURE_REF;
|
|
2976
|
+
});
|
|
2977
|
+
if (!p || typeof p.value !== 'string') {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
let parts = p.value.trim().split(';');
|
|
2981
|
+
if (parts[0] !== 'v1') {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
// labelProps: the properties the label shows, in order (null when the
|
|
2985
|
+
// figure does not say, [] when it says none); show: the desktop's
|
|
2986
|
+
// display switches by DisplayOption name ("show.SHOW_PROPERTIES=true")
|
|
2987
|
+
let figure = {columns: [], labelProps: null, show: {}};
|
|
2988
|
+
parts.slice(1).forEach(function (kv) {
|
|
2989
|
+
let at = kv.indexOf('=');
|
|
2990
|
+
if (at < 1) {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
let key = kv.substring(0, at);
|
|
2994
|
+
let value = forester.unescapeFigureValue(kv.substring(at + 1));
|
|
2995
|
+
if (key === 'labelprops') {
|
|
2996
|
+
figure.labelProps = value.split('|').filter(function (r) {
|
|
2997
|
+
return r.length > 0;
|
|
2998
|
+
});
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (key.indexOf('show.') === 0 && (value === 'true' || value === 'false')) {
|
|
3002
|
+
figure.show[key.substring(5)] = value === 'true';
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (key !== 'columns') {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
value.split('|').forEach(function (col) {
|
|
3009
|
+
let f = col.split('~');
|
|
3010
|
+
if (f[0] && f[0].length > 0) {
|
|
3011
|
+
figure.columns.push({ref: f[0], type: f[1] || '', shape: f[2] || 'CIRCLE',
|
|
3012
|
+
normalized: f[3] === 'true'});
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
});
|
|
3016
|
+
return figure;
|
|
3017
|
+
};
|
|
3018
|
+
|
|
3019
|
+
// The refs the figure draws as a MATRIX, in its order ([] without one).
|
|
3020
|
+
forester.figureMatrixRefs = function (tree) {
|
|
3021
|
+
let figure = forester.readFigure(tree);
|
|
3022
|
+
let seen = Object.create(null);
|
|
3023
|
+
return figure ? figure.columns.filter(function (c) {
|
|
3024
|
+
let keep = c.type === 'MATRIX' && !seen[c.ref];
|
|
3025
|
+
seen[c.ref] = true;
|
|
3026
|
+
return keep;
|
|
3027
|
+
}).map(function (c) {
|
|
3028
|
+
return c.ref;
|
|
3029
|
+
}) : [];
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
// ---- Properties in the tip label (the desktop's "Properties" option) ----
|
|
3033
|
+
|
|
3034
|
+
// A property a reader would want to see: not the viewer's own aptx:
|
|
3035
|
+
// metadata, not a style: instruction.
|
|
3036
|
+
const LABEL_PROPERTY_SEPARATOR = ' | ';
|
|
3037
|
+
|
|
3038
|
+
forester.isUserVisiblePropertyRef = function (ref) {
|
|
3039
|
+
return typeof ref === 'string' && ref.length > 0 && ref.indexOf('aptx:') !== 0 && ref.indexOf('style:') !== 0;
|
|
3040
|
+
};
|
|
3041
|
+
|
|
3042
|
+
// Every user-visible property ref any node carries, internal nodes
|
|
3043
|
+
// included, sorted by the name the reader sees (then by ref): what the
|
|
3044
|
+
// label-field chooser offers. Deliberately BROADER than the colour-by
|
|
3045
|
+
// candidates, which drop constant, per-tip-unique and internal-only fields
|
|
3046
|
+
// -- all perfectly good label text (the desktop's userVisiblePropertyRefs).
|
|
3047
|
+
forester.labelPropertyRefs = function (tree) {
|
|
3048
|
+
let seen = Object.create(null);
|
|
3049
|
+
forester.preOrderTraversalAll(tree, function (n) {
|
|
3050
|
+
(n.properties || []).forEach(function (p) {
|
|
3051
|
+
if (p && forester.isUserVisiblePropertyRef(p.ref)
|
|
3052
|
+
&& p.value !== undefined && p.value !== null && String(p.value).trim().length > 0) {
|
|
3053
|
+
seen[p.ref] = true;
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
});
|
|
3057
|
+
return Object.keys(seen).sort(function (a, b) {
|
|
3058
|
+
let da = forester.propertyDisplayName(a).toLowerCase();
|
|
3059
|
+
let db = forester.propertyDisplayName(b).toLowerCase();
|
|
3060
|
+
return da < db ? -1 : (da > db ? 1 : (a < b ? -1 : (a > b ? 1 : 0)));
|
|
3061
|
+
});
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
// A node's properties as one line of label text: values only, a unit
|
|
3065
|
+
// after its value (without its namespace), joined by " | " as the rest of
|
|
3066
|
+
// the label is. Not by ", " as on the desktop (Christian, 2026-09-30): a
|
|
3067
|
+
// value often holds a comma itself -- a BV-BRC host "Bottlenose dolphin,
|
|
3068
|
+
// Tursiops truncatus" -- and "host, species" then reads as three fields. `refs` picks and orders the fields; null is every
|
|
3069
|
+
// user-visible property in the node's own order, less `excluded` (the
|
|
3070
|
+
// heat map's columns: a field has one display role).
|
|
3071
|
+
forester.labelPropertiesText = function (node, refs, excluded) {
|
|
3072
|
+
let props = (node && node.properties) || [];
|
|
3073
|
+
let parts = [];
|
|
3074
|
+
let add = function (p) {
|
|
3075
|
+
if (!p || !forester.isUserVisiblePropertyRef(p.ref) || p.value === undefined || p.value === null) {
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
let v = String(p.value).trim();
|
|
3079
|
+
if (v.length < 1) {
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
if (typeof p.unit === 'string' && p.unit.length > 0) {
|
|
3083
|
+
v += ' ' + p.unit.substring(p.unit.lastIndexOf(':') + 1);
|
|
3084
|
+
}
|
|
3085
|
+
parts.push(v);
|
|
3086
|
+
};
|
|
3087
|
+
if (refs) {
|
|
3088
|
+
refs.forEach(function (ref) {
|
|
3089
|
+
props.forEach(function (p) {
|
|
3090
|
+
if (p && p.ref === ref) {
|
|
3091
|
+
add(p);
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
});
|
|
3095
|
+
} else {
|
|
3096
|
+
props.forEach(function (p) {
|
|
3097
|
+
if (!(excluded && p && excluded[p.ref])) {
|
|
3098
|
+
add(p);
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
return parts.join(LABEL_PROPERTY_SEPARATOR);
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
// Removes an aptx:figure from every clade (the old place, now ignored),
|
|
3106
|
+
// so a tree saved from here carries no dead copy. Returns how many.
|
|
3107
|
+
forester.dropCladeFigures = function (tree) {
|
|
3108
|
+
let n = 0;
|
|
3109
|
+
forester.preOrderTraversalAll(tree, function (node) {
|
|
3110
|
+
if (node !== tree && Array.isArray(node.properties)) {
|
|
3111
|
+
let kept = node.properties.filter(function (q) {
|
|
3112
|
+
return !(q && q.ref === forester.FIGURE_REF);
|
|
3113
|
+
});
|
|
3114
|
+
n += node.properties.length - kept.length;
|
|
3115
|
+
node.properties = kept;
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
return n;
|
|
3119
|
+
};
|
|
3120
|
+
|
|
2936
3121
|
/**
|
|
2937
3122
|
* The number in `node`'s heat-map cell for `ref`, or `null` when there is none.
|
|
2938
3123
|
*
|
|
@@ -9461,13 +9646,22 @@
|
|
|
9461
9646
|
// Tick ages for a "Ma before present" ruler: ~8 nice steps from 0 back
|
|
9462
9647
|
// to the root age.
|
|
9463
9648
|
forester.maAxisTickValues = function (rootAge) {
|
|
9464
|
-
|
|
9649
|
+
return forester.distanceAxisTickValues(rootAge);
|
|
9650
|
+
};
|
|
9651
|
+
|
|
9652
|
+
// Tick values for a distance axis: ~8 nice steps from 0 (the root) out
|
|
9653
|
+
// to maxDist (the deepest tip), in the tree's branch-length units.
|
|
9654
|
+
forester.distanceAxisTickValues = function (maxDist) {
|
|
9655
|
+
if (!(maxDist > 0) || !isFinite(maxDist)) {
|
|
9656
|
+
return [];
|
|
9657
|
+
}
|
|
9658
|
+
let step = forester.niceAxisStep(maxDist / 8);
|
|
9659
|
+
if (!(step > 0)) {
|
|
9465
9660
|
return [];
|
|
9466
9661
|
}
|
|
9467
|
-
let step = forester.niceAxisStep(rootAge / 8);
|
|
9468
9662
|
let vals = [];
|
|
9469
|
-
for (let
|
|
9470
|
-
vals.push(
|
|
9663
|
+
for (let i = 0; (i * step) <= maxDist + (step * 1e-9); ++i) {
|
|
9664
|
+
vals.push(Number((i * step).toPrecision(12)));
|
|
9471
9665
|
}
|
|
9472
9666
|
return vals;
|
|
9473
9667
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
|
|
5
5
|
"main": "archaeopteryx.js",
|
|
6
6
|
"types": "archaeopteryx.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"d3": "^7.9.0",
|
|
38
|
-
"phyloxml": "^1.1.
|
|
38
|
+
"phyloxml": "^1.1.7"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^9.13.0",
|