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/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.18.0
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
- forester.visualizationCandidates = function (tree) {
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
- if (!(rootAge > 0) || !isFinite(rootAge)) {
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 v = 0; v <= rootAge + 1e-9; v += step) {
9470
- vals.push(Math.round(v * 1e6) / 1e6);
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.18.0",
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.2"
38
+ "phyloxml": "^1.1.7"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.13.0",