archaeopteryx 3.15.0 → 3.16.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 +117 -15
- package/archaeopteryx.js +813 -93
- package/forester.js +191 -29
- package/package.json +4 -2
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.16.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.16.0';
|
|
107
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
108
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
109
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -408,6 +408,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
408
408
|
const VIS_CB = 'vis_cb';
|
|
409
409
|
const LADDERIZE_BUTTON = 'ladderize_b';
|
|
410
410
|
const PHYLOGRAM_ALIGNED_BUTTON = 'phya_b';
|
|
411
|
+
// What the three display types DO, in one place: the tooltip, the reason a
|
|
412
|
+
// type is greyed, and the cheat-sheet row are all this sentence. A control
|
|
413
|
+
// named only after itself ("phylogram display (uses branch length
|
|
414
|
+
// values)") tells a reader who already knows what it is.
|
|
415
|
+
// Each says WHEN it is greyed, whether or not it is greyed now: that is a
|
|
416
|
+
// property of the control, not of the view, and a reader of the sheet in
|
|
417
|
+
// one layout is owed what happens in the others.
|
|
418
|
+
const PHYLOGRAM_WHAT = 'phylogram: branch lengths drawn to scale, so the tips end ragged.'
|
|
419
|
+
+ ' Greyed in the circular layout, which always carries its labels to the outer ring,'
|
|
420
|
+
+ ' so there the aligned phylogram is the one drawn';
|
|
421
|
+
const PHYLOGRAM_ALIGNED_WHAT = 'aligned phylogram: branch lengths to scale, with every tip'
|
|
422
|
+
+ ' carried on to a common column, or in the circular layout to the outer ring.'
|
|
423
|
+
+ ' Greyed in the unrooted layout, which has no column or ring to carry them to';
|
|
424
|
+
const CLADOGRAM_WHAT = 'cladogram: topology only, branch lengths ignored, every tip flush';
|
|
411
425
|
const PHYLOGRAM_BUTTON = 'phy_b';
|
|
412
426
|
const PHYLOGRAM_CLADOGRAM_CONTROLGROUP = 'phy_cla_g';
|
|
413
427
|
const BRANCH_SCALE_CONTROLGROUP = 'branch_scale_g';
|
|
@@ -2966,7 +2980,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
2966
2980
|
return {source: link.source.data, target: link.target.data};
|
|
2967
2981
|
});
|
|
2968
2982
|
_overviewLinks = links; // the overview is built from these, not from the DOM
|
|
2969
|
-
let gap = _state.nodeLabelGap;
|
|
2970
2983
|
|
|
2971
2984
|
if (_state.phylogram === true) {
|
|
2972
2985
|
_yScale = branchLengthScaling(forester.getAllExternalNodes(_root), _w);
|
|
@@ -3048,9 +3061,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
3048
3061
|
}
|
|
3049
3062
|
|
|
3050
3063
|
if (_state.dynahide) {
|
|
3051
|
-
if (
|
|
3052
|
-
//
|
|
3053
|
-
//
|
|
3064
|
+
if (radialDisplay()) {
|
|
3065
|
+
// The every-k-th DECIMATION does not run in either radial
|
|
3066
|
+
// layout: it thins by index against even ROW spacing, which a
|
|
3067
|
+
// fan does not have. An unrooted fan has no rows at all; a
|
|
3068
|
+
// circular one has a ring, whose room is set by its
|
|
3069
|
+
// circumference and not by the display's height. Measured on
|
|
3070
|
+
// Caliciviridae_100 at 1100x850, the index rule kept 48 of 97
|
|
3071
|
+
// names in circular while all 97 fitted the ring with NOT ONE
|
|
3072
|
+
// overlapping pair, and on flu_h5 it kept 59 of 354 where 118
|
|
3073
|
+
// are readable. (118 is the shipped rule measured; 100 was a
|
|
3074
|
+
// greedy estimate in DOM order made before it was built, and
|
|
3075
|
+
// is what this comment said until a review caught it against
|
|
3076
|
+
// README and CHANGELOG.) Both are now thinned by hideCrowdedTipLabels
|
|
3077
|
+
// instead, on whether the names actually overprint.
|
|
3054
3078
|
forester.preOrderTraversal(_root, function (n) {
|
|
3055
3079
|
n.hide = false;
|
|
3056
3080
|
});
|
|
@@ -3062,8 +3086,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
3062
3086
|
});
|
|
3063
3087
|
}
|
|
3064
3088
|
}
|
|
3065
|
-
syncDynahideIndicator(_state.dynahide && !_state.unrootedDisplay && _dynahide_factor >= 2);
|
|
3066
|
-
|
|
3067
3089
|
updateButtonEnabledState();
|
|
3068
3090
|
|
|
3069
3091
|
// Only the nodes with something drawn for them individually get a
|
|
@@ -3130,11 +3152,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3130
3152
|
return makeFoundOutlineColor(d) ? '0.9px' : null;
|
|
3131
3153
|
})
|
|
3132
3154
|
.style('paint-order', 'stroke')
|
|
3155
|
+
// anchor, dy and x all come from nodeLabelPlacement, which the
|
|
3156
|
+
// occupancy box is built from too
|
|
3133
3157
|
.attr('text-anchor', function (d) {
|
|
3134
|
-
|
|
3135
|
-
return labelFlip(d) ? 'end' : 'start';
|
|
3136
|
-
}
|
|
3137
|
-
return d.children ? 'end' : 'start';
|
|
3158
|
+
return nodeLabelPlacement(d).anchor;
|
|
3138
3159
|
})
|
|
3139
3160
|
.attr('transform', function (d) {
|
|
3140
3161
|
if (_state.unrootedDisplay) {
|
|
@@ -3162,42 +3183,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3162
3183
|
return 'rotate(' + labelAngleDeg(d) + ') translate(' + off + ',0)' + (labelFlip(d) ? ' rotate(180)' : '');
|
|
3163
3184
|
})
|
|
3164
3185
|
.attr('dy', function (d) {
|
|
3165
|
-
|
|
3166
|
-
return '0.32em';
|
|
3167
|
-
}
|
|
3168
|
-
if (d.children) {
|
|
3169
|
-
// the desktop's above-branch internal label: the baseline
|
|
3170
|
-
// is lifted so the glyph bottoms sit just clear of the
|
|
3171
|
-
// horizontal branch line, instead of straddling it
|
|
3172
|
-
return -(Math.round(0.2 * _state.internalNodeFontSize) + 1) + 'px';
|
|
3173
|
-
}
|
|
3174
|
-
return 0.3 * _state.externalNodeFontSize + 'px';
|
|
3186
|
+
return nodeLabelPlacement(d).dy + 'px';
|
|
3175
3187
|
})
|
|
3176
3188
|
.attr('x', function (d) {
|
|
3177
|
-
|
|
3178
|
-
return labelFlip(d) ? -gap : gap;
|
|
3179
|
-
}
|
|
3180
|
-
if (!(d.children)) {
|
|
3181
|
-
if (_state.phylogram && _state.alignPhylogram) {
|
|
3182
|
-
return (-_yScale(d.distToRoot) + _w + gap);
|
|
3183
|
-
} else {
|
|
3184
|
-
return gap;
|
|
3185
|
-
}
|
|
3186
|
-
} else {
|
|
3187
|
-
// right-aligned so the label ends just left of the node
|
|
3188
|
-
// (the desktop's publication-style placement); a long
|
|
3189
|
-
// label on a node near the root shifts right so its
|
|
3190
|
-
// leading characters stay on the canvas rather than
|
|
3191
|
-
// clipping off the left edge
|
|
3192
|
-
let label = makeNodeLabel(d) || '';
|
|
3193
|
-
let est = label.length * _state.internalNodeFontSize * 0.55;
|
|
3194
|
-
let leftmost = d.y - gap - est;
|
|
3195
|
-
let minX = 2 - _settings.rootOffset;
|
|
3196
|
-
if (leftmost < minX) {
|
|
3197
|
-
return -gap + (minX - leftmost);
|
|
3198
|
-
}
|
|
3199
|
-
return -gap;
|
|
3200
|
-
}
|
|
3189
|
+
return nodeLabelPlacement(d).x;
|
|
3201
3190
|
});
|
|
3202
3191
|
|
|
3203
3192
|
node.select('text.bllabel')
|
|
@@ -3361,7 +3350,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3361
3350
|
// a collapsed clade's label always shows, so its guide does too: from
|
|
3362
3351
|
// past the wedge's farthest tip to the label column
|
|
3363
3352
|
let collapsedGuides = alignedRect ? nodes.filter(function (d) {
|
|
3364
|
-
return isCollapsed(d) &&
|
|
3353
|
+
return isCollapsed(d) && nameDrawn(d);
|
|
3365
3354
|
}) : [];
|
|
3366
3355
|
if (tipGuides || collapsedGuides.length > 0) {
|
|
3367
3356
|
let ext = _svgGroup.insert('g', 'g').attr('class', 'aptx-align-ext');
|
|
@@ -3374,7 +3363,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3374
3363
|
if (tipGuides) {
|
|
3375
3364
|
guideStyle(ext.selectAll('path.aptx-tip-guide')
|
|
3376
3365
|
.data(links.filter(function (d) {
|
|
3377
|
-
return (!d.target.children &&
|
|
3366
|
+
return (!d.target.children && nameDrawn(d.target));
|
|
3378
3367
|
}))
|
|
3379
3368
|
.enter().append('path').attr('class', 'aptx-tip-guide'))
|
|
3380
3369
|
.attr('d', function (d) {
|
|
@@ -3397,7 +3386,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3397
3386
|
// tips while their labels show; a collapsed clade always (its label
|
|
3398
3387
|
// always shows), from its wedge's farthest reach out to the ring
|
|
3399
3388
|
let connected = nodes.filter(function (d) {
|
|
3400
|
-
if (
|
|
3389
|
+
if (!nameDrawn(d)) {
|
|
3401
3390
|
return false;
|
|
3402
3391
|
}
|
|
3403
3392
|
if (isCollapsed(d)) {
|
|
@@ -3870,8 +3859,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
3870
3859
|
// in y as much as in x, and branch length only sees x. Measured here
|
|
3871
3860
|
// before adopting it: with Auto-hide Labels already ON, 240 of 267 drawn
|
|
3872
3861
|
// support numbers overlapped another on flu_h5.xml (90%), 149 of 262 on
|
|
3873
|
-
// Adenoviridae, 13 of 119 on bcl2 -- our auto-hide
|
|
3874
|
-
// and had never touched branch data.
|
|
3862
|
+
// Adenoviridae, 13 of 119 on bcl2 -- our auto-hide only thinned TIP labels
|
|
3863
|
+
// then, and had never touched branch data.
|
|
3875
3864
|
//
|
|
3876
3865
|
// WHAT MUST MATCH, or the two viewers drop different marks:
|
|
3877
3866
|
// - the tie-break: PREORDER, first claim wins, so the mark nearer the
|
|
@@ -3903,11 +3892,29 @@ function (root, d3, forester, phyloXml) {
|
|
|
3903
3892
|
// Measured from the font rather than assumed. The fallback is what this
|
|
3904
3893
|
// font actually reports at 9px -- ascent 9, descent 2, line 11 -- read off
|
|
3905
3894
|
// a rendered label's getBBox rather than guessed.
|
|
3906
|
-
let
|
|
3895
|
+
let _fontMetricsCache = new Map();
|
|
3896
|
+
|
|
3897
|
+
// how many names the radial overlap rule took on the last render, for the
|
|
3898
|
+
// Auto-hide indicator: those layouts thin by overlap, not by index, so
|
|
3899
|
+
// _dynahide_factor says nothing about them
|
|
3900
|
+
let _radialNamesHidden = 0;
|
|
3901
|
+
|
|
3902
|
+
// and how many branch marks the crowding rule refused on the last render.
|
|
3903
|
+
// The switch governs three things -- the every-k-th thinning, the radial
|
|
3904
|
+
// overlap thinning and the branch-data rule -- and the indicator used to
|
|
3905
|
+
// ask only about names, so a view where every name fitted but 201 numbers
|
|
3906
|
+
// were being refused showed a dark toggle over a tree it was thinning.
|
|
3907
|
+
// (Caliciviridae circular; apaf RECTANGULAR did it too, so this half was
|
|
3908
|
+
// never right in any layout.)
|
|
3909
|
+
let _branchMarksHidden = 0;
|
|
3907
3910
|
|
|
3908
3911
|
function branchFontMetrics(fontPx) {
|
|
3909
|
-
|
|
3910
|
-
|
|
3912
|
+
// One cache for every size asked for, not one slot: labels and branch
|
|
3913
|
+
// marks run at different sizes in the same pass, and a single slot
|
|
3914
|
+
// would re-measure on every alternation.
|
|
3915
|
+
let hit = _fontMetricsCache.get(fontPx);
|
|
3916
|
+
if (hit) {
|
|
3917
|
+
return hit;
|
|
3911
3918
|
}
|
|
3912
3919
|
let ctx = measureCtx();
|
|
3913
3920
|
ctx.font = fontPx + 'px ' + FONT_DEFAULTS;
|
|
@@ -3924,9 +3931,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3924
3931
|
if (!(descent >= 0)) {
|
|
3925
3932
|
descent = fontPx * (2 / 9);
|
|
3926
3933
|
}
|
|
3927
|
-
|
|
3934
|
+
let metrics = {px: fontPx, ascent: ascent, descent: descent,
|
|
3928
3935
|
height: ascent + descent};
|
|
3929
|
-
|
|
3936
|
+
_fontMetricsCache.set(fontPx, metrics);
|
|
3937
|
+
return metrics;
|
|
3930
3938
|
}
|
|
3931
3939
|
|
|
3932
3940
|
// Where a branch mark sits on its branch, read off the rendered elements
|
|
@@ -4018,6 +4026,238 @@ function (root, d3, forester, phyloXml) {
|
|
|
4018
4026
|
return [x, (d.x + at.dy) - fm.ascent, w, h];
|
|
4019
4027
|
}
|
|
4020
4028
|
|
|
4029
|
+
// ---- the node's own label: one source for drawing it and for reserving
|
|
4030
|
+
// its space ----
|
|
4031
|
+
//
|
|
4032
|
+
// The three attributes below (text-anchor, x, dy) and the occupancy box
|
|
4033
|
+
// that keeps branch numbers off the name are the same decision, so they
|
|
4034
|
+
// are made once here. Modelled after branchMarkPlacement, and for the same
|
|
4035
|
+
// reason: a box that is not where the text is reserves the wrong space,
|
|
4036
|
+
// and two copies of the arithmetic drift the first time one is edited.
|
|
4037
|
+
function nodeLabelPlacement(d) {
|
|
4038
|
+
let gap = _state.nodeLabelGap;
|
|
4039
|
+
if (radialDisplay()) {
|
|
4040
|
+
let flip = labelFlip(d);
|
|
4041
|
+
return {anchor: flip ? 'end' : 'start', x: flip ? -gap : gap,
|
|
4042
|
+
dy: 0.32 * nodeLabelFontSize(d)};
|
|
4043
|
+
}
|
|
4044
|
+
if (!d.children) {
|
|
4045
|
+
let x = (_state.phylogram && _state.alignPhylogram)
|
|
4046
|
+
? (-_yScale(d.distToRoot) + _w + gap)
|
|
4047
|
+
: gap;
|
|
4048
|
+
return {anchor: 'start', x: x, dy: 0.3 * _state.externalNodeFontSize};
|
|
4049
|
+
}
|
|
4050
|
+
// the desktop's above-branch internal label: right-aligned so it ends
|
|
4051
|
+
// just left of the node, and shifted right when a long label on a node
|
|
4052
|
+
// near the root would otherwise clip off the left edge
|
|
4053
|
+
let label = makeNodeLabel(d) || '';
|
|
4054
|
+
let est = label.length * _state.internalNodeFontSize * 0.55;
|
|
4055
|
+
let leftmost = d.y - gap - est;
|
|
4056
|
+
let minX = 2 - _settings.rootOffset;
|
|
4057
|
+
let x = (leftmost < minX) ? (-gap + (minX - leftmost)) : -gap;
|
|
4058
|
+
return {anchor: 'end', x: x,
|
|
4059
|
+
dy: -(Math.round(0.2 * _state.internalNodeFontSize) + 1)};
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
// The size the label is actually drawn at: a per-node style overrides the
|
|
4063
|
+
// internal/external default, and the box has to follow it or a styled
|
|
4064
|
+
// tree reserves the wrong space.
|
|
4065
|
+
function nodeLabelFontSize(d) {
|
|
4066
|
+
let style = nodeStyle(d);
|
|
4067
|
+
if (style && style.fontSize) {
|
|
4068
|
+
return style.fontSize;
|
|
4069
|
+
}
|
|
4070
|
+
return d.children ? _state.internalNodeFontSize : _state.externalNodeFontSize;
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
// The label's rectangle in the tree group's coordinates, as a start point
|
|
4074
|
+
// on the baseline, a direction the text runs in, and its extents.
|
|
4075
|
+
// Returns null when there is no label drawn.
|
|
4076
|
+
function nodeLabelGeometry(d) {
|
|
4077
|
+
let text = d._extLabelText;
|
|
4078
|
+
if (!text) {
|
|
4079
|
+
return null;
|
|
4080
|
+
}
|
|
4081
|
+
let fontPx = nodeLabelFontSize(d);
|
|
4082
|
+
let style = nodeStyle(d);
|
|
4083
|
+
let bold = getFoundColor(d)
|
|
4084
|
+
|| (style && (style.fontStyle === 'bold' || style.fontStyle === 'bold_italic'));
|
|
4085
|
+
let w = legendTextWidth(text, (bold ? 'bold ' : '') + fontPx + 'px ' + FONT_DEFAULTS);
|
|
4086
|
+
let fm = branchFontMetrics(fontPx);
|
|
4087
|
+
let p = nodeLabelPlacement(d);
|
|
4088
|
+
let at = layoutPointXY(d);
|
|
4089
|
+
if (at[0] === undefined || at[1] === undefined) {
|
|
4090
|
+
return null; // not laid out (an unrooted node the layout never reached)
|
|
4091
|
+
}
|
|
4092
|
+
if (!radialDisplay() || _radialLabelsHorizontal) {
|
|
4093
|
+
// upright text, whatever the layout: one axis-aligned rectangle
|
|
4094
|
+
let base = at;
|
|
4095
|
+
if (_state.circularDisplay && _radialLabelsHorizontal && !d.children && _radial) {
|
|
4096
|
+
// the label rides out to its point on the ring, in screen terms
|
|
4097
|
+
base = polarXY(radialAngle(d.x), _radial.maxRad);
|
|
4098
|
+
}
|
|
4099
|
+
let x0 = base[0] + p.x - (p.anchor === 'end' ? w : 0);
|
|
4100
|
+
return {x: x0, y: (base[1] + p.dy) - fm.ascent, w: w, h: fm.height, angle: 0};
|
|
4101
|
+
}
|
|
4102
|
+
// Radial: the text starts a gap beyond the node (or beyond the ring,
|
|
4103
|
+
// for a circular tip) and runs OUTWARD along the spoke -- on both
|
|
4104
|
+
// halves of the fan, because the 180-degree flip that keeps the far
|
|
4105
|
+
// half upright also reverses which end the anchor is.
|
|
4106
|
+
//
|
|
4107
|
+
// Built as an offset FROM THE NODE, not as a distance from the origin.
|
|
4108
|
+
// A circular node lies on the ray its label runs along, so radius
|
|
4109
|
+
// arithmetic happens to work there; an unrooted one does not -- its
|
|
4110
|
+
// spoke angle is the direction its own branch came in at, which has
|
|
4111
|
+
// nothing to do with its bearing from the centre. Measured with the
|
|
4112
|
+
// radius version: unrooted still had 7 of 12 numbers printing through
|
|
4113
|
+
// a name on flu_h5, because the boxes were being reserved in places
|
|
4114
|
+
// no label was.
|
|
4115
|
+
let theta = spokeAngle(d);
|
|
4116
|
+
let along = _state.nodeLabelGap;
|
|
4117
|
+
if (_state.circularDisplay && !d.children && _radial) {
|
|
4118
|
+
along += _radial.maxRad - radialRadius(d.y); // the pull out to the ring
|
|
4119
|
+
}
|
|
4120
|
+
let flip = labelFlip(d);
|
|
4121
|
+
// the baseline offset is perpendicular to the text, and the flip
|
|
4122
|
+
// carries it across with everything else
|
|
4123
|
+
let off = flip ? -p.dy : p.dy;
|
|
4124
|
+
let top = flip ? (off - fm.descent) : (off - fm.ascent);
|
|
4125
|
+
let cos = Math.cos(theta), sin = Math.sin(theta);
|
|
4126
|
+
return {x: at[0] + (cos * along) - (sin * top),
|
|
4127
|
+
y: at[1] + (sin * along) + (cos * top),
|
|
4128
|
+
w: w, h: fm.height, angle: theta};
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// The label's own outline, as four corners in the tree group's
|
|
4132
|
+
// coordinates: what the turned-box occupancy tests against.
|
|
4133
|
+
function nodeLabelQuad(g) {
|
|
4134
|
+
if (!g || !(g.w > 0) || !(g.h > 0)) {
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
let cos = Math.cos(g.angle), sin = Math.sin(g.angle);
|
|
4138
|
+
let corners = [[0, 0], [g.w, 0], [g.w, g.h], [0, g.h]];
|
|
4139
|
+
let quad = [];
|
|
4140
|
+
for (let i = 0; i < corners.length; ++i) {
|
|
4141
|
+
quad.push([g.x + (cos * corners[i][0]) - (sin * corners[i][1]),
|
|
4142
|
+
g.y + (sin * corners[i][0]) + (cos * corners[i][1])]);
|
|
4143
|
+
}
|
|
4144
|
+
return quad;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
// ---- crowded TIP LABELS, in both radial layouts ----
|
|
4148
|
+
//
|
|
4149
|
+
// The desktop's rule: a tip label is drawn only if its own outline overlaps
|
|
4150
|
+
// no label already drawn this pass. Both fans use it; the rectangular
|
|
4151
|
+
// layout keeps the every-k-th thinning, whose row pitch is a real quantity
|
|
4152
|
+
// there.
|
|
4153
|
+
//
|
|
4154
|
+
// Unrooted had nothing at all before this -- its labels sit wherever the
|
|
4155
|
+
// equal-angle layout put the tip. Circular had the rectangular rule, which
|
|
4156
|
+
// reads the display's HEIGHT against the node count and says nothing about
|
|
4157
|
+
// a ring: it kept 48 of 97 names on Caliciviridae where all 97 fit with no
|
|
4158
|
+
// overlap, and 59 of 354 on flu_h5 where 118 are readable.
|
|
4159
|
+
//
|
|
4160
|
+
// A SEARCH HIT is placed without being asked, so it can be drawn across a
|
|
4161
|
+
// name already down -- intended (the hit is what is being looked for), and
|
|
4162
|
+
// measured: a 3-hit search on confidences unrooted puts 3 hits across
|
|
4163
|
+
// non-hits, 8.1 px deep. It is the one case where "drawn only where it
|
|
4164
|
+
// overlaps nothing" does not hold, and the README says so.
|
|
4165
|
+
// Measured at 1100x850 before this, overlapping pairs among the labels
|
|
4166
|
+
// drawn: 172 of 97 labels on Caliciviridae_100, 71 of 42 on confidences,
|
|
4167
|
+
// 10 of 31 on apaf -- clumps of a dozen names printed through each other.
|
|
4168
|
+
//
|
|
4169
|
+
// OUTLINES, not their axis-aligned bounds: a name lying near its spoke has
|
|
4170
|
+
// bounds several times its own area, and on a 48-label ring those bounds
|
|
4171
|
+
// called 28 pairs crowded where a reader sees none.
|
|
4172
|
+
//
|
|
4173
|
+
// Order is the tree's own, root first (forester.preorderOf), which is also
|
|
4174
|
+
// what the desktop's unrooted painter does -- it reaches an internal node
|
|
4175
|
+
// before its subtree -- so both programs keep the same label when two
|
|
4176
|
+
// contend. A search HIT is placed without being asked, so the name being
|
|
4177
|
+
// looked for is always on the screen and everything after it keeps clear
|
|
4178
|
+
// of it (Christian, 2026-09-26: hits are exempt for tip labels, and for
|
|
4179
|
+
// nothing else).
|
|
4180
|
+
// Is this node's name on the screen? Asked by everything drawn to a label
|
|
4181
|
+
// -- the aligned guides, the circular ring connectors -- and it must be
|
|
4182
|
+
// ONE question. It used to be spelled `_state.dynahide && d.hide`, which
|
|
4183
|
+
// is the every-k-th rule and nothing else: when the radial layouts started
|
|
4184
|
+
// hiding by overlap they left `hide` false for everyone, so on flu_h5 in
|
|
4185
|
+
// circular 354 dashed connectors ran out to the ring for 118 names, 236 of
|
|
4186
|
+
// them ending at nothing. `_labelDropped` is set by BOTH rules.
|
|
4187
|
+
function nameDrawn(d) {
|
|
4188
|
+
return !d._labelDropped;
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function hideCrowdedTipLabels(nodes) {
|
|
4192
|
+
_radialNamesHidden = 0;
|
|
4193
|
+
if (!_state.dynahide || !radialDisplay()) {
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
// no layout, no honest outlines; bailing draws everything
|
|
4197
|
+
if (_state.unrootedDisplay ? !_unroot : !_radial) {
|
|
4198
|
+
return;
|
|
4199
|
+
}
|
|
4200
|
+
let labels = forester.orientedOccupancy(Math.max(4, _state.externalNodeFontSize));
|
|
4201
|
+
let ordered = forester.preorderOf(nodes);
|
|
4202
|
+
for (let i = 0, len = ordered.length; i !== len; ++i) {
|
|
4203
|
+
let d = ordered[i];
|
|
4204
|
+
if (d.children || d._extLabelText === '') {
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
let quad = nodeLabelQuad(nodeLabelGeometry(d));
|
|
4208
|
+
if (!quad) {
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
if (getFoundColor(d)) {
|
|
4212
|
+
labels.occupy(quad);
|
|
4213
|
+
} else if (!labels.claim(quad)) {
|
|
4214
|
+
d._extLabelText = '';
|
|
4215
|
+
d._labelDropped = true;
|
|
4216
|
+
++_radialNamesHidden;
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// The boxes to reserve for a label. An upright label is one rectangle. A
|
|
4222
|
+
// rotated one is reserved as a RUN of short boxes along its baseline,
|
|
4223
|
+
// never as one axis-aligned box around the whole thing: the bounds of a
|
|
4224
|
+
// rotated rectangle grow with its LENGTH, so a 120 px name lying at 30
|
|
4225
|
+
// degrees would reserve several times its own area. Stepping by the line
|
|
4226
|
+
// height bounds the error at one step whatever the name is.
|
|
4227
|
+
//
|
|
4228
|
+
// Honestly measured: the run and the single box draw the SAME numbers on
|
|
4229
|
+
// every tree tried (apaf, Caliciviridae, confidences) bar one -- flu_h5
|
|
4230
|
+
// unrooted, 4 against 3 -- and a purpose-built sparse fixture with 28-
|
|
4231
|
+
// character names could not separate them at all
|
|
4232
|
+
// (test_trees/label_reservation.html). The views that would show the
|
|
4233
|
+
// difference are already saturated, and the ones with room refuse nothing
|
|
4234
|
+
// either way. It is kept for the bound, not for the count: the error of
|
|
4235
|
+
// the single box grows without limit as names get longer, and this is
|
|
4236
|
+
// ten lines.
|
|
4237
|
+
function nodeLabelClaimBoxes(g) {
|
|
4238
|
+
if (!g || !(g.w > 0) || !(g.h > 0)) {
|
|
4239
|
+
return [];
|
|
4240
|
+
}
|
|
4241
|
+
if (g.angle === 0) {
|
|
4242
|
+
return [[g.x, g.y, g.w, g.h]];
|
|
4243
|
+
}
|
|
4244
|
+
let cos = Math.cos(g.angle), sin = Math.sin(g.angle);
|
|
4245
|
+
let step = Math.max(4, g.h);
|
|
4246
|
+
let boxes = [];
|
|
4247
|
+
for (let t = 0; t < g.w; t += step) {
|
|
4248
|
+
let len = Math.min(step, g.w - t);
|
|
4249
|
+
// the four corners of this slice, in tree coordinates
|
|
4250
|
+
let xs = [], ys = [];
|
|
4251
|
+
for (const [a, b] of [[t, 0], [t + len, 0], [t + len, g.h], [t, g.h]]) {
|
|
4252
|
+
xs.push(g.x + (cos * a) - (sin * b));
|
|
4253
|
+
ys.push(g.y + (sin * a) + (cos * b));
|
|
4254
|
+
}
|
|
4255
|
+
let x0 = Math.min(...xs), y0 = Math.min(...ys);
|
|
4256
|
+
boxes.push([x0, y0, Math.max(...xs) - x0, Math.max(...ys) - y0]);
|
|
4257
|
+
}
|
|
4258
|
+
return boxes;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4021
4261
|
function prepareNodeDrawing(nodes) {
|
|
4022
4262
|
let wantBl = _state.showBranchLengthValues === true;
|
|
4023
4263
|
let wantConf = _state.showConfidenceValues === true || _state.showMadValues === true;
|
|
@@ -4034,7 +4274,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
4034
4274
|
}
|
|
4035
4275
|
// Empty is the common case on a big tree: Auto-hide Labels blanks
|
|
4036
4276
|
// them exactly when there are most nodes to blank.
|
|
4037
|
-
|
|
4277
|
+
// WHY the text is empty matters downstream: a label the crowding
|
|
4278
|
+
// rule took away is a different thing from a label the user
|
|
4279
|
+
// switched off, and marks that accompany a name must tell them
|
|
4280
|
+
// apart (drawDomainArchitectures).
|
|
4281
|
+
d._labelDropped = _state.dynahide === true && d.hide === true;
|
|
4282
|
+
d._extLabelText = d._labelDropped ? '' : (makeNodeLabel(d) || '');
|
|
4038
4283
|
d._blText = wantBl ? asText(makeBranchLengthLabel(d)) : '';
|
|
4039
4284
|
d._confText = wantConf ? asText(makeConfidenceValuesLabel(d)) : '';
|
|
4040
4285
|
d._eventText = wantEvent ? asText(makeBranchEventsLabel(d)) : '';
|
|
@@ -4044,7 +4289,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
4044
4289
|
// childless <g class="node"> behind, and those are transformed on
|
|
4045
4290
|
// every animation frame. On the big tree that was ~18,000 empty groups
|
|
4046
4291
|
// per redraw, defeating the very thing the `drawn` filter is for.
|
|
4292
|
+
// names first: the ones that survive then reserve their space inside
|
|
4293
|
+
// hideCrowdedBranchData, and the marks are placed against what is left
|
|
4294
|
+
hideCrowdedTipLabels(nodes);
|
|
4047
4295
|
hideCrowdedBranchData(nodes);
|
|
4296
|
+
// told after the rules have run, not before: what the unrooted rule
|
|
4297
|
+
// hid is only known once it has looked at the labels
|
|
4298
|
+
syncDynahideIndicator();
|
|
4048
4299
|
for (let i = 0, len = nodes.length; i !== len; ++i) {
|
|
4049
4300
|
let d = nodes[i];
|
|
4050
4301
|
if (d._extLabelText !== '' || d._blText !== '' || d._confText !== ''
|
|
@@ -4070,8 +4321,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
4070
4321
|
// built in the tree group's own coordinates, which is where every mark is
|
|
4071
4322
|
// actually drawn, so the circular and unrooted views are measured in their
|
|
4072
4323
|
// own space and not in the cluster's. (The tip-label decimation that shares
|
|
4073
|
-
// the toggle
|
|
4074
|
-
// to decimate against, and
|
|
4324
|
+
// the toggle runs in the rectangular layout alone; both fans have no rows
|
|
4325
|
+
// to decimate against, and hideCrowdedTipLabels thins their names by
|
|
4326
|
+
// overlap instead, running before this does.)
|
|
4075
4327
|
function hideCrowdedBranchData(nodes) {
|
|
4076
4328
|
if (!_state.dynahide) {
|
|
4077
4329
|
return;
|
|
@@ -4082,6 +4334,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
4082
4334
|
if ((_state.circularDisplay && !_radial) || (_state.unrootedDisplay && !_unroot)) {
|
|
4083
4335
|
return;
|
|
4084
4336
|
}
|
|
4337
|
+
_branchMarksHidden = 0;
|
|
4338
|
+
// Each granted mark's claimed box is kept on the node, as a label's is
|
|
4339
|
+
// (_labelBoxes). Names have had an ink-coverage check since the
|
|
4340
|
+
// reservation went in -- every point of a drawn name must lie inside
|
|
4341
|
+
// the space it claimed -- and branch MARKS never did, which is the
|
|
4342
|
+
// half that failed on a Linux CI runner while the label half passed.
|
|
4343
|
+
for (let i = 0; i < nodes.length; ++i) {
|
|
4344
|
+
nodes[i]._markBoxes = null;
|
|
4345
|
+
}
|
|
4346
|
+
let stash = function (d, kind, box) {
|
|
4347
|
+
if (!box) {
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
4350
|
+
if (!d._markBoxes) {
|
|
4351
|
+
d._markBoxes = {};
|
|
4352
|
+
}
|
|
4353
|
+
d._markBoxes[kind] = box;
|
|
4354
|
+
};
|
|
4085
4355
|
let fontPx = _state.branchDataFontSize;
|
|
4086
4356
|
let dotCell = 0;
|
|
4087
4357
|
for (let i = 0; i < nodes.length; ++i) {
|
|
@@ -4093,6 +4363,33 @@ function (root, d3, forester, phyloXml) {
|
|
|
4093
4363
|
let numbers = forester.labelOccupancy(cell);
|
|
4094
4364
|
let symbols = forester.labelOccupancy(cell);
|
|
4095
4365
|
let ordered = forester.preorderOf(nodes);
|
|
4366
|
+
|
|
4367
|
+
// THE NAMES GO DOWN FIRST, and they are not asked: a node label is
|
|
4368
|
+
// drawn whatever else is there, so it reserves its space with occupy
|
|
4369
|
+
// rather than claim. Everything below is then refused against it, and
|
|
4370
|
+
// names outrank numbers -- the desktop's order, and the one a reader
|
|
4371
|
+
// expects, since a name is what the tree is for and a value that has
|
|
4372
|
+
// printed through one is worse than a value not drawn at all.
|
|
4373
|
+
//
|
|
4374
|
+
// Measured before this existed, at 1100x850 with both numbers on:
|
|
4375
|
+
// 44 of 93 numbers printed through a name on flu_h5 in the
|
|
4376
|
+
// rectangular layout, 30 of 55 on confidences, 10 of 15 unrooted.
|
|
4377
|
+
// The 3.15.0 pass kept numbers off each OTHER and off nothing else.
|
|
4378
|
+
for (let i = 0, len = ordered.length; i !== len; ++i) {
|
|
4379
|
+
let boxes = nodeLabelClaimBoxes(nodeLabelGeometry(ordered[i]));
|
|
4380
|
+
// kept on the node, beside the other per-render drawing state, so
|
|
4381
|
+
// the reservation can be compared with the ink it is supposed to
|
|
4382
|
+
// cover: a box that is merely SMALL -- short by a descent, or a
|
|
4383
|
+
// few per cent narrow -- still hides nothing wrongly and still
|
|
4384
|
+
// shows no overlap anywhere a number does not happen to sit. The
|
|
4385
|
+
// desktop pins the same property pixel by pixel.
|
|
4386
|
+
ordered[i]._labelBoxes = boxes.length ? boxes : null;
|
|
4387
|
+
for (let b = 0; b < boxes.length; ++b) {
|
|
4388
|
+
numbers.occupy(boxes[b][0], boxes[b][1], boxes[b][2], boxes[b][3]);
|
|
4389
|
+
symbols.occupy(boxes[b][0], boxes[b][1], boxes[b][2], boxes[b][3]);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4096
4393
|
for (let i = 0, len = ordered.length; i !== len; ++i) {
|
|
4097
4394
|
let d = ordered[i];
|
|
4098
4395
|
// Branch length first, then confidence: they sit on opposite sides
|
|
@@ -4102,12 +4399,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
4102
4399
|
let b = branchMarkBox(d, 'bl', d._blText, fontPx);
|
|
4103
4400
|
if (b && !numbers.claim(b[0], b[1], b[2], b[3])) {
|
|
4104
4401
|
d._blText = '';
|
|
4402
|
+
++_branchMarksHidden;
|
|
4403
|
+
} else {
|
|
4404
|
+
stash(d, 'bl', b);
|
|
4105
4405
|
}
|
|
4106
4406
|
}
|
|
4107
4407
|
if (d._confText !== '') {
|
|
4108
4408
|
let b = branchMarkBox(d, 'conf', d._confText, fontPx);
|
|
4109
4409
|
if (b && !numbers.claim(b[0], b[1], b[2], b[3])) {
|
|
4110
4410
|
d._confText = '';
|
|
4411
|
+
++_branchMarksHidden;
|
|
4412
|
+
} else {
|
|
4413
|
+
stash(d, 'conf', b);
|
|
4111
4414
|
}
|
|
4112
4415
|
}
|
|
4113
4416
|
// Branch EVENTS are branch-anchored too, half a branch from the
|
|
@@ -4119,6 +4422,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
4119
4422
|
let b = branchMarkBox(d, 'event', d._eventText, fontPx);
|
|
4120
4423
|
if (b && !numbers.claim(b[0], b[1], b[2], b[3])) {
|
|
4121
4424
|
d._eventText = '';
|
|
4425
|
+
++_branchMarksHidden;
|
|
4426
|
+
} else {
|
|
4427
|
+
stash(d, 'event', b);
|
|
4122
4428
|
}
|
|
4123
4429
|
}
|
|
4124
4430
|
if (d._suppDot) {
|
|
@@ -4132,6 +4438,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4132
4438
|
: [d.y + (0.5 * (d.parent.y - d.y)), d.x];
|
|
4133
4439
|
if (at && !symbols.claim(at[0] - r, at[1] - r, 2 * r, 2 * r)) {
|
|
4134
4440
|
d._suppDot = false;
|
|
4441
|
+
++_branchMarksHidden;
|
|
4135
4442
|
}
|
|
4136
4443
|
}
|
|
4137
4444
|
}
|
|
@@ -5165,8 +5472,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5165
5472
|
showTaxonomyButton: 'shown automatically when the tree has taxonomies',
|
|
5166
5473
|
showSequenceButton: 'shown automatically when the tree has sequences',
|
|
5167
5474
|
showBranchColorsButton: 'the Visual Styles checkbox appears when the tree has branch colours or style properties',
|
|
5168
|
-
|
|
5169
|
-
|
|
5475
|
+
// These two are ALWAYS offered, deliberately: they do nothing on a tree
|
|
5476
|
+
// that needs neither, and a control that comes and goes between trees
|
|
5477
|
+
// is worse than one that is simply there. (Both messages used to
|
|
5478
|
+
// promise a conditional appearance the code had already rejected.)
|
|
5479
|
+
showDynahideButton: 'the Auto-hide Labels checkbox is always shown',
|
|
5480
|
+
showShortenNodeNamesButton: 'the Short Names checkbox is always shown; it starts on when the tree has long node names',
|
|
5170
5481
|
showExternalLabelsButton: 'always shown',
|
|
5171
5482
|
showInternalLabelsButton: 'shown automatically when the tree has internal node data',
|
|
5172
5483
|
// the collapse-by-depth / rank / feature feature is gone
|
|
@@ -9913,7 +10224,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9913
10224
|
}
|
|
9914
10225
|
if (_state.phylogram && !_state.alignPhylogram) {
|
|
9915
10226
|
if (_state.unrootedDisplay) {
|
|
9916
|
-
toCladegram();
|
|
10227
|
+
toCladegram(); // unrooted cannot align
|
|
10228
|
+
} else if (_state.circularDisplay) {
|
|
10229
|
+
toCladegram(); // circular is already showing the aligned type
|
|
9917
10230
|
} else {
|
|
9918
10231
|
toAlignedPhylogram();
|
|
9919
10232
|
}
|
|
@@ -10017,6 +10330,274 @@ function (root, d3, forester, phyloXml) {
|
|
|
10017
10330
|
}
|
|
10018
10331
|
|
|
10019
10332
|
// The cheat sheet: every combo above, then the plain keys.
|
|
10333
|
+
// ===================== The control-panel cheat sheet =====================
|
|
10334
|
+
//
|
|
10335
|
+
// Every control the panel is SHOWING, in the order it shows them, each
|
|
10336
|
+
// beside the sentence that already explains it. The desktop's
|
|
10337
|
+
// ControlPanelCheatSheet, and built the same way on one principle: nothing
|
|
10338
|
+
// is written twice. A row's picture is the control's own glyph, cloned;
|
|
10339
|
+
// a row's words are the tooltip the control already carries. So a new
|
|
10340
|
+
// control needs no edit here -- it needs a tooltip, and
|
|
10341
|
+
// test_trees/panel_descriptions.html fails by name for any visible control
|
|
10342
|
+
// without one.
|
|
10343
|
+
//
|
|
10344
|
+
// Two rules of Christian's, 2026-09-25, which the desktop follows too:
|
|
10345
|
+
// name and description are separated by a COLON, never a dash; and no live
|
|
10346
|
+
// VALUES appear -- a label reading "Font size: 6" is cut at the colon, so
|
|
10347
|
+
// the sheet says what the control is and never what it is currently set
|
|
10348
|
+
// to. "A printed 40% is wrong for every reader but the one who printed
|
|
10349
|
+
// it."
|
|
10350
|
+
//
|
|
10351
|
+
// Modeless, and placed beside the panel rather than over it: a sheet you
|
|
10352
|
+
// must close to touch the control it describes is half a sheet.
|
|
10353
|
+
const CHEAT_SHEET_DIALOG = 'aptx_cheatsheet';
|
|
10354
|
+
let _cheatSheetBody = null;
|
|
10355
|
+
|
|
10356
|
+
// this viewer's own panel, not another viewer's on the same page
|
|
10357
|
+
function cheatSheetPanel() {
|
|
10358
|
+
return (_container && _container.querySelector(':scope > .aptx-panel'))
|
|
10359
|
+
|| document.querySelector('.aptx-panel');
|
|
10360
|
+
}
|
|
10361
|
+
|
|
10362
|
+
// The words for one control: its own title, or the title of the <label>
|
|
10363
|
+
// that names it -- which is a SIBLING, not an ancestor, for the select
|
|
10364
|
+
// menus (makeSelectMenu puts it there).
|
|
10365
|
+
function controlDescription(el) {
|
|
10366
|
+
let n = el;
|
|
10367
|
+
for (let i = 0; i < 4 && n; ++i) {
|
|
10368
|
+
let t = (n.getAttribute && (n.getAttribute('title') || n.getAttribute('aria-label'))) || '';
|
|
10369
|
+
if (t.trim() !== '') {
|
|
10370
|
+
return t.trim();
|
|
10371
|
+
}
|
|
10372
|
+
n = n.parentElement;
|
|
10373
|
+
}
|
|
10374
|
+
if (el.id) {
|
|
10375
|
+
let lab = document.querySelector('label[for="' + el.id + '"]');
|
|
10376
|
+
let t = lab ? (lab.getAttribute('title') || lab.getAttribute('aria-label') || '') : '';
|
|
10377
|
+
if (t.trim() !== '') {
|
|
10378
|
+
return t.trim();
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
return '';
|
|
10382
|
+
}
|
|
10383
|
+
|
|
10384
|
+
// What the control is called, with any live value cut away. A checkbox is
|
|
10385
|
+
// named by its own text, a menu or slider by the label beside it, a glyph
|
|
10386
|
+
// button by nothing -- its picture is its name, and the row shows that.
|
|
10387
|
+
function controlName(el) {
|
|
10388
|
+
// ONE cut, at the end, over whatever source the name came from.
|
|
10389
|
+
//
|
|
10390
|
+
// It used to be applied only to the two sources that could carry a
|
|
10391
|
+
// value -- a <label for> and an aria-label -- and not to a checkbox's
|
|
10392
|
+
// own text or a button's value. That left "no sheet name contains a
|
|
10393
|
+
// colon" meaning two different things on the two programs: on the
|
|
10394
|
+
// desktop every name IS a cut label, so the rule asserts the cut ran
|
|
10395
|
+
// and can never fire on correct output; here it would ALSO have
|
|
10396
|
+
// constrained what a control may be called, and a checkbox legitimately
|
|
10397
|
+
// labelled "Ratio 1:1" would have failed it. Cutting everywhere makes
|
|
10398
|
+
// the invariant mean the same thing on both sides.
|
|
10399
|
+
//
|
|
10400
|
+
// What the cut is doing here, exactly: our labels read "Color by:" and
|
|
10401
|
+
// "Shape:", so it removes a TRAILING colon, and removing the cut fails
|
|
10402
|
+
// the sheet's value rule by name ("Color by:"). What it is NOT doing
|
|
10403
|
+
// here is removing a value -- nothing follows a colon on this panel,
|
|
10404
|
+
// unlike the desktop's "Font size: 6". So it is exercised but for the
|
|
10405
|
+
// lesser of its two reasons. (Said precisely because the looser
|
|
10406
|
+
// version of this note -- "a no-op, removing it passes every check" --
|
|
10407
|
+
// was true for about an hour and then was not, once the desktop's
|
|
10408
|
+
// sharper value rule was adopted here.)
|
|
10409
|
+
return cutAtValue(rawControlName(el));
|
|
10410
|
+
}
|
|
10411
|
+
|
|
10412
|
+
// "Font size: 6" -> "Font size". The colon is where a value starts, and a
|
|
10413
|
+
// value on a sheet is wrong for everyone but whoever was looking when it
|
|
10414
|
+
// was made.
|
|
10415
|
+
function cutAtValue(name) {
|
|
10416
|
+
return name ? name.split(':')[0].trim() : '';
|
|
10417
|
+
}
|
|
10418
|
+
|
|
10419
|
+
// What the control is called, before the cut. A checkbox is named by its
|
|
10420
|
+
// own text, a menu or slider by the label beside it, a glyph button by
|
|
10421
|
+
// nothing -- its picture is its name, and the row shows that.
|
|
10422
|
+
function rawControlName(el) {
|
|
10423
|
+
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
10424
|
+
let span = el.parentElement ? el.parentElement.querySelector('span') : null;
|
|
10425
|
+
if (span && span.textContent.trim() !== '') {
|
|
10426
|
+
return span.textContent.trim();
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
if (el.tagName === 'INPUT' && el.value && (el.type === 'button' || el.type === 'submit')) {
|
|
10430
|
+
return el.value.trim();
|
|
10431
|
+
}
|
|
10432
|
+
if (el.id) {
|
|
10433
|
+
let lab = document.querySelector('label[for="' + el.id + '"]');
|
|
10434
|
+
if (lab && lab.textContent.trim() !== '') {
|
|
10435
|
+
return lab.textContent.trim();
|
|
10436
|
+
}
|
|
10437
|
+
}
|
|
10438
|
+
// a menu with no visible label of its own says what it is here
|
|
10439
|
+
let aria = el.getAttribute('aria-label');
|
|
10440
|
+
if (aria && aria.trim() !== '') {
|
|
10441
|
+
return aria.trim();
|
|
10442
|
+
}
|
|
10443
|
+
// and a text button -- the hide toggle, the reset chips -- is its text
|
|
10444
|
+
let own = (el.textContent || '').trim();
|
|
10445
|
+
if (own !== '' && el.tagName === 'BUTTON' && !el.querySelector('svg')) {
|
|
10446
|
+
return own;
|
|
10447
|
+
}
|
|
10448
|
+
// a segmented button drawn as TEXT rather than a glyph (Time / Div) is
|
|
10449
|
+
// named by its own label, which is the only thing on it
|
|
10450
|
+
let seg = el.closest ? el.closest('label.aptx-seg') : null;
|
|
10451
|
+
if (seg && !seg.querySelector('svg')) {
|
|
10452
|
+
let segText = (seg.textContent || '').trim();
|
|
10453
|
+
if (segText !== '') {
|
|
10454
|
+
return segText;
|
|
10455
|
+
}
|
|
10456
|
+
}
|
|
10457
|
+
return '';
|
|
10458
|
+
}
|
|
10459
|
+
|
|
10460
|
+
// What the user actually sees and clicks. A segmented button's <input> is
|
|
10461
|
+
// 0x0 and transparent -- the visible thing is the <label> around it, which
|
|
10462
|
+
// carries the glyph and the tooltip -- so asking the input whether it is
|
|
10463
|
+
// on the screen answered no for all eight of them: the three layouts, the
|
|
10464
|
+
// three display types and the time/divergence pair were absent from the
|
|
10465
|
+
// sheet entirely (Christian, 2026-09-28).
|
|
10466
|
+
function controlSurface(el) {
|
|
10467
|
+
let box = el.getBoundingClientRect();
|
|
10468
|
+
if (box.width > 0 || box.height > 0) {
|
|
10469
|
+
return el;
|
|
10470
|
+
}
|
|
10471
|
+
let label = el.closest ? el.closest('label') : null;
|
|
10472
|
+
if (label) {
|
|
10473
|
+
let lb = label.getBoundingClientRect();
|
|
10474
|
+
if (lb.width > 0 || lb.height > 0) {
|
|
10475
|
+
return label;
|
|
10476
|
+
}
|
|
10477
|
+
}
|
|
10478
|
+
return null;
|
|
10479
|
+
}
|
|
10480
|
+
|
|
10481
|
+
function cheatSheetRows() {
|
|
10482
|
+
let panel = cheatSheetPanel();
|
|
10483
|
+
if (!panel) {
|
|
10484
|
+
return [];
|
|
10485
|
+
}
|
|
10486
|
+
let rows = [];
|
|
10487
|
+
let seen = new Set();
|
|
10488
|
+
let all = panel.querySelectorAll('input, button, select');
|
|
10489
|
+
for (let i = 0; i < all.length; ++i) {
|
|
10490
|
+
let el = all[i];
|
|
10491
|
+
// what the panel is SHOWING: a control the tree gives no data for
|
|
10492
|
+
// is not on the screen and does not belong on the sheet. A
|
|
10493
|
+
// DISABLED one is on the screen, and saying what it is is how a
|
|
10494
|
+
// reader learns why it is greyed.
|
|
10495
|
+
let surface = el.disabled ? (controlSurface(el) || el) : controlSurface(el);
|
|
10496
|
+
if (!surface) {
|
|
10497
|
+
continue;
|
|
10498
|
+
}
|
|
10499
|
+
let words = controlDescription(el);
|
|
10500
|
+
if (words === '' || seen.has(el)) {
|
|
10501
|
+
continue;
|
|
10502
|
+
}
|
|
10503
|
+
seen.add(el);
|
|
10504
|
+
// the glyph belongs to the surface, not to a 0x0 input inside it
|
|
10505
|
+
let svg = surface.querySelector ? surface.querySelector('svg.aptx-glyph') : null;
|
|
10506
|
+
rows.push({name: controlName(el), words: words, glyph: svg});
|
|
10507
|
+
}
|
|
10508
|
+
return rows;
|
|
10509
|
+
}
|
|
10510
|
+
|
|
10511
|
+
function renderCheatSheet(body) {
|
|
10512
|
+
// everything but the standing lead paragraph
|
|
10513
|
+
let keep = body.querySelector('.aptx-cheat-lead');
|
|
10514
|
+
while (body.firstChild) {
|
|
10515
|
+
body.removeChild(body.firstChild);
|
|
10516
|
+
}
|
|
10517
|
+
if (keep) {
|
|
10518
|
+
body.appendChild(keep);
|
|
10519
|
+
}
|
|
10520
|
+
let rows = cheatSheetRows();
|
|
10521
|
+
rows.forEach(function (row) {
|
|
10522
|
+
let line = document.createElement('div');
|
|
10523
|
+
line.className = 'aptx-dialog-line aptx-cheat-line';
|
|
10524
|
+
let key = document.createElement('span');
|
|
10525
|
+
key.className = 'aptx-dialog-key aptx-cheat-key';
|
|
10526
|
+
if (row.glyph) {
|
|
10527
|
+
let g = row.glyph.cloneNode(true);
|
|
10528
|
+
g.removeAttribute('class');
|
|
10529
|
+
g.setAttribute('class', 'aptx-glyph aptx-cheat-glyph');
|
|
10530
|
+
key.appendChild(g);
|
|
10531
|
+
}
|
|
10532
|
+
if (row.name) {
|
|
10533
|
+
let n = document.createElement('span');
|
|
10534
|
+
n.className = 'aptx-cheat-name';
|
|
10535
|
+
n.textContent = row.name;
|
|
10536
|
+
key.appendChild(n);
|
|
10537
|
+
}
|
|
10538
|
+
let val = document.createElement('span');
|
|
10539
|
+
val.className = 'aptx-dialog-val';
|
|
10540
|
+
val.textContent = row.words;
|
|
10541
|
+
line.appendChild(key);
|
|
10542
|
+
line.appendChild(val);
|
|
10543
|
+
body.appendChild(line);
|
|
10544
|
+
});
|
|
10545
|
+
if (rows.length === 0) {
|
|
10546
|
+
let empty = document.createElement('p');
|
|
10547
|
+
empty.className = 'aptx-shortcuts-note';
|
|
10548
|
+
empty.textContent = 'No controls are showing.';
|
|
10549
|
+
body.appendChild(empty);
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
|
|
10553
|
+
// The panel is rebuilt on every render, so an open sheet is re-read from
|
|
10554
|
+
// it rather than left describing a panel that has changed underneath.
|
|
10555
|
+
function refreshCheatSheet() {
|
|
10556
|
+
if (_cheatSheetBody && _cheatSheetBody.isConnected) {
|
|
10557
|
+
renderCheatSheet(_cheatSheetBody);
|
|
10558
|
+
}
|
|
10559
|
+
}
|
|
10560
|
+
|
|
10561
|
+
function showCheatSheetDialog() {
|
|
10562
|
+
let shell = makeDialogShell(CHEAT_SHEET_DIALOG, 'Control panel', 460);
|
|
10563
|
+
shell.body.classList.add('aptx-shortcuts');
|
|
10564
|
+
shell.body.classList.add('aptx-cheat');
|
|
10565
|
+
// says what the reader is looking at, as the desktop's sheet does
|
|
10566
|
+
let lead = document.createElement('p');
|
|
10567
|
+
lead.className = 'aptx-cheat-lead';
|
|
10568
|
+
lead.textContent = 'Every control the panel is showing, with the icon it draws and what it does.';
|
|
10569
|
+
shell.body.appendChild(lead);
|
|
10570
|
+
_cheatSheetBody = shell.body;
|
|
10571
|
+
renderCheatSheet(shell.body);
|
|
10572
|
+
shell.dialog.addEventListener('close', function () {
|
|
10573
|
+
if (_cheatSheetBody === shell.body) {
|
|
10574
|
+
_cheatSheetBody = null;
|
|
10575
|
+
}
|
|
10576
|
+
});
|
|
10577
|
+
// modeless, as the colour picker is: the point is to read a row and
|
|
10578
|
+
// click the control it names without closing anything
|
|
10579
|
+
shell.dialog.show();
|
|
10580
|
+
placeBesidePanel(shell.dialog);
|
|
10581
|
+
}
|
|
10582
|
+
|
|
10583
|
+
// Beside the panel, never over it, and inside the window.
|
|
10584
|
+
function placeBesidePanel(dialog) {
|
|
10585
|
+
let panel = cheatSheetPanel();
|
|
10586
|
+
let box = panel ? panel.getBoundingClientRect() : null;
|
|
10587
|
+
let w = dialog.offsetWidth;
|
|
10588
|
+
let h = dialog.offsetHeight;
|
|
10589
|
+
let left = box ? (box.right + 10) : 10;
|
|
10590
|
+
if (left + w > window.innerWidth - 6) {
|
|
10591
|
+
left = box ? (box.left - w - 10) : 6;
|
|
10592
|
+
}
|
|
10593
|
+
setStyles(dialog, {
|
|
10594
|
+
'position': 'fixed',
|
|
10595
|
+
'margin': '0',
|
|
10596
|
+
'left': Math.max(6, Math.min(window.innerWidth - w - 6, left)) + 'px',
|
|
10597
|
+
'top': Math.max(6, Math.min(window.innerHeight - h - 6, box ? box.top : 10)) + 'px'
|
|
10598
|
+
});
|
|
10599
|
+
}
|
|
10600
|
+
|
|
10020
10601
|
function showShortcutsDialog() {
|
|
10021
10602
|
let shell = makeDialogShell(SHORTCUTS_DIALOG, 'Keyboard shortcuts', 400);
|
|
10022
10603
|
shell.body.classList.add('aptx-shortcuts');
|
|
@@ -10353,7 +10934,27 @@ function (root, d3, forester, phyloXml) {
|
|
|
10353
10934
|
return;
|
|
10354
10935
|
}
|
|
10355
10936
|
let existing = _svgGroup.select('g.aptx-domains');
|
|
10937
|
+
// An architecture goes WITH ITS NAME, and is left out when the name
|
|
10938
|
+
// is. domainsShown() already ties the whole track to external labels
|
|
10939
|
+
// being on; this is the same rule per tip, and it became necessary the
|
|
10940
|
+
// day unrooted started hiding crowded names -- before that every tip
|
|
10941
|
+
// in every layout kept its label, so a nameless architecture could not
|
|
10942
|
+
// happen. Measured on apaf when it could: 7 of 31 tips drew an
|
|
10943
|
+
// architecture with no name beside it, identifiable only by which
|
|
10944
|
+
// spoke it sat on. (The desktop reached the same rule from the other
|
|
10945
|
+
// end: a tip IMAGE painted before its label's claim was left orphaned
|
|
10946
|
+
// under a neighbour's name.)
|
|
10356
10947
|
let tips = (domainsShown() && _root) ? displayedTips().filter(function (d) {
|
|
10948
|
+
// Keyed on "the RULE hid this name", not on "there is no name".
|
|
10949
|
+
// The two part company the moment the user switches the name
|
|
10950
|
+
// fields off: nothing is hidden by the rule then, every label is
|
|
10951
|
+
// empty, and every architecture should still be drawn. Keying on
|
|
10952
|
+
// the empty text took all 31 of apaf's architectures away with the
|
|
10953
|
+
// Node Name checkbox -- shipped, and caught by the desktop
|
|
10954
|
+
// warning us they had met the same reading of their own fix.
|
|
10955
|
+
if (d._labelDropped) {
|
|
10956
|
+
return false;
|
|
10957
|
+
}
|
|
10357
10958
|
return _state.unrootedDisplay ? (d.ux !== undefined) : (d.x !== undefined);
|
|
10358
10959
|
}) : [];
|
|
10359
10960
|
let f = tips.length > 0 ? domainScale() : 0;
|
|
@@ -11519,7 +12120,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
11519
12120
|
|
|
11520
12121
|
// what the viewer writes onto a tree's nodes, left off a tree cut out of it
|
|
11521
12122
|
const VIEW_NODE_FIELDS = ['viewId', 'collapsed', 'x', 'y', 'x0', 'y0', 'id', 'hide', 'hasVis', 'distToRoot',
|
|
11522
|
-
'ux', 'uy', 'uangle', '_style', '_suppDot', '_extLabelText', '_eventText', '_confText', '_blText'
|
|
12123
|
+
'ux', 'uy', 'uangle', '_style', '_suppDot', '_extLabelText', '_eventText', '_confText', '_blText',
|
|
12124
|
+
'_labelBoxes', '_labelDropped', '_markBoxes'];
|
|
11523
12125
|
|
|
11524
12126
|
// the whole tree's tip count, counted again only when the tree changes
|
|
11525
12127
|
// (every change makes new _basicTreeProperties)
|
|
@@ -12315,16 +12917,27 @@ function (root, d3, forester, phyloXml) {
|
|
|
12315
12917
|
scheduleUpdate(null, 0);
|
|
12316
12918
|
}
|
|
12317
12919
|
|
|
12920
|
+
// In CIRCULAR these two leave the aligned/unaligned flag alone. That
|
|
12921
|
+
// layout aligns whatever the flag says, so the button shown as chosen
|
|
12922
|
+
// there is "aligned" regardless -- and writing the flag from it would
|
|
12923
|
+
// silently change the user's choice for the layouts that CAN express it.
|
|
12924
|
+
// A user who likes the plain phylogram in rectangular, steps into
|
|
12925
|
+
// circular, flips to the cladogram and back, and returns, would have
|
|
12926
|
+
// found the aligned one waiting.
|
|
12318
12927
|
function toAlignedPhylogram() {
|
|
12319
12928
|
_state.phylogram = true;
|
|
12320
|
-
_state.
|
|
12929
|
+
if (!_state.circularDisplay) {
|
|
12930
|
+
_state.alignPhylogram = true;
|
|
12931
|
+
}
|
|
12321
12932
|
setDisplayTypeButtons();
|
|
12322
12933
|
scheduleUpdate(null, 0);
|
|
12323
12934
|
}
|
|
12324
12935
|
|
|
12325
12936
|
function toCladegram() {
|
|
12326
12937
|
_state.phylogram = false;
|
|
12327
|
-
_state.
|
|
12938
|
+
if (!_state.circularDisplay) {
|
|
12939
|
+
_state.alignPhylogram = false;
|
|
12940
|
+
}
|
|
12328
12941
|
setDisplayTypeButtons();
|
|
12329
12942
|
scheduleUpdate(null, 0);
|
|
12330
12943
|
}
|
|
@@ -12356,19 +12969,40 @@ function (root, d3, forester, phyloXml) {
|
|
|
12356
12969
|
// against; an alignment / time axis is inherently horizontal. Synced
|
|
12357
12970
|
// BEFORE the zoom-row early return, and (via updateButtonEnabledState)
|
|
12358
12971
|
// on every render, so subtree switches keep them honest.
|
|
12972
|
+
// WHICH of the two phylogram buttons is dead depends on the layout,
|
|
12973
|
+
// and it is the one the layout CANNOT do -- not the one it always
|
|
12974
|
+
// does. Circular always carries its external labels out to a common
|
|
12975
|
+
// ring on dashed connectors, whatever is chosen, so the circular
|
|
12976
|
+
// phylogram IS the aligned one: the unavailable option there is the
|
|
12977
|
+
// UNALIGNED phylogram. Unrooted is the other way round -- it has no
|
|
12978
|
+
// common edge to align to, so what it cannot do is align.
|
|
12979
|
+
//
|
|
12980
|
+
// (Greying the aligned button in circular was the first attempt, on
|
|
12981
|
+
// the grounds that picking it changed nothing. True, but backwards:
|
|
12982
|
+
// it disabled the button that describes what the layout does and left
|
|
12983
|
+
// live the one it cannot do, and the panel then showed "phylogram"
|
|
12984
|
+
// selected over a picture whose labels are plainly aligned. Christian
|
|
12985
|
+
// spotted the inconsistency, 2026-09-27.)
|
|
12986
|
+
let measured = _basicTreeProperties.branchLengths === true;
|
|
12359
12987
|
let phyBtn = byId(PHYLOGRAM_BUTTON);
|
|
12360
12988
|
if (phyBtn) {
|
|
12361
|
-
phyBtn.disabled =
|
|
12989
|
+
phyBtn.disabled = !measured || _state.circularDisplay;
|
|
12990
|
+
phyBtn.title = PHYLOGRAM_WHAT;
|
|
12362
12991
|
}
|
|
12363
12992
|
let alignBtn = byId(PHYLOGRAM_ALIGNED_BUTTON);
|
|
12364
12993
|
if (alignBtn) {
|
|
12365
|
-
alignBtn.disabled =
|
|
12994
|
+
alignBtn.disabled = !measured || _state.unrootedDisplay;
|
|
12995
|
+
alignBtn.title = PHYLOGRAM_ALIGNED_WHAT;
|
|
12366
12996
|
}
|
|
12997
|
+
// Which button is SHOWN as chosen is layout-dependent too, and this
|
|
12998
|
+
// was only re-synced when the display type changed -- so switching
|
|
12999
|
+
// layout left the previous layout's answer on the screen.
|
|
13000
|
+
setDisplayTypeButtons();
|
|
12367
13001
|
// Auto-hide Labels stays live in every layout. It used to be greyed out
|
|
12368
|
-
// in unrooted, where the tip-label decimation
|
|
12369
|
-
// but it also governs the crowded branch-data rule,
|
|
12370
|
-
//
|
|
12371
|
-
//
|
|
13002
|
+
// in unrooted, where the every-k-th tip-label decimation does not run --
|
|
13003
|
+
// but it also governs the crowded branch-data rule, and now the
|
|
13004
|
+
// unrooted tip-label rule, both of which do run there, so greying it
|
|
13005
|
+
// out left the user unable to switch off what they could see.
|
|
12372
13006
|
let msaCb = byId(MSA_CB);
|
|
12373
13007
|
if (msaCb) {
|
|
12374
13008
|
msaCb.disabled = radialDisplay();
|
|
@@ -12585,7 +13219,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
12585
13219
|
// panel's accent: the checkbox item gets a box and its tooltip says how
|
|
12586
13220
|
// many labels are shown. Called on every redraw, since the density
|
|
12587
13221
|
// changes with the vertical zoom, the font size and the view.
|
|
12588
|
-
|
|
13222
|
+
// Lights the toggle while it is actually taking something away, and says
|
|
13223
|
+
// what. The switch governs three rules, and the light has to ask all three
|
|
13224
|
+
// rather than whichever one was written first: the every-k-th thinning
|
|
13225
|
+
// (rectangular, by index against the row pitch), the overlap thinning
|
|
13226
|
+
// (both fans, by whether names would overprint) and the crowded
|
|
13227
|
+
// branch-data rule (every layout). Asking only the first two left it dark
|
|
13228
|
+
// over a circular tree with 201 numbers refused and every name drawn --
|
|
13229
|
+
// and over a RECTANGULAR one with 18 refused, which was never right.
|
|
13230
|
+
function syncDynahideIndicator() {
|
|
12589
13231
|
let cb = byId(DYNAHIDE_CB);
|
|
12590
13232
|
let item = cb ? cb.closest('.aptx-check') : null;
|
|
12591
13233
|
if (!item) {
|
|
@@ -12594,10 +13236,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
12594
13236
|
if (item.dataset.baseTitle === undefined) {
|
|
12595
13237
|
item.dataset.baseTitle = item.title;
|
|
12596
13238
|
}
|
|
12597
|
-
|
|
13239
|
+
let parts = [];
|
|
13240
|
+
if (!radialDisplay() && _dynahide_factor >= 2) {
|
|
13241
|
+
parts.push('1 in ' + _dynahide_factor + ' labels shown');
|
|
13242
|
+
} else if (radialDisplay() && _radialNamesHidden > 0) {
|
|
13243
|
+
parts.push(_radialNamesHidden + ' name' + (_radialNamesHidden === 1 ? '' : 's')
|
|
13244
|
+
+ ' that would overprint');
|
|
13245
|
+
}
|
|
13246
|
+
if (_branchMarksHidden > 0) {
|
|
13247
|
+
// said only when it is happening: the clause used to be printed
|
|
13248
|
+
// unconditionally, which made a promise of every render
|
|
13249
|
+
parts.push(_branchMarksHidden + ' branch value' + (_branchMarksHidden === 1 ? '' : 's')
|
|
13250
|
+
+ ' that would overlap');
|
|
13251
|
+
}
|
|
13252
|
+
let active = _state.dynahide === true && parts.length > 0;
|
|
13253
|
+
item.classList.toggle('aptx-check-active', active);
|
|
12598
13254
|
item.title = active
|
|
12599
|
-
? item.dataset.baseTitle + ', hiding now:
|
|
12600
|
-
+ ' labels shown, and branch data that would overlap'
|
|
13255
|
+
? item.dataset.baseTitle + ', hiding now: ' + parts.join(', and ')
|
|
12601
13256
|
: item.dataset.baseTitle;
|
|
12602
13257
|
}
|
|
12603
13258
|
|
|
@@ -13236,6 +13891,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
13236
13891
|
+ glyphDot(50, 29, 5.5);
|
|
13237
13892
|
}
|
|
13238
13893
|
|
|
13894
|
+
// a reference card: the outline, and the rows on it
|
|
13895
|
+
function glyphCheatSheet() {
|
|
13896
|
+
return '<rect x="18" y="12" width="64" height="76" rx="8"/>'
|
|
13897
|
+
+ glyphLine(32, 34, 68, 34)
|
|
13898
|
+
+ glyphLine(32, 50, 68, 50)
|
|
13899
|
+
+ glyphLine(32, 66, 56, 66);
|
|
13900
|
+
}
|
|
13901
|
+
|
|
13239
13902
|
function glyphSun() {
|
|
13240
13903
|
let s = glyphDot(50, 50, 20);
|
|
13241
13904
|
for (let i = 0; i < 8; ++i) {
|
|
@@ -13304,6 +13967,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13304
13967
|
case 'tree_prev': sw = 9; cap = 'round'; body = glyphChevron(false); break;
|
|
13305
13968
|
case 'tree_next': sw = 9; cap = 'round'; body = glyphChevron(true); break;
|
|
13306
13969
|
case 'info': sw = 7; cap = 'round'; body = glyphInfo(); break;
|
|
13970
|
+
case 'cheat_sheet': sw = 7; cap = 'round'; body = glyphCheatSheet(); break;
|
|
13307
13971
|
case 'sun': body = glyphSun(); break;
|
|
13308
13972
|
case 'moon': body = glyphMoon(); break;
|
|
13309
13973
|
default: throw new Error('unknown control-panel glyph: ' + kind);
|
|
@@ -13502,6 +14166,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
13502
14166
|
+ '.aptx-keys { display:inline-flex; gap:3px; flex-wrap:wrap; }'
|
|
13503
14167
|
+ '.aptx-kbd { font-family:inherit; font-size:10.5px; line-height:1; padding:3px 5px; min-width:18px; text-align:center; color:var(--p-ink); background:var(--p-surface2); border:1px solid var(--p-line-strong); border-bottom-width:2px; border-radius:4px; }'
|
|
13504
14168
|
+ '.aptx-shortcuts-note { margin:8px 0 0; font-size:10.5px; line-height:1.4; color:var(--p-muted); }'
|
|
14169
|
+
// the cheat sheet: a scrolling card of glyph + name : description
|
|
14170
|
+
// 31 rows is taller than a short window; the card scrolls rather
|
|
14171
|
+
// than running off the bottom, and the title bar stays put
|
|
14172
|
+
+ '#' + CHEAT_SHEET_DIALOG + ' { max-height:min(78vh, 760px); display:flex; flex-direction:column; }'
|
|
14173
|
+
+ '#' + CHEAT_SHEET_DIALOG + ' .aptx-dialog-body { min-height:0; }'
|
|
14174
|
+
+ '.aptx-cheat-lead { margin:0 0 9px; font-size:10.5px; line-height:1.4; color:var(--p-muted); }'
|
|
14175
|
+
+ '.aptx-cheat .aptx-dialog-line { padding:3px 0; align-items:flex-start; }'
|
|
14176
|
+
+ '.aptx-cheat .aptx-cheat-key { flex:0 0 38%; display:flex; align-items:center; gap:6px; color:var(--p-ink); }'
|
|
14177
|
+
+ '.aptx-cheat .aptx-cheat-glyph { width:14px; height:14px; flex:none; color:var(--p-muted); }'
|
|
14178
|
+
+ '.aptx-cheat .aptx-cheat-name { overflow-wrap:anywhere; }'
|
|
14179
|
+
+ '.aptx-cheat .aptx-dialog-val { color:var(--p-muted); line-height:1.35; }'
|
|
14180
|
+
+ '.aptx-cheat .aptx-cheat-key:not(:empty)::after { content:":"; color:var(--p-faint); }'
|
|
13505
14181
|
// representative tips: the input dialog and its result
|
|
13506
14182
|
+ '.aptx-reps { line-height:1.35; }'
|
|
13507
14183
|
+ '.aptx-reps-lead { margin:0 0 4px; }'
|
|
@@ -13573,7 +14249,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
13573
14249
|
+ '.aptx-tip .aptx-dialog-line { padding:1px 0; }'
|
|
13574
14250
|
+ '.aptx-tip .aptx-dialog-line + .aptx-dialog-line { border-top:0; }'
|
|
13575
14251
|
+ '.aptx-tip .aptx-dialog-key { flex:0 0 auto; min-width:96px; }'
|
|
13576
|
-
|
|
14252
|
+
// WRAPS. The title's longest word cannot break, so on a narrow
|
|
14253
|
+
// panel the buttons had nowhere to go and overflowed the padding:
|
|
14254
|
+
// with four of them they ended 2.4 px past the panel's own right
|
|
14255
|
+
// edge, and squeezed the title into two ragged centred lines on
|
|
14256
|
+
// the way. Wrapping puts them on their own right-aligned row
|
|
14257
|
+
// instead, which also gives the title back its full width -- so
|
|
14258
|
+
// "Archaeopteryx.js 3.15.0" sits on ONE line where it used to
|
|
14259
|
+
// break in two.
|
|
14260
|
+
+ '.aptx-panel .' + PROG_NAME + ' { display:flex; align-items:center; flex-wrap:wrap; gap:6px 8px; padding:9px 12px; border-bottom:1px solid var(--p-line); font-weight:600; letter-spacing:-0.01em; }'
|
|
13577
14261
|
+ '.aptx-panel .' + PROGNAMELINK + ',.aptx-panel .' + PROGNAMELINK + ':link,.aptx-panel .' + PROGNAMELINK + ':visited { color:var(--p-accent-ink); text-decoration:none; font-size:12px; border:0; }'
|
|
13578
14262
|
+ '.aptx-panel .' + PROGNAMELINK + ' { background:none; padding:0; margin:0; cursor:pointer;'
|
|
13579
14263
|
+ ' font-family:inherit; font-weight:inherit; letter-spacing:inherit; }'
|
|
@@ -13824,6 +14508,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
13824
14508
|
});
|
|
13825
14509
|
actions.appendChild(infoBtn);
|
|
13826
14510
|
|
|
14511
|
+
let sheetBtn = document.createElement('button');
|
|
14512
|
+
sheetBtn.type = 'button';
|
|
14513
|
+
sheetBtn.className = 'aptx-info-btn';
|
|
14514
|
+
sheetBtn.title = 'Control panel: what each control does';
|
|
14515
|
+
sheetBtn.setAttribute('aria-label', 'Control panel cheat sheet');
|
|
14516
|
+
sheetBtn.innerHTML = makeGlyph('cheat_sheet');
|
|
14517
|
+
sheetBtn.addEventListener('click', function (e) {
|
|
14518
|
+
e.stopPropagation();
|
|
14519
|
+
showCheatSheetDialog();
|
|
14520
|
+
});
|
|
14521
|
+
actions.appendChild(sheetBtn);
|
|
14522
|
+
|
|
13827
14523
|
let progName = header.querySelector('.' + PROGNAMELINK);
|
|
13828
14524
|
if (progName) {
|
|
13829
14525
|
// Clicking the title used to navigate away to the project site;
|
|
@@ -13895,6 +14591,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
13895
14591
|
fitPanelSections(panel, name);
|
|
13896
14592
|
}
|
|
13897
14593
|
savePanelSections();
|
|
14594
|
+
// a folded section's controls are off the screen, so they
|
|
14595
|
+
// leave the cheat sheet: it describes what the panel is
|
|
14596
|
+
// showing, and folding does not go through a tree render
|
|
14597
|
+
refreshCheatSheet();
|
|
13898
14598
|
});
|
|
13899
14599
|
}
|
|
13900
14600
|
// What arrived open counts as opened, oldest at the BOTTOM: a panel is
|
|
@@ -14683,7 +15383,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
14683
15383
|
['Desktop version', DESKTOP_WEBSITE, 'cmzmasek.github.io/archaeopteryx'],
|
|
14684
15384
|
['Source code', SOURCE_WEBSITE, 'github.com/cmzmasek/archaeopteryx-js'],
|
|
14685
15385
|
['License', LICENSE_WEBSITE, LICENSE_NAME],
|
|
14686
|
-
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')']
|
|
15386
|
+
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')'],
|
|
15387
|
+
['Control panel', null, 'what each control does']].forEach(function (row) {
|
|
14687
15388
|
let line = document.createElement('div');
|
|
14688
15389
|
line.className = 'aptx-dialog-line';
|
|
14689
15390
|
let key = document.createElement('span');
|
|
@@ -14697,12 +15398,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
14697
15398
|
a.target = '_blank';
|
|
14698
15399
|
a.rel = 'noopener noreferrer';
|
|
14699
15400
|
} else {
|
|
14700
|
-
// the
|
|
15401
|
+
// the two rows with no URL open another dialog in place of
|
|
15402
|
+
// this one: the key list, and the control-panel sheet
|
|
15403
|
+
let opener = row[0] === 'Control panel' ? showCheatSheetDialog : showShortcutsDialog;
|
|
14701
15404
|
a.href = '#';
|
|
14702
15405
|
a.addEventListener('click', function (e) {
|
|
14703
15406
|
e.preventDefault();
|
|
14704
15407
|
shell.dialog.close();
|
|
14705
|
-
|
|
15408
|
+
opener();
|
|
14706
15409
|
});
|
|
14707
15410
|
}
|
|
14708
15411
|
a.textContent = row[2];
|
|
@@ -15273,7 +15976,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
15273
15976
|
function makeProgramDesc() {
|
|
15274
15977
|
let h = "";
|
|
15275
15978
|
h = h.concat('<div class=' + PROG_NAME + '>');
|
|
15276
|
-
|
|
15979
|
+
// aria-label without the version: the cheat sheet names a control
|
|
15980
|
+
// by this, and a version is a value -- it belongs in the About box
|
|
15981
|
+
// it opens, not in a row that says what the button is
|
|
15982
|
+
h = h.concat('<button type="button" class="' + PROGNAMELINK + '" aria-label="' + NAME
|
|
15983
|
+
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION + '</button>');
|
|
15277
15984
|
h = h.concat('</div>');
|
|
15278
15985
|
return h;
|
|
15279
15986
|
}
|
|
@@ -15424,14 +16131,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
15424
16131
|
// desktop
|
|
15425
16132
|
let layoutGroup = 'layout_control_radio';
|
|
15426
16133
|
h = h.concat('<div class="aptx-segmented">');
|
|
15427
|
-
h = h.concat(makeSegment(makeGlyph('rectangular'), LAYOUT_RECT_BUTTON, layoutGroup,
|
|
15428
|
-
|
|
15429
|
-
h = h.concat(makeSegment(makeGlyph('
|
|
16134
|
+
h = h.concat(makeSegment(makeGlyph('rectangular'), LAYOUT_RECT_BUTTON, layoutGroup,
|
|
16135
|
+
'rectangular layout: the root at the left, the tips in even rows down the page'));
|
|
16136
|
+
h = h.concat(makeSegment(makeGlyph('circular'), LAYOUT_CIRC_BUTTON, layoutGroup,
|
|
16137
|
+
'circular layout: the tree as a fan, every tip label carried out to a common ring'));
|
|
16138
|
+
h = h.concat(makeSegment(makeGlyph('unrooted'), LAYOUT_UNROOTED_BUTTON, layoutGroup,
|
|
16139
|
+
'unrooted layout: equal-angle, drawing no root branch at all'));
|
|
15430
16140
|
h = h.concat('</div>');
|
|
15431
16141
|
h = h.concat('<div class="' + PHYLOGRAM_CLADOGRAM_CONTROLGROUP + ' aptx-segmented">');
|
|
15432
|
-
h = h.concat(makeSegment(makeGlyph('phylogram'), PHYLOGRAM_BUTTON, radioGroup,
|
|
15433
|
-
h = h.concat(makeSegment(makeGlyph('aligned_phylogram'), PHYLOGRAM_ALIGNED_BUTTON, radioGroup,
|
|
15434
|
-
h = h.concat(makeSegment(makeGlyph('cladogram'), CLADOGRAM_BUTTON, radioGroup,
|
|
16142
|
+
h = h.concat(makeSegment(makeGlyph('phylogram'), PHYLOGRAM_BUTTON, radioGroup, PHYLOGRAM_WHAT));
|
|
16143
|
+
h = h.concat(makeSegment(makeGlyph('aligned_phylogram'), PHYLOGRAM_ALIGNED_BUTTON, radioGroup, PHYLOGRAM_ALIGNED_WHAT));
|
|
16144
|
+
h = h.concat(makeSegment(makeGlyph('cladogram'), CLADOGRAM_BUTTON, radioGroup, CLADOGRAM_WHAT));
|
|
15435
16145
|
h = h.concat('</div>');
|
|
15436
16146
|
// The branch SCALE, when the tree states two different things: its
|
|
15437
16147
|
// nodes' dates, and its branches' divergence. Hidden otherwise,
|
|
@@ -15514,7 +16224,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
15514
16224
|
// "Auto-hide Labels" matches the desktop, which renamed the historical
|
|
15515
16225
|
// "Dyna Hide" because that named the mechanism rather than what the user
|
|
15516
16226
|
// sees. The _state.dynahide field keeps its name internally.
|
|
15517
|
-
opts.push(makeCheckboxItem('Auto-hide Labels', DYNAHIDE_CB, 'automatically hide external labels, and branch values that would overprint each other, where the tree is too dense for them to be readable', true));
|
|
16227
|
+
opts.push(makeCheckboxItem('Auto-hide Labels', DYNAHIDE_CB, 'automatically hide external labels, and branch values that would overprint a name or each other, where the tree is too dense for them to be readable', true));
|
|
15518
16228
|
opts.push(makeCheckboxItem('Short Names', SHORTEN_NODE_NAME_CB, 'to shorten long node names'));
|
|
15519
16229
|
if (_basicTreeProperties.confidences) {
|
|
15520
16230
|
opts.push(makeCheckboxItem('Support Dots', SUPPORT_DOTS_CB, 'to mark a branch with support of at least ' + _settings.supportDotMinimum + '% with a dot at its midpoint, where there is room to draw it'));
|
|
@@ -15594,7 +16304,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
15594
16304
|
h = h.concat('<fieldset>');
|
|
15595
16305
|
h = h.concat('<input type="button" value="Download" name="' + DOWNLOAD_BUTTON + '" title="download/export tree in a selected format" id="' + DOWNLOAD_BUTTON + '">');
|
|
15596
16306
|
//h = h.concat('<br>');
|
|
15597
|
-
h = h.concat('<select name="' + EXPORT_FORMAT_SELECT + '" id="' + EXPORT_FORMAT_SELECT
|
|
16307
|
+
h = h.concat('<select name="' + EXPORT_FORMAT_SELECT + '" id="' + EXPORT_FORMAT_SELECT
|
|
16308
|
+
+ '" aria-label="Download format" title="the file format the Download button writes">');
|
|
15598
16309
|
if (pngExportAvailable()) {
|
|
15599
16310
|
h = h.concat('<option value="' + PNG_EXPORT_FORMAT + '">' + PNG_EXPORT_FORMAT + '</option>');
|
|
15600
16311
|
}
|
|
@@ -15744,8 +16455,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
15744
16455
|
let h = "";
|
|
15745
16456
|
h = h.concat('<label class="aptx-field-label" for="' + val + '">' + label + '</label>');
|
|
15746
16457
|
h = h.concat('<div class="aptx-search-menus">');
|
|
15747
|
-
h = h.concat('<select class="aptx-search-field" name="' + fsel + '" id="' + fsel
|
|
15748
|
-
|
|
16458
|
+
h = h.concat('<select class="aptx-search-field" name="' + fsel + '" id="' + fsel
|
|
16459
|
+
+ '" aria-label="' + label + ' field" title="the field to search in"></select>');
|
|
16460
|
+
h = h.concat('<select class="aptx-search-mode" name="' + msel + '" id="' + msel
|
|
16461
|
+
+ '" aria-label="' + label + ' match" title="how to match"></select>');
|
|
15749
16462
|
h = h.concat('</div>');
|
|
15750
16463
|
h = h.concat('<div class="aptx-search-row">');
|
|
15751
16464
|
h = h.concat('<input class="aptx-search-value" autocomplete="off" title="' + valTip + '" type="text" name="' + val + '" id="' + val + '">');
|
|
@@ -15952,9 +16665,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
15952
16665
|
}
|
|
15953
16666
|
|
|
15954
16667
|
function setDisplayTypeButtons() {
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
16668
|
+
// In circular the labels ARE aligned, whatever alignPhylogram says, so
|
|
16669
|
+
// that is the button shown as chosen: the panel describes the picture.
|
|
16670
|
+
// The flag itself is left alone -- it is the user's choice for the
|
|
16671
|
+
// layouts that can express it, and returning to rectangular restores
|
|
16672
|
+
// what they picked there.
|
|
16673
|
+
let alignedLook = _state.phylogram && (_state.alignPhylogram || _state.circularDisplay);
|
|
16674
|
+
setRadioButtonValue(PHYLOGRAM_BUTTON, _state.phylogram && !alignedLook);
|
|
16675
|
+
setRadioButtonValue(CLADOGRAM_BUTTON, !_state.phylogram);
|
|
16676
|
+
setRadioButtonValue(PHYLOGRAM_ALIGNED_BUTTON, alignedLook);
|
|
15958
16677
|
setCheckboxValue(LAYOUT_CIRC_BUTTON, _state.circularDisplay);
|
|
15959
16678
|
setCheckboxValue(LAYOUT_UNROOTED_BUTTON, _state.unrootedDisplay);
|
|
15960
16679
|
setCheckboxValue(LAYOUT_RECT_BUTTON, !radialDisplay());
|
|
@@ -15975,6 +16694,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
15975
16694
|
|
|
15976
16695
|
function updateButtonEnabledState() {
|
|
15977
16696
|
syncZoomRowButtons(); // layout- and tree-dependent disables track every render
|
|
16697
|
+
refreshCheatSheet(); // an open sheet describes the panel as it is now
|
|
15978
16698
|
// the uncollapse-all button lives only while something is collapsed, as on the desktop
|
|
15979
16699
|
((_root && collapseAllowed() && hasCollapsedIn(_root)) ? enableButton : disableButton)(byId(UNCOLLAPSE_ALL_BUTTON));
|
|
15980
16700
|
if (_in_subtree) {
|