archaeopteryx 3.27.0 → 3.28.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/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.27.0
23
+ // v 3.28.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.27.0';
106
+ const VERSION = '3.28.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';
@@ -272,6 +272,9 @@ function (root, d3, forester, phyloXml) {
272
272
  const PHYLOGRAM_MIN_BRANCH_FRACTION = 0.5;
273
273
  const SHORTEN_NAME_MAX_LENGTH = 18;
274
274
  const PANEL_STYLE_ID = 'aptx-panel-styles';
275
+ // The tree's own <svg>: named so that the stylesheet can say what a host
276
+ // page's rules for a bare "svg" would otherwise decide (hostProofCss).
277
+ const TREE_SVG_CLASS = 'aptx-tree';
275
278
  // How wide a legend row is treated as, for grabbing it with the mouse.
276
279
  const PANEL_WIDTH = 214; // fixed control-panel width; shared by the .aptx-panel CSS and leftPanelClearance() so the two can't drift
277
280
  const PANEL_WIDTH_COMPACT = 196; // the same, at panelDensity 'compact'; panelWidth() picks between them
@@ -454,6 +457,7 @@ function (root, d3, forester, phyloXml) {
454
457
  // archaeopteryx-anime.svg), inlined so the library stays a single file.
455
458
  // Gradient ids are prefixed: they were generic enough to collide with an
456
459
  // embedding page's own defs.
460
+ // (drawn with attributes, as any svg file is; paintInline moves its paint into styles where it is used)
457
461
  const ARCHAEOPTERYX_LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Archaeopteryx"> <defs> <linearGradient id="aptxlogo-wg" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#4f46e5"/><stop offset="0.55" stop-color="#38bdf8"/><stop offset="1" stop-color="#a7f3d0"/> </linearGradient> <linearGradient id="aptxlogo-wg2" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#3730a3"/><stop offset="1" stop-color="#4f79e0"/> </linearGradient> <linearGradient id="aptxlogo-tg" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#f43f5e"/><stop offset="0.5" stop-color="#fb923c"/><stop offset="1" stop-color="#fde047"/> </linearGradient> <linearGradient id="aptxlogo-bg" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#fff7e0"/><stop offset="1" stop-color="#fcd34d"/> </linearGradient> </defs> <g transform="translate(28.9,18.0) scale(0.9091)"> <g stroke="#25304f" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"> <path d="M 78.0,152.0 C 49.3,150.6 12.5,173.3 -4.4,194.3 C 25.1,192.0 66.1,175.5 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 48.1,143.8 3.5,158.8 -19.7,176.6 C 12.1,181.2 59.5,173.7 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 51.8,138.2 6.9,142.5 -18.7,154.0 C 10.0,164.8 56.0,167.9 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 58.9,135.3 20.6,130.3 -2.8,135.1 C 18.9,149.7 56.6,161.2 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 118.0,126.0 C 110.4,106.2 84.4,93.0 65.5,92.1 C 75.9,110.5 99.0,129.6 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 118.0,126.0 C 115.6,103.3 92.6,79.7 73.5,71.3 C 79.1,93.8 97.5,122.0 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 118.0,126.0 C 123.0,106.1 110.4,79.8 96.5,67.4 C 94.4,87.9 101.6,117.0 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 106,172 L 101,202 M 132,172 L 129,202" fill="none" stroke-width="9"/> <path d="M 101,202 L 88,209 M 101,202 L 112,210 M 101,202 L 99,213 M 129,202 L 116,209 M 129,202 L 140,210 M 129,202 L 128,213" fill="none" stroke-width="6"/> <path d="M 96,176 C 74,168 66,146 76,128 C 86,110 110,102 132,106 C 152,110 164,126 162,146 C 160,166 142,180 120,180 C 112,180 103,179 96,176 Z" fill="url(#aptxlogo-bg)"/> <path d="M 100,174 C 86,166 82,150 90,138 C 96,146 108,152 122,152 C 136,152 148,148 156,140 C 160,156 150,172 128,177 C 118,179 108,178 100,174 Z" fill="#fff6dd" stroke="none"/> <path d="M 96,176 C 74,168 66,146 76,128 C 86,110 110,102 132,106 C 152,110 164,126 162,146 C 160,166 142,180 120,180 C 112,180 103,179 96,176 Z" fill="none"/> <path d="M 150.0,60.0 C 159.5,51.2 158.7,35.2 152.9,25.9 C 145.3,35.7 141.5,51.8 150.0,60.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 164.0,56.0 C 174.4,51.0 177.8,37.3 175.2,28.0 C 166.2,34.2 158.9,46.9 164.0,56.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 136.0,66.0 C 141.8,56.5 137.0,44.2 129.6,38.5 C 125.7,48.3 126.7,61.9 136.0,66.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 158,58 C 186,54 208,72 208,96 C 208,120 188,136 164,134 C 140,132 126,114 128,92 C 130,72 142,60 158,58 Z" fill="url(#aptxlogo-bg)"/> <path d="M 203,82 C 216,84 230,92 238,100 C 230,108 216,113 202,112 C 206,104 206,90 203,82 Z" fill="#fb923c"/> <path d="M 204,101 C 214,102 224,103 234,101" fill="none" stroke-width="4"/> <path d="M 128.0,116.0 C 113.1,91.9 75.3,80.3 50.0,83.2 C 68.3,104.8 103.7,124.6 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 118.0,85.8 79.7,60.2 51.1,54.1 C 65.4,83.1 98.9,116.3 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 127.0,83.7 97.3,47.3 71.5,32.7 C 76.9,65.0 99.8,107.4 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 136.1,88.7 121.1,50.1 103.3,30.9 C 99.0,59.5 106.6,101.2 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> </g> <g> <ellipse cx="172" cy="94" rx="15" ry="17" fill="#25304f"/> <ellipse cx="172" cy="97" rx="10" ry="11" fill="#38bdf8"/> <circle cx="167" cy="88" r="6" fill="#ffffff"/> <circle cx="178" cy="103" r="3" fill="#ffffff" opacity="0.9"/> <ellipse cx="146" cy="112" rx="9" ry="5" fill="#fb7185" opacity="0.75"/> <path d="M 52,53 C 53.62,59.75 54.25,60.38 61,62 C 54.25,63.62 53.62,64.25 52,71 C 50.38,64.25 49.75,63.62 43,62 C 49.75,60.38 50.38,59.75 52,53 Z" fill="#fde68a"/> <path d="M 222,161 C 223.26,166.25 223.75,166.74 229,168 C 223.75,169.26 223.26,169.75 222,175 C 220.74,169.75 220.25,169.26 215,168 C 220.25,166.74 220.74,166.25 222,161 Z" fill="#fde68a"/> <path d="M 86,208 C 87.08,212.5 87.5,212.92 92,214 C 87.5,215.08 87.08,215.5 86,220 C 84.92,215.5 84.5,215.08 80,214 C 84.5,212.92 84.92,212.5 86,208 Z" fill="#fde68a"/> </g> </g> </svg>';
458
462
  const NODE_DATA = 'node_data_dialog';
459
463
  const COLOR_PICKER = 'color_picker_dialog';
@@ -1538,9 +1542,9 @@ function (root, d3, forester, phyloXml) {
1538
1542
  let fill = hoverGlowColor(d);
1539
1543
  HOVER_GLOW_RADII.forEach(function (radius, i) {
1540
1544
  g.append('circle')
1541
- .attr('fill-opacity', HOVER_GLOW_ALPHAS[i])
1542
- .attr('stroke', 'none')
1543
- .attr('fill', fill)
1545
+ .style('fill-opacity', HOVER_GLOW_ALPHAS[i])
1546
+ .style('stroke', 'none')
1547
+ .style('fill', fill)
1544
1548
  .attr('r', (dia * radius) / 2);
1545
1549
  });
1546
1550
  }
@@ -2758,7 +2762,7 @@ function (root, d3, forester, phyloXml) {
2758
2762
  chips.forEach(function (chip) {
2759
2763
  let t = g.append('text')
2760
2764
  .attr('x', chipRight).attr('y', baseline)
2761
- .attr('text-anchor', 'end')
2765
+ .style('text-anchor', 'end')
2762
2766
  .style('font', rowFont)
2763
2767
  .style('fill', ink)
2764
2768
  .style('fill-opacity', 0.75)
@@ -2786,10 +2790,10 @@ function (root, d3, forester, phyloXml) {
2786
2790
  let frac = max > min ? (mean - min) / (max - min) : 0.5;
2787
2791
  let gradId = 'aptx_legend_ramp_' + id;
2788
2792
  let grad = g.append('defs').append('linearGradient').attr('id', gradId);
2789
- grad.append('stop').attr('offset', '0%').attr('stop-color', VIS_COLOR_RAMP[0]);
2793
+ grad.append('stop').attr('offset', '0%').style('stop-color', VIS_COLOR_RAMP[0]);
2790
2794
  grad.append('stop').attr('offset', (100 * Math.max(0.01, Math.min(0.99, frac))) + '%')
2791
- .attr('stop-color', VIS_COLOR_RAMP[1]);
2792
- grad.append('stop').attr('offset', '100%').attr('stop-color', VIS_COLOR_RAMP[2]);
2795
+ .style('stop-color', VIS_COLOR_RAMP[1]);
2796
+ grad.append('stop').attr('offset', '100%').style('stop-color', VIS_COLOR_RAMP[2]);
2793
2797
  let barY = baseline + 6;
2794
2798
  let barW = width - 2 * PAD;
2795
2799
  g.append('rect')
@@ -2805,7 +2809,7 @@ function (root, d3, forester, phyloXml) {
2805
2809
  .text(legendNumberLabel(min));
2806
2810
  g.append('text')
2807
2811
  .attr('x', x + PAD + barW).attr('y', lblY)
2808
- .attr('text-anchor', 'end')
2812
+ .style('text-anchor', 'end')
2809
2813
  .style('font', rowFont).style('fill', ink)
2810
2814
  .text(legendNumberLabel(max));
2811
2815
  baseline = lblY;
@@ -2870,7 +2874,7 @@ function (root, d3, forester, phyloXml) {
2870
2874
  .text(r.text);
2871
2875
  g.append('text')
2872
2876
  .attr('x', x + width - PAD).attr('y', baseline)
2873
- .attr('text-anchor', 'end')
2877
+ .style('text-anchor', 'end')
2874
2878
  .style('font', rowFont)
2875
2879
  .style('fill', ink)
2876
2880
  .style('fill-opacity', 0.55)
@@ -3380,7 +3384,7 @@ function (root, d3, forester, phyloXml) {
3380
3384
  .style('paint-order', 'stroke')
3381
3385
  // anchor, dy and x all come from nodeLabelPlacement, which the
3382
3386
  // occupancy box is built from too
3383
- .attr('text-anchor', function (d) {
3387
+ .style('text-anchor', function (d) {
3384
3388
  return nodeLabelPlacement(d).anchor;
3385
3389
  })
3386
3390
  .attr('transform', function (d) {
@@ -3418,7 +3422,7 @@ function (root, d3, forester, phyloXml) {
3418
3422
  node.select('text.bllabel')
3419
3423
  .style('font-size', _state.branchDataFontSize + 'px')
3420
3424
  .style('fill', _state.branchColorDefault)
3421
- .attr('text-anchor', function () {
3425
+ .style('text-anchor', function () {
3422
3426
  return radialDisplay() ? 'middle' : null;
3423
3427
  })
3424
3428
  .attr('transform', function (d) {
@@ -3585,9 +3589,9 @@ function (root, d3, forester, phyloXml) {
3585
3589
  if (tipGuides || collapsedGuides.length > 0) {
3586
3590
  let ext = _svgGroup.insert('g', 'g').attr('class', 'aptx-align-ext');
3587
3591
  let guideStyle = function (sel) {
3588
- return sel.attr('fill', 'none')
3589
- .attr('stroke-width', 1)
3590
- .attr('stroke', _state.branchColorDefault)
3592
+ return sel.style('fill', 'none')
3593
+ .style('stroke-width', 1)
3594
+ .style('stroke', _state.branchColorDefault)
3591
3595
  .style('stroke-opacity', 0.25);
3592
3596
  };
3593
3597
  if (tipGuides) {
@@ -3632,10 +3636,10 @@ function (root, d3, forester, phyloXml) {
3632
3636
  conn.selectAll('line')
3633
3637
  .data(connected)
3634
3638
  .enter().append('line')
3635
- .attr('stroke', _state.branchColorDefault)
3639
+ .style('stroke', _state.branchColorDefault)
3636
3640
  // width/opacity match the rectangular aligned extensions; the
3637
3641
  // dash alone marks these as connectors (0.5px at 0.3 vanished)
3638
- .attr('stroke-width', 1)
3642
+ .style('stroke-width', 1)
3639
3643
  .style('stroke-opacity', 0.4)
3640
3644
  .style('stroke-dasharray', '2,3')
3641
3645
  .attr('x1', function (d) { return radialXY(d.x, innerY(d))[0]; })
@@ -4729,7 +4733,7 @@ function (root, d3, forester, phyloXml) {
4729
4733
  sync(g, 'text.extlabel', d._extLabelText !== '', function (sel) {
4730
4734
  sel.append('text')
4731
4735
  .attr('class', 'extlabel')
4732
- .attr('text-anchor', d.children ? 'end' : 'start')
4736
+ .style('text-anchor', d.children ? 'end' : 'start')
4733
4737
  .style('font-family', font)
4734
4738
  .style('fill-opacity', 0.5);
4735
4739
  });
@@ -4745,7 +4749,7 @@ function (root, d3, forester, phyloXml) {
4745
4749
  sync(g, 'text.conflabel', d._confText !== '', function (sel) {
4746
4750
  sel.append('text')
4747
4751
  .attr('class', 'conflabel')
4748
- .attr('text-anchor', 'middle')
4752
+ .style('text-anchor', 'middle')
4749
4753
  .style('font-family', font);
4750
4754
  });
4751
4755
  }
@@ -4753,7 +4757,7 @@ function (root, d3, forester, phyloXml) {
4753
4757
  sync(g, 'text.brancheventlabel', d._eventText !== '', function (sel) {
4754
4758
  sel.append('text')
4755
4759
  .attr('class', 'brancheventlabel')
4756
- .attr('text-anchor', 'middle')
4760
+ .style('text-anchor', 'middle')
4757
4761
  .style('font-family', font);
4758
4762
  });
4759
4763
  }
@@ -5393,9 +5397,16 @@ function (root, d3, forester, phyloXml) {
5393
5397
  let els = sizeLayer(_geomLayers.branches, 'path', branchList.length);
5394
5398
  branchList.forEach(function (b, k) {
5395
5399
  b.el = els[k];
5396
- b.el.setAttribute('fill', 'none');
5397
- b.el.setAttribute('stroke', b.color);
5398
- b.el.setAttribute('stroke-width', b.width);
5400
+ // These paths are kept from one drawing to the next, and a style
5401
+ // is not an attribute: a value that is no colour or no width (a
5402
+ // file's <width>-1</width>) is IGNORED, and the path would keep
5403
+ // what another batch gave it the last time. So the old is taken
5404
+ // off first, and such a batch is drawn as the svg has it.
5405
+ b.el.style.setProperty('fill', 'none');
5406
+ b.el.style.removeProperty('stroke');
5407
+ b.el.style.setProperty('stroke', b.color);
5408
+ b.el.style.removeProperty('stroke-width');
5409
+ b.el.style.setProperty('stroke-width', b.width);
5399
5410
  });
5400
5411
  // `last`: batches painted after the sorted ones, whatever their count
5401
5412
  let paintLayer = function (batches, layer, last) {
@@ -6892,8 +6903,15 @@ function (root, d3, forester, phyloXml) {
6892
6903
  createGui();
6893
6904
 
6894
6905
  _baseSvg = d3.select(containerEl).append('svg')
6895
- .attr('width', _displayWidth)
6896
- .attr('height', _displayHeight)
6906
+ .attr('class', TREE_SVG_CLASS)
6907
+ .call(sizeTreeSvg, _displayWidth, _displayHeight)
6908
+ // The tree's font, for the texts that state only a size (the
6909
+ // alignment strip's ruler and caption, the heat map strip's
6910
+ // numbers and caption): they were drawn in the PAGE's font,
6911
+ // in boxes measured for this one, and in an exported file in
6912
+ // whatever the program that opened it thought best. Inline,
6913
+ // so that the file has it too.
6914
+ .style('font-family', FONT_DEFAULTS)
6897
6915
  // an inline svg sits on a text baseline, and the room left
6898
6916
  // under it for descenders makes a container sized to the
6899
6917
  // svg a few pixels taller than the svg itself -- enough to
@@ -6916,8 +6934,7 @@ function (root, d3, forester, phyloXml) {
6916
6934
  }
6917
6935
  _displayWidth = size.w;
6918
6936
  _displayHeight = size.h;
6919
- _baseSvg.attr('width', size.w);
6920
- _baseSvg.attr('height', size.h);
6937
+ sizeTreeSvg(_baseSvg, size.w, size.h);
6921
6938
  rebuildOverview();
6922
6939
  if ((_settings.zoomToFitUponWindowResize === true) && (_zoomed_x_or_y === false) && (Math.abs(currentZoomScale() - 1.0) < 0.001)) {
6923
6940
  zoomToFit();
@@ -7989,7 +8006,7 @@ function (root, d3, forester, phyloXml) {
7989
8006
  let x1 = Math.round(originX + (endI * cw));
7990
8007
  g.append('rect').attr('x', x0).attr('y', cy)
7991
8008
  .attr('width', Math.max(1, x1 - x0)).attr('height', rh)
7992
- .attr('fill', 'rgb(' + runRgb.join(',') + ')');
8009
+ .style('fill', 'rgb(' + runRgb.join(',') + ')');
7993
8010
  runStart = -1;
7994
8011
  runRgb = null;
7995
8012
  };
@@ -8004,7 +8021,7 @@ function (root, d3, forester, phyloXml) {
8004
8021
  g.append('line')
8005
8022
  .attr('x1', Math.round(originX + (gapStart * cw))).attr('y1', mid)
8006
8023
  .attr('x2', Math.round(originX + (endI * cw))).attr('y2', mid)
8007
- .attr('stroke', ink).attr('stroke-opacity', 0.35).attr('stroke-width', 1);
8024
+ .style('stroke', ink).style('stroke-opacity', 0.35).style('stroke-width', 1);
8008
8025
  gapStart = -1;
8009
8026
  };
8010
8027
  for (let i = 0; i < visible; ++i) {
@@ -8028,9 +8045,9 @@ function (root, d3, forester, phyloXml) {
8028
8045
  let x1 = Math.round(originX + ((i + 1) * cw));
8029
8046
  g.append('rect').attr('x', x0).attr('y', cy)
8030
8047
  .attr('width', Math.max(1, x1 - x0)).attr('height', rh)
8031
- .attr('fill', 'rgb(' + rgb.join(',') + ')');
8048
+ .style('fill', 'rgb(' + rgb.join(',') + ')');
8032
8049
  g.append('text').attr('x', x0 + ((x1 - x0) / 2)).attr('y', cy + (rh / 2))
8033
- .attr('text-anchor', 'middle').attr('dy', '0.35em')
8050
+ .style('text-anchor', 'middle').attr('dy', '0.35em')
8034
8051
  .style('font-family', 'monospace').style('font-size', fontPx + 'px')
8035
8052
  .style('fill', forester.msaLetterInk(rgb))
8036
8053
  .text(ch.toUpperCase());
@@ -8054,13 +8071,13 @@ function (root, d3, forester, phyloXml) {
8054
8071
  if (offset <= 0) {
8055
8072
  g.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX))
8056
8073
  .attr('y1', rowsTop).attr('y2', rowsBottom)
8057
- .attr('stroke', ink).attr('stroke-opacity', 0.7).attr('stroke-width', 1);
8074
+ .style('stroke', ink).style('stroke-opacity', 0.7).style('stroke-width', 1);
8058
8075
  }
8059
8076
  if ((offset + visible) >= total) {
8060
8077
  let xe = Math.round(originX + (visible * cw));
8061
8078
  g.append('line').attr('x1', xe).attr('x2', xe)
8062
8079
  .attr('y1', rowsTop).attr('y2', rowsBottom)
8063
- .attr('stroke', ink).attr('stroke-opacity', 0.7).attr('stroke-width', 1);
8080
+ .style('stroke', ink).style('stroke-opacity', 0.7).style('stroke-width', 1);
8064
8081
  }
8065
8082
 
8066
8083
  // Scored over the visible WINDOW only -- the whole alignment would
@@ -8099,9 +8116,9 @@ function (root, d3, forester, phyloXml) {
8099
8116
  g.append('line')
8100
8117
  .attr('x1', from).attr('x2', guideEnd)
8101
8118
  .attr('y1', d.x).attr('y2', d.x)
8102
- .attr('stroke', ink)
8103
- .attr('stroke-width', 1)
8104
- .attr('stroke-dasharray', '2,3')
8119
+ .style('stroke', ink)
8120
+ .style('stroke-width', 1)
8121
+ .style('stroke-dasharray', '2,3')
8105
8122
  .style('stroke-opacity', 0.35)
8106
8123
  .style('pointer-events', 'none');
8107
8124
  }
@@ -8111,7 +8128,7 @@ function (root, d3, forester, phyloXml) {
8111
8128
  let strip = floatStripGroup('aptx-msa-strip', _clusterH, stripH);
8112
8129
  strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
8113
8130
  .attr('width', trackW + 4).attr('height', stripH)
8114
- .attr('fill', _state.backgroundColorDefault);
8131
+ .style('fill', _state.backgroundColorDefault);
8115
8132
  let consTop = _clusterH + MSA_CONS_TOP_GAP;
8116
8133
  // A caption on the panel's own background, so it stays readable over
8117
8134
  // whatever the band happens to draw underneath it.
@@ -8124,9 +8141,9 @@ function (root, d3, forester, phyloXml) {
8124
8141
  let xe = Math.round(originX) + trackW - 3;
8125
8142
  strip.append('rect').attr('x', xe - w).attr('y', y - 8)
8126
8143
  .attr('width', w).attr('height', 11)
8127
- .attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
8144
+ .style('fill', _state.backgroundColorDefault).style('fill-opacity', 0.85);
8128
8145
  strip.append('text').attr('x', xe - 3).attr('y', y)
8129
- .attr('text-anchor', 'end')
8146
+ .style('text-anchor', 'end')
8130
8147
  .style('font-size', '8px').style('fill', ink).style('fill-opacity', 0.9)
8131
8148
  .text(txt);
8132
8149
  };
@@ -8143,7 +8160,7 @@ function (root, d3, forester, phyloXml) {
8143
8160
  strip.append('line')
8144
8161
  .attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
8145
8162
  .attr('y1', floor).attr('y2', floor)
8146
- .attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 1);
8163
+ .style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 1);
8147
8164
  for (let i = 0; i < visible; ++i) {
8148
8165
  let col = logo.columns[i];
8149
8166
  if (!col || !(col.height > 0)) {
@@ -8165,7 +8182,7 @@ function (root, d3, forester, phyloXml) {
8165
8182
  .attr('transform', 'translate(' + (x0 + ((x1 - x0) / 2)) + ','
8166
8183
  + (y - (glyph.descent * sy)) + ')'
8167
8184
  + ' scale(' + (Math.max(1, x1 - x0) / glyph.width) + ',' + sy + ')')
8168
- .attr('text-anchor', 'middle')
8185
+ .style('text-anchor', 'middle')
8169
8186
  .style('font-family', 'monospace')
8170
8187
  .style('font-size', MSA_LOGO_FONT_PX + 'px')
8171
8188
  .style('fill', rgb === null ? ink : 'rgb(' + rgb.join(',') + ')')
@@ -8178,7 +8195,7 @@ function (root, d3, forester, phyloXml) {
8178
8195
  } else {
8179
8196
  strip.append('rect').attr('x', Math.round(originX)).attr('y', consTop)
8180
8197
  .attr('width', trackW).attr('height', MSA_CONS_BAR_H)
8181
- .attr('fill', ink).attr('fill-opacity', 0.08);
8198
+ .style('fill', ink).style('fill-opacity', 0.08);
8182
8199
  for (let i = 0; i < visible; ++i) {
8183
8200
  let score = cons.scores[i] || 0;
8184
8201
  if (score <= 0) {
@@ -8189,7 +8206,7 @@ function (root, d3, forester, phyloXml) {
8189
8206
  let x1 = Math.round(originX + ((i + 1) * cw));
8190
8207
  strip.append('rect').attr('x', x0).attr('y', (consTop + MSA_CONS_BAR_H) - bh)
8191
8208
  .attr('width', Math.max(1, x1 - x0)).attr('height', bh)
8192
- .attr('fill', ink).attr('fill-opacity', 0.7);
8209
+ .style('fill', ink).style('fill-opacity', 0.7);
8193
8210
  }
8194
8211
  caption('Consensus identity (n = ' + n + ')', consTop + 9);
8195
8212
  for (let i = 0; i < visible; ++i) {
@@ -8199,7 +8216,7 @@ function (root, d3, forester, phyloXml) {
8199
8216
  }
8200
8217
  strip.append('text').attr('x', Math.round(originX + (i * cw)) + (cw / 2))
8201
8218
  .attr('y', consTop + MSA_CONS_BAR_H + 10)
8202
- .attr('text-anchor', 'middle')
8219
+ .style('text-anchor', 'middle')
8203
8220
  .style('font-family', 'monospace').style('font-size', '8px')
8204
8221
  .style('fill', ink)
8205
8222
  .text(cc);
@@ -8211,7 +8228,7 @@ function (root, d3, forester, phyloXml) {
8211
8228
  let rulerY = consTop + (logo ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H);
8212
8229
  strip.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
8213
8230
  .attr('y1', rulerY).attr('y2', rulerY)
8214
- .attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
8231
+ .style('stroke', ink).style('stroke-opacity', 0.8).style('stroke-width', 1);
8215
8232
  let lastRight = -Infinity;
8216
8233
  let drawTick = function (c, force) {
8217
8234
  let i = c - 1 - offset;
@@ -8220,11 +8237,11 @@ function (root, d3, forester, phyloXml) {
8220
8237
  }
8221
8238
  let x = Math.round(originX + (i * cw)) + (cw / 2);
8222
8239
  strip.append('line').attr('x1', x).attr('x2', x).attr('y1', rulerY).attr('y2', rulerY + 4)
8223
- .attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
8240
+ .style('stroke', ink).style('stroke-opacity', 0.8).style('stroke-width', 1);
8224
8241
  let label = String(c);
8225
8242
  let half = label.length * 2.8;
8226
8243
  if (force || (x - half) >= (lastRight + 4)) {
8227
- strip.append('text').attr('x', x).attr('y', rulerY + 14).attr('text-anchor', 'middle')
8244
+ strip.append('text').attr('x', x).attr('y', rulerY + 14).style('text-anchor', 'middle')
8228
8245
  .style('font-size', '9px').style('fill', ink)
8229
8246
  .text(label);
8230
8247
  lastRight = x + half;
@@ -8241,7 +8258,7 @@ function (root, d3, forester, phyloXml) {
8241
8258
  g.append('rect').attr('class', 'aptx-msa-hover')
8242
8259
  .attr('x', Math.round(originX)).attr('y', rowsTop)
8243
8260
  .attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
8244
- .attr('fill', 'transparent')
8261
+ .style('fill', 'transparent')
8245
8262
  .on('mousemove', msaHoverMove)
8246
8263
  .on('mouseout', msaHoverOut)
8247
8264
  .on('wheel', function (event) {
@@ -8743,10 +8760,14 @@ function (root, d3, forester, phyloXml) {
8743
8760
  // that came and went with the zoom level. Both strokes are a constant
8744
8761
  // screen width, so a zoom neither fattens nor loses them.
8745
8762
  function paintHeatmapCell(el, fill, bordered) {
8746
- el.attr('fill', fill)
8747
- .attr('stroke', bordered ? heatmapBorderColor(fill) : fill)
8748
- .attr('stroke-width', bordered ? HEATMAP_BORDER_W : HEATMAP_SEAL_W)
8749
- .attr('vector-effect', 'non-scaling-stroke');
8763
+ el.style('fill', fill)
8764
+ .style('stroke', bordered ? heatmapBorderColor(fill) : fill)
8765
+ .style('stroke-width', bordered ? HEATMAP_BORDER_W : HEATMAP_SEAL_W)
8766
+ // ... said twice: as a style for the page, as everything here is,
8767
+ // and as an attribute for canvg, which draws the PNG and reads this
8768
+ // from the attribute alone (as a style only, every border came out
8769
+ // at the export's scale, four times as wide)
8770
+ .style('vector-effect', 'non-scaling-stroke').attr('vector-effect', 'non-scaling-stroke');
8750
8771
  return el;
8751
8772
  }
8752
8773
 
@@ -8876,8 +8897,8 @@ function (root, d3, forester, phyloXml) {
8876
8897
  let rect = g.append('rect').attr('x', x0).attr('y', cy)
8877
8898
  .attr('width', Math.max(1, x1 - x0)).attr('height', rh);
8878
8899
  if (runFill === HEATMAP_BLANK) {
8879
- rect.attr('fill', _state.backgroundColorDefault)
8880
- .attr('stroke', ink).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
8900
+ rect.style('fill', _state.backgroundColorDefault)
8901
+ .style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
8881
8902
  } else {
8882
8903
  paintHeatmapCell(rect, runFill, bordered);
8883
8904
  }
@@ -8909,8 +8930,8 @@ function (root, d3, forester, phyloXml) {
8909
8930
  g.append('rect')
8910
8931
  .attr('x', Math.round(originX)).attr('y', rowsTop)
8911
8932
  .attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
8912
- .attr('fill', 'none')
8913
- .attr('stroke', ink).attr('stroke-opacity', 0.55).attr('stroke-width', 1);
8933
+ .style('fill', 'none')
8934
+ .style('stroke', ink).style('stroke-opacity', 0.55).style('stroke-width', 1);
8914
8935
 
8915
8936
  // ---- the clustering behind the column order, above the grid ----
8916
8937
  // Drawn with the cells, in the tree's own group, so it zooms, pans and
@@ -8963,9 +8984,9 @@ function (root, d3, forester, phyloXml) {
8963
8984
  }
8964
8985
  g.append('path').attr('class', 'aptx-heatmap-dendro')
8965
8986
  .attr('d', path)
8966
- .attr('fill', 'none')
8967
- .attr('stroke', ink)
8968
- .attr('stroke-width', Math.max(1, _state.branchWidthDefault))
8987
+ .style('fill', 'none')
8988
+ .style('stroke', ink)
8989
+ .style('stroke-width', Math.max(1, _state.branchWidthDefault))
8969
8990
  .style('pointer-events', 'none');
8970
8991
  }
8971
8992
 
@@ -8994,9 +9015,9 @@ function (root, d3, forester, phyloXml) {
8994
9015
  g.append('line')
8995
9016
  .attr('x1', from).attr('x2', guideEnd)
8996
9017
  .attr('y1', d.x).attr('y2', d.x)
8997
- .attr('stroke', ink)
8998
- .attr('stroke-width', 1)
8999
- .attr('stroke-dasharray', '2,3')
9018
+ .style('stroke', ink)
9019
+ .style('stroke-width', 1)
9020
+ .style('stroke-dasharray', '2,3')
9000
9021
  .style('stroke-opacity', 0.35)
9001
9022
  .style('pointer-events', 'none');
9002
9023
  }
@@ -9008,7 +9029,7 @@ function (root, d3, forester, phyloXml) {
9008
9029
  let strip = floatStripGroup('aptx-heatmap-strip', _clusterH, heatmapBottomReserve());
9009
9030
  strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
9010
9031
  .attr('width', trackW + 4).attr('height', heatmapStripHeight())
9011
- .attr('fill', _state.backgroundColorDefault);
9032
+ .style('fill', _state.backgroundColorDefault);
9012
9033
  let labelTop = _clusterH + HEATMAP_HEAD_GAP;
9013
9034
  let headH = heatmapHeadHeight();
9014
9035
  if (headH > 0) {
@@ -9019,7 +9040,7 @@ function (root, d3, forester, phyloXml) {
9019
9040
  .attr('transform', 'translate(' + (originX + (i * cw) + (cw / 2)) + ','
9020
9041
  + labelTop + ') rotate(-90)')
9021
9042
  .attr('dy', '0.32em')
9022
- .attr('text-anchor', 'end')
9043
+ .style('text-anchor', 'end')
9023
9044
  .style('font', font)
9024
9045
  .style('fill', _state.labelColorDefault)
9025
9046
  .text(clipTextToWidth(col.label, font, HEATMAP_LABEL_MAX_PX));
@@ -9036,20 +9057,20 @@ function (root, d3, forester, phyloXml) {
9036
9057
  VIS_COLOR_RAMP.forEach(function (c, i) {
9037
9058
  grad.append('stop')
9038
9059
  .attr('offset', ((i * 100) / (VIS_COLOR_RAMP.length - 1)) + '%')
9039
- .attr('stop-color', c);
9060
+ .style('stop-color', c);
9040
9061
  });
9041
9062
  strip.append('rect')
9042
9063
  .attr('x', Math.round(originX)).attr('y', scaleTop)
9043
9064
  .attr('width', barW).attr('height', HEATMAP_SCALE_BAR_H)
9044
- .attr('fill', 'url(#' + gradId + ')')
9045
- .attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 1);
9065
+ .style('fill', 'url(#' + gradId + ')')
9066
+ .style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 1);
9046
9067
  let numY = scaleTop + HEATMAP_SCALE_BAR_H + 10;
9047
9068
  strip.append('text').attr('x', Math.round(originX)).attr('y', numY)
9048
- .attr('text-anchor', 'start')
9069
+ .style('text-anchor', 'start')
9049
9070
  .style('font-size', '9px').style('fill', ink)
9050
9071
  .text(heatmapNum(model.min));
9051
9072
  strip.append('text').attr('x', Math.round(originX) + barW).attr('y', numY)
9052
- .attr('text-anchor', 'end')
9073
+ .style('text-anchor', 'end')
9053
9074
  .style('font-size', '9px').style('fill', ink)
9054
9075
  .text(heatmapNum(model.max));
9055
9076
  // the caption: what the matrix is showing, and -- when it is windowed
@@ -9067,11 +9088,11 @@ function (root, d3, forester, phyloXml) {
9067
9088
  if ((rightEdge - cursor) >= keyW) {
9068
9089
  strip.append('rect').attr('x', cursor).attr('y', scaleTop)
9069
9090
  .attr('width', HEATMAP_SCALE_BAR_H).attr('height', HEATMAP_SCALE_BAR_H)
9070
- .attr('fill', _state.backgroundColorDefault)
9071
- .attr('stroke', ink).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
9091
+ .style('fill', _state.backgroundColorDefault)
9092
+ .style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
9072
9093
  strip.append('text').attr('x', cursor + HEATMAP_SCALE_BAR_H + 4)
9073
9094
  .attr('y', scaleTop + HEATMAP_SCALE_BAR_H)
9074
- .attr('text-anchor', 'start')
9095
+ .style('text-anchor', 'start')
9075
9096
  .style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
9076
9097
  .text(HEATMAP_NOT_ASSESSED);
9077
9098
  cursor += keyW + 10;
@@ -9079,7 +9100,7 @@ function (root, d3, forester, phyloXml) {
9079
9100
  }
9080
9101
  if ((rightEdge - cursor) > 60) {
9081
9102
  strip.append('text').attr('x', cursor).attr('y', scaleTop + HEATMAP_SCALE_BAR_H)
9082
- .attr('text-anchor', 'start')
9103
+ .style('text-anchor', 'start')
9083
9104
  .style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
9084
9105
  .text(clipTextToWidth(caption, capFont, rightEdge - cursor));
9085
9106
  }
@@ -9088,7 +9109,7 @@ function (root, d3, forester, phyloXml) {
9088
9109
  g.append('rect').attr('class', 'aptx-heatmap-hover')
9089
9110
  .attr('x', Math.round(originX)).attr('y', rowsTop)
9090
9111
  .attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
9091
- .attr('fill', 'transparent')
9112
+ .style('fill', 'transparent')
9092
9113
  .on('mousemove', heatmapHoverMove)
9093
9114
  .on('mouseout', heatmapHoverOut)
9094
9115
  .on('wheel', function (event) {
@@ -9230,12 +9251,12 @@ function (root, d3, forester, phyloXml) {
9230
9251
  let path = g.append('path')
9231
9252
  .attr('d', heatmapSector(edge[runStart], a1, r0, r1 + bleed));
9232
9253
  if (runFill === HEATMAP_BLANK) {
9233
- path.attr('fill', _state.backgroundColorDefault)
9234
- .attr('stroke', ink).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
9254
+ path.style('fill', _state.backgroundColorDefault)
9255
+ .style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
9235
9256
  } else if (bordered) {
9236
9257
  paintHeatmapCell(path, runFill, true);
9237
9258
  } else {
9238
- path.attr('fill', runFill); // the bleed already closes the seams here
9259
+ path.style('fill', runFill); // the bleed already closes the seams here
9239
9260
  }
9240
9261
  runStart = -1;
9241
9262
  runFill = null;
@@ -9261,7 +9282,7 @@ function (root, d3, forester, phyloXml) {
9261
9282
  [start, start + (columns.length * ringW)].forEach(function (r) {
9262
9283
  g.append('path')
9263
9284
  .attr('d', heatmapSector(edge[0], edge[n], r - 0.5, r + 0.5))
9264
- .attr('fill', ink).attr('fill-opacity', 0.5);
9285
+ .style('fill', ink).style('fill-opacity', 0.5);
9265
9286
  });
9266
9287
 
9267
9288
  // Each ring's name, laid along the ring at the fan's seam -- the gap
@@ -9285,7 +9306,7 @@ function (root, d3, forester, phyloXml) {
9285
9306
  g.append('text')
9286
9307
  .attr('transform', 'translate(' + xy[0].toFixed(1) + ',' + xy[1].toFixed(1) + ') '
9287
9308
  + 'rotate(' + (deg + (flip ? 180 : 0)).toFixed(1) + ')')
9288
- .attr('text-anchor', 'middle')
9309
+ .style('text-anchor', 'middle')
9289
9310
  .attr('dy', '0.32em')
9290
9311
  .style('font', font)
9291
9312
  .style('fill', _state.labelColorDefault)
@@ -9297,7 +9318,7 @@ function (root, d3, forester, phyloXml) {
9297
9318
  g.append('path')
9298
9319
  .attr('class', 'aptx-heatmap-hover')
9299
9320
  .attr('d', heatmapSector(edge[0], edge[n], start, start + (columns.length * ringW)))
9300
- .attr('fill', 'transparent')
9321
+ .style('fill', 'transparent')
9301
9322
  .on('mousemove', heatmapHoverMove)
9302
9323
  .on('mouseout', heatmapHoverOut);
9303
9324
 
@@ -9351,31 +9372,31 @@ function (root, d3, forester, phyloXml) {
9351
9372
  VIS_COLOR_RAMP.forEach(function (c, i) {
9352
9373
  grad.append('stop')
9353
9374
  .attr('offset', ((i * 100) / (VIS_COLOR_RAMP.length - 1)) + '%')
9354
- .attr('stop-color', c);
9375
+ .style('stop-color', c);
9355
9376
  });
9356
9377
  let barY = y + PAD;
9357
9378
  g.append('rect').attr('x', x + PAD).attr('y', barY)
9358
9379
  .attr('width', BAR_W).attr('height', HEATMAP_SCALE_BAR_H)
9359
- .attr('fill', 'url(#' + gradId + ')')
9360
- .attr('stroke', frame).attr('stroke-opacity', 0.5).attr('stroke-width', 1);
9380
+ .style('fill', 'url(#' + gradId + ')')
9381
+ .style('stroke', frame).style('stroke-opacity', 0.5).style('stroke-width', 1);
9361
9382
  let line = barY + HEATMAP_SCALE_BAR_H + FS + 2;
9362
- g.append('text').attr('x', x + PAD).attr('y', line).attr('text-anchor', 'start')
9383
+ g.append('text').attr('x', x + PAD).attr('y', line).style('text-anchor', 'start')
9363
9384
  .style('font', capFont).style('fill', ink).text(heatmapNum(m.min));
9364
- g.append('text').attr('x', x + PAD + BAR_W).attr('y', line).attr('text-anchor', 'end')
9385
+ g.append('text').attr('x', x + PAD + BAR_W).attr('y', line).style('text-anchor', 'end')
9365
9386
  .style('font', capFont).style('fill', ink).text(heatmapNum(m.max));
9366
9387
  if (blanks) {
9367
9388
  line += FS + 4;
9368
9389
  g.append('rect').attr('x', x + PAD).attr('y', line - HEATMAP_SCALE_BAR_H)
9369
9390
  .attr('width', HEATMAP_SCALE_BAR_H).attr('height', HEATMAP_SCALE_BAR_H)
9370
- .attr('fill', _state.backgroundColorDefault)
9371
- .attr('stroke', frame).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
9391
+ .style('fill', _state.backgroundColorDefault)
9392
+ .style('stroke', frame).style('stroke-opacity', 0.45).style('stroke-width', 1);
9372
9393
  g.append('text').attr('x', x + PAD + HEATMAP_SCALE_BAR_H + 4).attr('y', line)
9373
- .attr('text-anchor', 'start')
9394
+ .style('text-anchor', 'start')
9374
9395
  .style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
9375
9396
  .text(HEATMAP_NOT_ASSESSED);
9376
9397
  }
9377
9398
  line += FS + 4;
9378
- g.append('text').attr('x', x + PAD).attr('y', line).attr('text-anchor', 'start')
9399
+ g.append('text').attr('x', x + PAD).attr('y', line).style('text-anchor', 'start')
9379
9400
  .style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
9380
9401
  .text(caption);
9381
9402
  }
@@ -9970,7 +9991,7 @@ function (root, d3, forester, phyloXml) {
9970
9991
  let gap = _state.nodeLabelGap;
9971
9992
  let flip = radialDisplay() && labelFlip(d);
9972
9993
  if (_state.circularDisplay && _radial) {
9973
- t.attr('text-anchor', flip ? 'end' : 'start')
9994
+ t.style('text-anchor', flip ? 'end' : 'start')
9974
9995
  .attr('x', flip ? -gap : gap).attr('dy', '0.32em');
9975
9996
  if (_radialLabelsHorizontal) {
9976
9997
  let p = radialXY(d.x, d.y);
@@ -9983,12 +10004,12 @@ function (root, d3, forester, phyloXml) {
9983
10004
  } else if (radialDisplay()) {
9984
10005
  let r = radialRadius(far) - radialRadius(d.y) + gap;
9985
10006
  t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + r + ',0)' + (flip ? ' rotate(180)' : ''))
9986
- .attr('text-anchor', flip ? 'end' : 'start')
10007
+ .style('text-anchor', flip ? 'end' : 'start')
9987
10008
  .attr('x', 0).attr('dy', '0.32em');
9988
10009
  } else {
9989
10010
  let column = (_state.phylogram && _state.alignPhylogram) ? _w : far;
9990
10011
  t.attr('transform', null)
9991
- .attr('text-anchor', 'start')
10012
+ .style('text-anchor', 'start')
9992
10013
  .attr('x', column - d.y + gap)
9993
10014
  .attr('dy', (0.3 * fs) + 'px');
9994
10015
  }
@@ -11255,6 +11276,9 @@ function (root, d3, forester, phyloXml) {
11255
11276
  let svg = clockSvg(body, 'svg', 'aptx-clock-svg');
11256
11277
  svg.setAttribute('width', CLOCK_PLOT_W);
11257
11278
  svg.setAttribute('height', CLOCK_PLOT_H);
11279
+ // inline as well: an attribute gives way to a page's rule for a bare "svg" (sizeTreeSvg)
11280
+ svg.style.width = CLOCK_PLOT_W + 'px';
11281
+ svg.style.height = CLOCK_PLOT_H + 'px';
11258
11282
  svg.setAttribute('viewBox', '0 0 ' + CLOCK_PLOT_W + ' ' + CLOCK_PLOT_H);
11259
11283
  svg.setAttribute('role', 'img');
11260
11284
  svg.setAttribute('aria-label', 'Clock plot: each tip\'s date against its divergence from the root');
@@ -11581,7 +11605,7 @@ function (root, d3, forester, phyloXml) {
11581
11605
  let t = clockSvg(cp.axes, 'text', cls);
11582
11606
  t.setAttribute('x', tx);
11583
11607
  t.setAttribute('y', ty);
11584
- t.setAttribute('text-anchor', anchor);
11608
+ t.style.setProperty('text-anchor', anchor);
11585
11609
  t.textContent = s;
11586
11610
  return t;
11587
11611
  };
@@ -12374,8 +12398,8 @@ function (root, d3, forester, phyloXml) {
12374
12398
  made[id] = true;
12375
12399
  let grad = d3.select(defs).append('linearGradient').attr('id', id)
12376
12400
  .attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
12377
- grad.append('stop').attr('offset', '0%').attr('stop-color', forester.domainLighten(base, 0.12));
12378
- grad.append('stop').attr('offset', '100%').attr('stop-color', forester.domainDarken(base, 0.10));
12401
+ grad.append('stop').attr('offset', '0%').style('stop-color', forester.domainLighten(base, 0.12));
12402
+ grad.append('stop').attr('offset', '100%').style('stop-color', forester.domainDarken(base, 0.10));
12379
12403
  }
12380
12404
  return 'url(#' + id + ')';
12381
12405
  }
@@ -12556,7 +12580,7 @@ function (root, d3, forester, phyloXml) {
12556
12580
  el.setAttribute('x', t.x);
12557
12581
  el.setAttribute('y', t.y);
12558
12582
  el.setAttribute('dy', '0.35em');
12559
- el.setAttribute('text-anchor', 'middle');
12583
+ el.style.setProperty('text-anchor', 'middle');
12560
12584
  el.style.font = t.font;
12561
12585
  el.style.fill = t.fill;
12562
12586
  el.textContent = t.text;
@@ -13269,7 +13293,7 @@ function (root, d3, forester, phyloXml) {
13269
13293
  el.setAttribute('x', t.x);
13270
13294
  el.setAttribute('y', t.y);
13271
13295
  el.setAttribute('dy', '0.35em');
13272
- el.setAttribute('text-anchor', 'middle');
13296
+ el.style.setProperty('text-anchor', 'middle');
13273
13297
  el.style.font = t.font;
13274
13298
  el.style.fill = t.fill;
13275
13299
  el.textContent = t.text;
@@ -13684,7 +13708,7 @@ function (root, d3, forester, phyloXml) {
13684
13708
  });
13685
13709
  g.append('text')
13686
13710
  .attr('x', x + (bar.px / 2)).attr('y', y - 6)
13687
- .attr('text-anchor', 'middle')
13711
+ .style('text-anchor', 'middle')
13688
13712
  .style('font', fs + 'px ' + FONT_DEFAULTS)
13689
13713
  .style('fill', _state.labelColorDefault)
13690
13714
  .text(bar.label);
@@ -13763,8 +13787,8 @@ function (root, d3, forester, phyloXml) {
13763
13787
  function gridLineAt(x) {
13764
13788
  grid.append('line').attr('x1', x).attr('x2', x)
13765
13789
  .attr('y1', 0).attr('y2', axisTop)
13766
- .attr('stroke', ink).attr('stroke-opacity', 0.18)
13767
- .attr('stroke-width', 1);
13790
+ .style('stroke', ink).style('stroke-opacity', 0.18)
13791
+ .style('stroke-width', 1);
13768
13792
  }
13769
13793
 
13770
13794
  let sc = info.type === 'calendar' ? -corr : corr;
@@ -13784,11 +13808,11 @@ function (root, d3, forester, phyloXml) {
13784
13808
  let p1 = polarXY(a, radialRadius(x1));
13785
13809
  el = g.append('line').attr('x1', p0[0]).attr('y1', p0[1])
13786
13810
  .attr('x2', p1[0]).attr('y2', p1[1])
13787
- .attr('stroke', fill).attr('stroke-width', thick);
13811
+ .style('stroke', fill).style('stroke-width', thick);
13788
13812
  } else {
13789
13813
  el = g.append('rect').attr('x', Math.min(x0, x1)).attr('y', d.x - (thick / 2))
13790
13814
  .attr('width', Math.max(1, Math.abs(x1 - x0))).attr('height', thick)
13791
- .attr('fill', fill);
13815
+ .style('fill', fill);
13792
13816
  }
13793
13817
  el.datum(d).attr('class', cls);
13794
13818
  }
@@ -13807,7 +13831,7 @@ function (root, d3, forester, phyloXml) {
13807
13831
  q = [x, d.x + 4];
13808
13832
  }
13809
13833
  g.append('line').attr('x1', p[0]).attr('y1', p[1]).attr('x2', q[0]).attr('y2', q[1])
13810
- .attr('stroke', FOSSIL_BAR_COLOR).attr('stroke-width', 1)
13834
+ .style('stroke', FOSSIL_BAR_COLOR).style('stroke-width', 1)
13811
13835
  .datum(d).attr('class', 'aptx-fossil-cap');
13812
13836
  }
13813
13837
 
@@ -13879,7 +13903,7 @@ function (root, d3, forester, phyloXml) {
13879
13903
  let left = Math.min(x0, x1);
13880
13904
  ax.append('rect').attr('x', left - 2).attr('y', axisTop - 4)
13881
13905
  .attr('width', Math.abs(x1 - x0) + 4).attr('height', scaleAxisBottomReserve() + 4)
13882
- .attr('fill', _state.backgroundColorDefault);
13906
+ .style('fill', _state.backgroundColorDefault);
13883
13907
  }
13884
13908
  if (info.type === 'geologic') {
13885
13909
  let rootAge = info.rootAge;
@@ -13920,11 +13944,11 @@ function (root, d3, forester, phyloXml) {
13920
13944
  }
13921
13945
  ax.append('rect').attr('x', left).attr('y', rowY)
13922
13946
  .attr('width', w).attr('height', TIME_BAND_ROW_H)
13923
- .attr('fill', iv.color)
13924
- .attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 0.5);
13947
+ .style('fill', iv.color)
13948
+ .style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 0.5);
13925
13949
  if ((iv.name.length * 5.5) + 4 <= w) {
13926
13950
  ax.append('text').attr('x', left + (w / 2)).attr('y', rowY + TIME_BAND_ROW_H - 3.5)
13927
- .attr('text-anchor', 'middle')
13951
+ .style('text-anchor', 'middle')
13928
13952
  .style('font-size', '9px')
13929
13953
  .style('fill', forester.msaLetterInk(hexToRgbTriple(iv.color)))
13930
13954
  .text(iv.name);
@@ -13935,7 +13959,7 @@ function (root, d3, forester, phyloXml) {
13935
13959
  let rulerY = axisTop + (2 * TIME_BAND_ROW_H) + 4;
13936
13960
  ax.append('line').attr('x1', xOfAge(rootAge)).attr('x2', xOfAge(youngBound))
13937
13961
  .attr('y1', rulerY).attr('y2', rulerY)
13938
- .attr('stroke', ink).attr('stroke-width', 1);
13962
+ .style('stroke', ink).style('stroke-width', 1);
13939
13963
  let labels = [];
13940
13964
  forester.maAxisTickValues(rootAge).forEach(function (v) {
13941
13965
  if (v >= youngBound - 1e-9 && v <= rootAge + 1e-9) {
@@ -13955,7 +13979,7 @@ function (root, d3, forester, phyloXml) {
13955
13979
  let x = xOfAge(l.age);
13956
13980
  ax.append('line').attr('x1', x).attr('x2', x)
13957
13981
  .attr('y1', rulerY).attr('y2', rulerY + 4)
13958
- .attr('stroke', ink).attr('stroke-width', 1);
13982
+ .style('stroke', ink).style('stroke-width', 1);
13959
13983
  let text = String(l.age);
13960
13984
  let half = text.length * 2.8;
13961
13985
  let ok = true;
@@ -13967,14 +13991,14 @@ function (root, d3, forester, phyloXml) {
13967
13991
  }
13968
13992
  if (ok || l.priority > 0) {
13969
13993
  ax.append('text').attr('x', x).attr('y', rulerY + 14)
13970
- .attr('text-anchor', 'middle')
13994
+ .style('text-anchor', 'middle')
13971
13995
  .style('font-size', '9px').style('fill', ink)
13972
13996
  .text(text);
13973
13997
  placed.push(x);
13974
13998
  }
13975
13999
  });
13976
14000
  ax.append('text').attr('x', xOfAge(youngBound) + 8).attr('y', rulerY + 14)
13977
- .attr('text-anchor', 'start')
14001
+ .style('text-anchor', 'start')
13978
14002
  .style('font-size', '9px').style('fill', ink)
13979
14003
  .text('Ma');
13980
14004
  } else {
@@ -14003,18 +14027,18 @@ function (root, d3, forester, phyloXml) {
14003
14027
  let rulerY = axisTop + 4;
14004
14028
  ax.append('line').attr('x1', xOfYear(rootYear)).attr('x2', xOfYear(present))
14005
14029
  .attr('y1', rulerY).attr('y2', rulerY)
14006
- .attr('stroke', ink).attr('stroke-width', 1);
14030
+ .style('stroke', ink).style('stroke-width', 1);
14007
14031
  let lastRight = -Infinity;
14008
14032
  forester.calendarTickYears(rootYear, present).forEach(function (yv) {
14009
14033
  let x = xOfYear(yv);
14010
14034
  ax.append('line').attr('x1', x).attr('x2', x)
14011
14035
  .attr('y1', rulerY).attr('y2', rulerY + 4)
14012
- .attr('stroke', ink).attr('stroke-width', 1);
14036
+ .style('stroke', ink).style('stroke-width', 1);
14013
14037
  let text = String(Math.round(yv));
14014
14038
  let half = text.length * 2.8;
14015
14039
  if ((x - half) >= lastRight + 4) {
14016
14040
  ax.append('text').attr('x', x).attr('y', rulerY + 14)
14017
- .attr('text-anchor', 'middle')
14041
+ .style('text-anchor', 'middle')
14018
14042
  .style('font-size', '9px').style('fill', ink)
14019
14043
  .text(text);
14020
14044
  lastRight = x + half;
@@ -14108,15 +14132,15 @@ function (root, d3, forester, phyloXml) {
14108
14132
  return;
14109
14133
  }
14110
14134
  behind.append('path').attr('d', ring.innerRadius(a).outerRadius(b)())
14111
- .attr('fill', iv.color).attr('fill-opacity', 0.35);
14135
+ .style('fill', iv.color).style('fill-opacity', 0.35);
14112
14136
  named.push({r: (a + b) / 2, span: b - a, text: iv.name, color: iv.color});
14113
14137
  });
14114
14138
  if (grid) {
14115
14139
  ticks.forEach(function (t) {
14116
14140
  let r = radiusOf(t.value);
14117
14141
  if (r > r0 + 0.5 && r < r1 - 0.5) {
14118
- grid.append('circle').attr('r', r).attr('fill', 'none')
14119
- .attr('stroke', ink).attr('stroke-opacity', 0.18).attr('stroke-width', 1);
14142
+ grid.append('circle').attr('r', r).style('fill', 'none')
14143
+ .style('stroke', ink).style('stroke-opacity', 0.18).style('stroke-width', 1);
14120
14144
  }
14121
14145
  });
14122
14146
  }
@@ -14135,7 +14159,7 @@ function (root, d3, forester, phyloXml) {
14135
14159
  let e0 = at(r0, 0);
14136
14160
  let e1 = at(r1, 0);
14137
14161
  ax.append('line').attr('x1', e0[0]).attr('y1', e0[1]).attr('x2', e1[0]).attr('y2', e1[1])
14138
- .attr('stroke', ink).attr('stroke-width', 1);
14162
+ .style('stroke', ink).style('stroke-width', 1);
14139
14163
  // numbers on one side of the ruler, interval names on the other; the
14140
14164
  // headline value first, then the round ticks outward, none overlapping
14141
14165
  function label(r, off, text, fill, backdrop) {
@@ -14145,9 +14169,9 @@ function (root, d3, forester, phyloXml) {
14145
14169
  let w = text.length * 5.5;
14146
14170
  if (backdrop) {
14147
14171
  t.append('rect').attr('x', -(w / 2) - 2).attr('y', -6).attr('width', w + 4).attr('height', 12)
14148
- .attr('rx', 2).attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
14172
+ .attr('rx', 2).style('fill', _state.backgroundColorDefault).style('fill-opacity', 0.85);
14149
14173
  }
14150
- t.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'central')
14174
+ t.append('text').style('text-anchor', 'middle').style('dominant-baseline', 'central')
14151
14175
  .style('font-size', '9px').style('fill', fill).text(text);
14152
14176
  }
14153
14177
  let side = flip ? -1 : 1;
@@ -14160,7 +14184,7 @@ function (root, d3, forester, phyloXml) {
14160
14184
  let c0 = at(r, -4);
14161
14185
  let c1 = at(r, 4);
14162
14186
  ax.append('line').attr('x1', c0[0]).attr('y1', c0[1]).attr('x2', c1[0]).attr('y2', c1[1])
14163
- .attr('stroke', ink).attr('stroke-width', 1);
14187
+ .style('stroke', ink).style('stroke-width', 1);
14164
14188
  let half = t.text.length * 2.8;
14165
14189
  for (let i = 0; i < placed.length; ++i) {
14166
14190
  if (Math.abs(placed[i] - r) < (half * 2) + 6) {
@@ -14234,8 +14258,8 @@ function (root, d3, forester, phyloXml) {
14234
14258
  if (v > 0 && v < maxDist - 1e-9) {
14235
14259
  grid.append('line').attr('x1', xOf(v)).attr('x2', xOf(v))
14236
14260
  .attr('y1', 0).attr('y2', axisTop)
14237
- .attr('stroke', ink).attr('stroke-opacity', 0.18)
14238
- .attr('stroke-width', 1);
14261
+ .style('stroke', ink).style('stroke-opacity', 0.18)
14262
+ .style('stroke-width', 1);
14239
14263
  }
14240
14264
  });
14241
14265
  }
@@ -14247,23 +14271,23 @@ function (root, d3, forester, phyloXml) {
14247
14271
  // opaque, as the time axis's, so tips panned under it do not show
14248
14272
  ax.append('rect').attr('x', x0 - 2).attr('y', axisTop - 4)
14249
14273
  .attr('width', (x1 - x0) + 4).attr('height', reserve + 4)
14250
- .attr('fill', _state.backgroundColorDefault);
14274
+ .style('fill', _state.backgroundColorDefault);
14251
14275
  let rulerY = axisTop + 4;
14252
14276
  ax.append('line').attr('x1', x0).attr('x2', x1)
14253
14277
  .attr('y1', rulerY).attr('y2', rulerY)
14254
- .attr('stroke', ink).attr('stroke-width', 1);
14278
+ .style('stroke', ink).style('stroke-width', 1);
14255
14279
  // every tick drawn, its number only where it clears the last one
14256
14280
  let lastRight = -Infinity;
14257
14281
  ticks.forEach(function (v) {
14258
14282
  let x = xOf(v);
14259
14283
  ax.append('line').attr('x1', x).attr('x2', x)
14260
14284
  .attr('y1', rulerY).attr('y2', rulerY + 4)
14261
- .attr('stroke', ink).attr('stroke-width', 1);
14285
+ .style('stroke', ink).style('stroke-width', 1);
14262
14286
  let text = String(v);
14263
14287
  let half = text.length * 2.8;
14264
14288
  if ((x - half) >= lastRight + 4) {
14265
14289
  ax.append('text').attr('x', x).attr('y', rulerY + 14)
14266
- .attr('text-anchor', 'middle')
14290
+ .style('text-anchor', 'middle')
14267
14291
  .style('font-size', '9px').style('fill', ink)
14268
14292
  .text(text);
14269
14293
  lastRight = x + half;
@@ -14273,7 +14297,7 @@ function (root, d3, forester, phyloXml) {
14273
14297
  // past the ruler's end, and past the last number, which is
14274
14298
  // centred on that end
14275
14299
  ax.append('text').attr('x', Math.max(x1 + 8, lastRight + 6)).attr('y', rulerY + 14)
14276
- .attr('text-anchor', 'start')
14300
+ .style('text-anchor', 'start')
14277
14301
  .style('font-size', '9px').style('fill', ink)
14278
14302
  .text(unit);
14279
14303
  }
@@ -14855,8 +14879,7 @@ function (root, d3, forester, phyloXml) {
14855
14879
  _displayHeight = size.h;
14856
14880
  // the canvas has to follow, or the layout is computed for one
14857
14881
  // size and drawn on another
14858
- _baseSvg.attr('width', size.w);
14859
- _baseSvg.attr('height', size.h);
14882
+ sizeTreeSvg(_baseSvg, size.w, size.h);
14860
14883
  }
14861
14884
  }
14862
14885
  // Where the user dragged the legend to is their choice; a resize is no
@@ -16745,9 +16768,11 @@ function (root, d3, forester, phyloXml) {
16745
16768
  case 'moon': body = glyphMoon(); break;
16746
16769
  default: throw new Error('unknown control-panel glyph: ' + kind);
16747
16770
  }
16748
- return '<svg class="aptx-glyph" viewBox="0 0 ' + w + ' 100" aria-hidden="true" focusable="false"'
16771
+ // written as an svg file would be, its paint in attributes; paintInline
16772
+ // makes inline styles of it, which no rule of a page outranks
16773
+ return paintInline('<svg class="aptx-glyph" viewBox="0 0 ' + w + ' 100" aria-hidden="true" focusable="false"'
16749
16774
  + ' fill="none" stroke="currentColor" stroke-width="' + sw + '"'
16750
- + ' stroke-linecap="' + cap + '" stroke-linejoin="' + join + '">' + body + '</svg>';
16775
+ + ' stroke-linecap="' + cap + '" stroke-linejoin="' + join + '">' + body + '</svg>');
16751
16776
  }
16752
16777
 
16753
16778
  // The desktop's UNCOLLAPSE_ALL glyph (ControlButtonIcon.paintUncollapse),
@@ -16764,6 +16789,214 @@ function (root, d3, forester, phyloXml) {
16764
16789
  + '<path d="M56,20 H94 M56,50 H94 M56,80 H94"/>';
16765
16790
  }
16766
16791
 
16792
+ // ===================== A host page's stylesheet =====================
16793
+ // The viewer is drawn inside somebody else's page, under that page's
16794
+ // stylesheet, and a rule of the host's wins wherever this sheet says
16795
+ // nothing. BV-BRC, 2026-10-09: ".Phylogeny svg { background:#FFFFFF }",
16796
+ // written when the only svg in that widget was the tree, put a white box
16797
+ // behind every icon of the panel. Measured after that, on pages nobody
16798
+ // had tried: a page set in capitals, or centred, set the dialogs and the
16799
+ // tooltip that way too; and under "* { box-sizing: border-box }", which
16800
+ // most pages have, the tooltip came out 22 px narrower.
16801
+ //
16802
+ // So the sheet opens by saying what it used to leave unsaid. These rules
16803
+ // come FIRST, and the general ones carry as little weight as will do:
16804
+ // every rule after them still has the last word. What they cannot do is
16805
+ // outrank a rule of the host's that is heavier still: one scoped by an
16806
+ // id, or by more classes than the rule here has, or marked !important.
16807
+ //
16808
+ // test/browser/host_css.html launches the viewer under real host sheets
16809
+ // and fails on any difference from a clean page. Something new that the
16810
+ // viewer hangs on the page goes into HOST_PROOF_ROOTS, and through it.
16811
+ const HOST_PROOF_ROOTS = ['.aptx-panel', '.aptx-dialog', '.aptx-node-menu', '.aptx-suggest', '.aptx-tip',
16812
+ '.aptx-busy', '.aptx-msa-nav'];
16813
+ // What paints a shape or sets a text, what shows or hides it, and what
16814
+ // would animate it: the properties a page's rule for a bare "path", "rect"
16815
+ // or "text" is likely to set. An svg ATTRIBUTE for one of these gives way
16816
+ // to any such rule, so the viewer states them as inline styles, and its
16817
+ // sheet says, for every shape of its svgs, "as a page that says nothing
16818
+ // has it" -- which for most of them is what the shape's parent has, and
16819
+ // so, in the end, what the svg itself has: the value beside each name,
16820
+ // where a parent hands it down. (Without one: not handed down; or stated
16821
+ // by the svg's own rule; or left to the page on purpose, as the cursor
16822
+ // and whether the thing is shown at all.)
16823
+ //
16824
+ // It also means that an ATTRIBUTE for one of these, on a shape of the
16825
+ // viewer's, no longer does anything, in any page: test/
16826
+ // visualization_test.js holds the drawing code to that, and test/browser/
16827
+ // host_css.html the page (with these rules taken out, nothing changes).
16828
+ // Markup with its paint in attributes goes through paintInline.
16829
+ //
16830
+ // Not here, and still attributes: where a shape is and how large (x, y,
16831
+ // d, r, transform), clip-path and mask.
16832
+ const SHAPE_PAINT = {
16833
+ 'fill': '#000', 'fill-opacity': '1', 'fill-rule': 'nonzero',
16834
+ 'stroke': 'none', 'stroke-width': '1', 'stroke-opacity': '1', 'stroke-dasharray': 'none',
16835
+ 'stroke-dashoffset': '0', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', 'stroke-miterlimit': '4',
16836
+ 'paint-order': 'normal', 'text-anchor': 'start', 'dominant-baseline': 'auto', 'shape-rendering': 'auto',
16837
+ 'vector-effect': null, 'stop-color': null, 'stop-opacity': null, 'opacity': null, 'filter': null,
16838
+ 'mix-blend-mode': null, 'display': null, 'visibility': null, 'cursor': null, 'pointer-events': null,
16839
+ 'color': null, 'font': null, 'letter-spacing': null, 'word-spacing': null, 'text-transform': null,
16840
+ 'text-decoration': null, 'text-shadow': null, 'text-rendering': null, 'white-space': null,
16841
+ 'transition': null, 'animation': null
16842
+ };
16843
+ // What of that paint a FILE states as attributes again (paintAsAttributes,
16844
+ // for the SVG, PNG and PDF downloads). A file has no page to hold its
16845
+ // ground against, and not every program that opens one reads a style as
16846
+ // well as it reads an attribute: MuPDF takes a line's colour from a style
16847
+ // and not its opacity, and drew the faint lines of a time axis at full
16848
+ // strength. These are the paint properties SVG 1.1 has as attributes, and
16849
+ // vector-effect, which every release wrote as one. (paint-order is SVG
16850
+ // 2's: it stays the style it always was. So do the fonts.)
16851
+ const FILE_PAINT = ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity',
16852
+ 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit',
16853
+ 'text-anchor', 'dominant-baseline', 'shape-rendering', 'vector-effect', 'stop-color', 'stop-opacity',
16854
+ 'opacity'];
16855
+ // The paints that are a colour, each with the attribute a colour's alpha
16856
+ // goes to in a file. SVG 1.1 has no colour with an alpha and no
16857
+ // "transparent", and a program that knows neither draws black: MuPDF put
16858
+ // a black slab over a heat map (the surface it listens to the pointer
16859
+ // with) and over every row of a legend, and a solid bar where a node's
16860
+ // age interval is a translucent one.
16861
+ const FILE_ALPHA = {'fill': 'fill-opacity', 'stroke': 'stroke-opacity', 'stop-color': 'stop-opacity'};
16862
+ // every kind of element an svg of the viewer's is made of: the rule names
16863
+ // them one by one, so that a browser never tries it on anything else
16864
+ const SHAPE_ELEMENTS = ['g', 'path', 'line', 'rect', 'circle', 'ellipse', 'polygon', 'polyline', 'text', 'tspan',
16865
+ 'textPath', 'use', 'image', 'a', 'defs', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop',
16866
+ 'symbol', 'marker', 'pattern', 'title', 'desc'];
16867
+ // Weight without rank. A selector made of element names alone weighs less
16868
+ // than one class however many names it has, so eight names that are no
16869
+ // element's, inside a ":not", make a rule outweigh every rule a page
16870
+ // writes with element names only ("label", "fieldset > legend", "*"),
16871
+ // whichever sheet came first, and still give way to any rule for a class.
16872
+ const ELEMENT_WEIGHT = ':not(' + new Array(8).fill('aptx-none').join(' ') + ')';
16873
+ // Where the browser's own focus ring is what shows the focus (the menus
16874
+ // light the entry instead), and the controls there that draw their own.
16875
+ const FOCUS_RING_ROOTS = ['.aptx-panel', '.aptx-dialog', '.aptx-msa-nav'];
16876
+ // (Every rule of this sheet that sets an outline has its control here: the
16877
+ // rule that gives the ring back outweighs them all, so one left out gets
16878
+ // the browser's ring on top of its own. A colour swatch draws an outline
16879
+ // under the pointer, and keeps the browser's ring otherwise.)
16880
+ const OWN_FOCUS = ['.aptx-cp-hex', '.aptx-reps-input', '.aptx-reorder-row', '.aptx-reps-button',
16881
+ '.aptx-msa-nav-jump', '.aptx-clampable', '.aptx-cp-sw:hover', 'input[type=text]', 'select'];
16882
+
16883
+ // Svg markup as a file has it, its paint in attributes, with that paint
16884
+ // moved into inline styles: an attribute gives way to any rule of the page
16885
+ // the markup is put in, and to the viewer's own "as a page that says
16886
+ // nothing has it" (SHAPE_PAINT). The icons and the logo go through here.
16887
+ // An attribute may say what a style may not: a font given piece by piece,
16888
+ // a size without its unit.
16889
+ function paintInline(markup) {
16890
+ let names = Object.keys(SHAPE_PAINT).concat(['font-family', 'font-size', 'font-style', 'font-weight']);
16891
+ let paint = new RegExp('\\s(' + names.join('|') + ')="([^"]*)"', 'g');
16892
+ return markup.replace(/<([a-zA-Z]+)(\s[^<>]*?)(\/?)>/g, function (all, tag, attributes, closed) {
16893
+ let styles = [];
16894
+ let rest = attributes.replace(paint, function (attribute, name, value) {
16895
+ let unitless = (name === 'font-size' || name === 'letter-spacing' || name === 'word-spacing')
16896
+ && /^-?[0-9.]+$/.test(value);
16897
+ styles.push(name + ':' + value + (unitless ? 'px' : ''));
16898
+ return '';
16899
+ });
16900
+ if (styles.length === 0) {
16901
+ return all;
16902
+ }
16903
+ // a style the element already has is said after, and so still has the last word
16904
+ let own = '';
16905
+ rest = rest.replace(/\sstyle="([^"]*)"/, function (attribute, value) {
16906
+ own = ';' + value;
16907
+ return '';
16908
+ });
16909
+ return '<' + tag + rest + ' style="' + styles.join(';') + own + '"' + closed + '>';
16910
+ });
16911
+ }
16912
+
16913
+ function hostProofCss() {
16914
+ let roots = HOST_PROOF_ROOTS.join(', ');
16915
+ // (the weight: BV-BRC's rule is ".Phylogeny svg", exactly as heavy as "svg.aptx-tree")
16916
+ let tree = 'svg.' + TREE_SVG_CLASS + ELEMENT_WEIGHT;
16917
+ // Every root and all that is in it (":where" adds no weight), svgs
16918
+ // apart: their shapes are drawn with attributes, which a rule saying
16919
+ // "revert" would undo. The part about the element itself stands last,
16920
+ // where a browser tries it first: a shape of the tree is turned away
16921
+ // at "svg *". (Measured, a whole page's styles worked out again: 8 ms
16922
+ // became 10 on a page of 20,000 elements around the viewer.)
16923
+ let everything = ':where(' + roots + ')' + ELEMENT_WEIGHT
16924
+ + ', :where(' + roots + ') :not(svg, svg *)' + ELEMENT_WEIGHT;
16925
+ // Every shape of every svg of the viewer's: the tree's, the icons',
16926
+ // the logo's, the clock plot's. With the weight of a class, as the
16927
+ // svgs themselves: "rect:hover", "text:first-child" and ".chart text"
16928
+ // all weigh one class and an element. So a rule of this sheet for a
16929
+ // shape names two classes (".aptx-dialog .aptx-clock-grid"), or it
16930
+ // loses to this one.
16931
+ let svgs = ':is(svg.' + TREE_SVG_CLASS + ', ' + HOST_PROOF_ROOTS.map(function (r) {
16932
+ return r + ' svg';
16933
+ }).join(', ') + ')';
16934
+ let shapes = SHAPE_ELEMENTS.map(function (element) {
16935
+ return svgs + ' ' + element + ELEMENT_WEIGHT;
16936
+ }).join(', ');
16937
+ let properties = Object.keys(SHAPE_PAINT);
16938
+ // where a shape's paint starts from, for what it does not state: said on the svg
16939
+ let start = properties.filter(function (name) {
16940
+ return SHAPE_PAINT[name] !== null;
16941
+ }).map(function (name) {
16942
+ return name + ':' + SHAPE_PAINT[name] + ';';
16943
+ }).join(' ');
16944
+ return ''
16945
+ // 1. Everything starts from the browser's own defaults, as in a
16946
+ // page that says nothing: a reset's "* { box-sizing:border-box }"
16947
+ // or "button { font: inherit }", a framework's "label { font-
16948
+ // weight: 700 }". In a page that does say nothing this rule
16949
+ // changes nothing; and every rule of this sheet for a class,
16950
+ // here or below, still wins (ELEMENT_WEIGHT).
16951
+ + everything + ' { all:revert; }'
16952
+ // An svg's own element likewise (its shapes are left alone),
16953
+ // and with the weight of a class: BV-BRC's rule was a class of
16954
+ // the page's and "svg". So an svg of the viewer's states its
16955
+ // size in the sheet or inline, never as attributes alone, and
16956
+ // its own rule names two classes (or it loses to this one).
16957
+ + ':is(' + roots + ') svg' + ELEMENT_WEIGHT + ' { all:revert; ' + start + ' }'
16958
+ // And a shape of an svg, in what paints it (SHAPE_PAINT): it
16959
+ // states that inline, so what a page says of a "path" or a
16960
+ // "text" is set aside for what the browser has, which for most
16961
+ // of these is what the shape's parent has.
16962
+ + shapes + ' { ' + properties.join(':revert; ') + ':revert; }'
16963
+ // Bootstrap 3 names tick boxes by their type, which outweighs
16964
+ // the rule above: the one tick box here that states no margin
16965
+ // of its own keeps the browser's, and a disabled one its cursor.
16966
+ + '.aptx-dialog .aptx-reorder-check { margin:revert; }'
16967
+ + '.aptx-dialog input[type=checkbox]:disabled, .aptx-dialog input[type=radio]:disabled { cursor:default; }'
16968
+ // 2. What a page hands down by inheritance stops at each root. The
16969
+ // values are the ones a page that says nothing has; a root's
16970
+ // own rule, further down, states its font, size and colour.
16971
+ // (Left to the page on purpose: direction, cursor, visibility,
16972
+ // pointer-events.)
16973
+ + roots + ', ' + tree + ' {'
16974
+ + ' color-scheme:light; accent-color:auto; caret-color:auto;'
16975
+ + ' font:400 11px system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
16976
+ + ' letter-spacing:normal; word-spacing:normal; text-align:start; text-align-last:auto; text-indent:0;'
16977
+ + ' text-transform:none; text-shadow:none; text-rendering:auto;'
16978
+ + ' -webkit-text-size-adjust:auto; text-size-adjust:auto;'
16979
+ + ' white-space:normal; word-break:normal; overflow-wrap:normal; hyphens:manual; tab-size:8;'
16980
+ + ' quotes:auto; list-style:disc outside none; }'
16981
+ // 3. The tree's own svg. Its shapes carry their paint inline; what
16982
+ // they inherit, and what a rule for a bare "svg" would set, is
16983
+ // said here. (Its width, height and font are inline: they are
16984
+ // the file's as well as the page's, see sizeTreeSvg and where
16985
+ // the svg is made. The background is the viewer's own rect.)
16986
+ // border-box, so that a border a page gives it on purpose fits
16987
+ // inside the size stated, in a page with a reset and without.
16988
+ // Nor does it move as a page's "* { transition: all .3s }" would
16989
+ // have it: the tree is redrawn frame by frame.
16990
+ + tree + ' { color:#000; background:none; max-width:none; box-sizing:border-box;'
16991
+ + ' vertical-align:baseline; transition:none; animation:none; ' + start + ' }'
16992
+ // "node" is every d3 example's class as well: ".node { font: 10px sans-serif; cursor: pointer }"
16993
+ + tree + ' g.node { font:inherit; cursor:inherit; }'
16994
+ // 4. The browser's own focus ring, back on whatever does not draw
16995
+ // one of its own: ":focus { outline:0 }" is in many a sheet.
16996
+ + ':is(' + FOCUS_RING_ROOTS.join(', ') + ') :focus-visible:not(' + OWN_FOCUS.join(', ') + ')'
16997
+ + ' { outline:revert; outline-offset:revert; }';
16998
+ }
16999
+
16767
17000
  function injectPanelStyles() {
16768
17001
  loadPanelTheme();
16769
17002
  if (document.getElementById(PANEL_STYLE_ID)) {
@@ -16775,7 +17008,7 @@ function (root, d3, forester, phyloXml) {
16775
17008
  + ' --p-line:#27343f; --p-line-strong:#35434f; --p-surface2:#202d38;'
16776
17009
  + ' --p-accent:#57a6ff; --p-accent-ink:#9cc7ff; --p-accent-weak:rgba(87,166,255,0.18);'
16777
17010
  + ' --p-shadow-sm:0 1px 2px rgba(0,0,0,0.4);';
16778
- let css = ''
17011
+ let css = hostProofCss()
16779
17012
  + '.aptx-panel {'
16780
17013
  + ' --p-bg: rgba(255,255,255,0.86); --p-ink:#1e2a35; --p-muted:#6b7a89; --p-faint:#93a3b2; --p-warn:#b15a0b;'
16781
17014
  + ' --p-line:#e3e9f0; --p-line-strong:#cad6e1; --p-surface2:#f3f6fa;'
@@ -16971,7 +17204,8 @@ function (root, d3, forester, phyloXml) {
16971
17204
  + '.aptx-reps { line-height:1.35; }'
16972
17205
  + '.aptx-reps-lead { margin:0 0 4px; }'
16973
17206
  + '.aptx-reps-choice { display:flex; align-items:center; gap:6px; padding:2px 0; cursor:pointer; }'
16974
- + '.aptx-reps-choice input { accent-color:var(--p-accent); width:13px; height:13px; margin:0; flex:none; }'
17207
+ // (".aptx-dialog": Bootstrap 3's rule for a tick box weighs as much as ".aptx-reps-choice input")
17208
+ + '.aptx-dialog .aptx-reps-choice input { accent-color:var(--p-accent); width:13px; height:13px; margin:0; flex:none; }'
16975
17209
  + '.aptx-reps-disabled { color:var(--p-faint); cursor:default; }'
16976
17210
  + '.aptx-reps-input { display:block; box-sizing:border-box; width:100%; font:inherit; color:var(--p-ink);'
16977
17211
  + ' background:var(--p-surface2); border:1px solid var(--p-line-strong); border-radius:6px; padding:4px 7px; }'
@@ -17020,18 +17254,18 @@ function (root, d3, forester, phyloXml) {
17020
17254
  // node's in the tree and is set on the point itself
17021
17255
  + '.aptx-dialog-drag { cursor:move; touch-action:none; user-select:none; -webkit-user-select:none; }'
17022
17256
  + '.aptx-clock.aptx-dialog-body { overflow:visible; }'
17023
- + '.aptx-clock-svg { display:block; touch-action:none; user-select:none; -webkit-user-select:none; }'
17024
- + '.aptx-clock-svg text { font-size:10px; fill:var(--p-muted); font-variant-numeric:tabular-nums; }'
17025
- + '.aptx-clock-svg text.aptx-clock-title { font-size:10.5px; fill:var(--p-ink); }'
17026
- + '.aptx-clock-ground { fill:var(--p-surface2); }'
17027
- + '.aptx-clock-grid { stroke:var(--p-line); stroke-width:1; shape-rendering:crispEdges; }'
17028
- + '.aptx-clock-frame { stroke:var(--p-line-strong); stroke-width:1; shape-rendering:crispEdges; }'
17029
- + '.aptx-clock-line { stroke:var(--p-accent); stroke-width:1.6; }'
17030
- + '.aptx-clock-internal circle { fill:var(--p-faint); fill-opacity:0.55; }'
17031
- + '.aptx-clock-tips circle { fill:var(--p-ink); fill-opacity:0.8; }'
17032
- + '.aptx-clock-svg circle.aptx-clock-hit { fill-opacity:1; stroke-width:1.2; }'
17033
- + '.aptx-clock-ring { fill:none; stroke:var(--p-accent); stroke-width:1.8; pointer-events:none; }'
17034
- + '.aptx-clock-box { fill:var(--p-accent-weak); stroke:var(--p-accent); stroke-width:1;'
17257
+ + '.aptx-dialog .aptx-clock-svg { display:block; touch-action:none; user-select:none; -webkit-user-select:none; }'
17258
+ + '.aptx-dialog .aptx-clock-svg text { font-size:10px; fill:var(--p-muted); font-variant-numeric:tabular-nums; }'
17259
+ + '.aptx-dialog .aptx-clock-svg text.aptx-clock-title { font-size:10.5px; fill:var(--p-ink); }'
17260
+ + '.aptx-dialog .aptx-clock-ground { fill:var(--p-surface2); }'
17261
+ + '.aptx-dialog .aptx-clock-grid { stroke:var(--p-line); stroke-width:1; shape-rendering:crispEdges; }'
17262
+ + '.aptx-dialog .aptx-clock-frame { stroke:var(--p-line-strong); stroke-width:1; shape-rendering:crispEdges; }'
17263
+ + '.aptx-dialog .aptx-clock-line { stroke:var(--p-accent); stroke-width:1.6; }'
17264
+ + '.aptx-dialog .aptx-clock-internal circle { fill:var(--p-faint); fill-opacity:0.55; }'
17265
+ + '.aptx-dialog .aptx-clock-tips circle { fill:var(--p-ink); fill-opacity:0.8; }'
17266
+ + '.aptx-dialog .aptx-clock-svg circle.aptx-clock-hit { fill-opacity:1; stroke-width:1.2; }'
17267
+ + '.aptx-dialog .aptx-clock-ring { fill:none; stroke:var(--p-accent); stroke-width:1.8; pointer-events:none; }'
17268
+ + '.aptx-dialog .aptx-clock-box { fill:var(--p-accent-weak); stroke:var(--p-accent); stroke-width:1;'
17035
17269
  + ' shape-rendering:crispEdges; pointer-events:none; }'
17036
17270
  + '.aptx-clock-options { display:flex; align-items:center; gap:14px; margin:6px 0 8px; }'
17037
17271
  + '.aptx-clock-options .aptx-clock-deselect { margin-left:auto; height:22px; padding:0 9px; }'
@@ -17097,7 +17331,7 @@ function (root, d3, forester, phyloXml) {
17097
17331
  + '.aptx-about .aptx-dialog-key { flex-basis:31%; }'
17098
17332
  + '.aptx-about-head { display:flex; align-items:center; gap:12px; margin-bottom:10px; }'
17099
17333
  + '.aptx-about-logo { flex:none; width:56px; height:56px; }'
17100
- + '.aptx-about-logo svg { width:100%; height:100%; display:block; }'
17334
+ + '.aptx-dialog .aptx-about-logo svg { width:100%; height:100%; display:block; }'
17101
17335
  + '.aptx-about-name { font-size:15px; font-weight:650; letter-spacing:-0.01em; }'
17102
17336
  + '.aptx-about-version { color:var(--p-muted); }'
17103
17337
  + '.aptx-about-blurb { margin:0 0 11px; color:var(--p-muted); }'
@@ -17123,6 +17357,7 @@ function (root, d3, forester, phyloXml) {
17123
17357
  + '.aptx-panel legend { float:none; width:auto; padding:0; margin:0 0 5px; font-size:9px; font-weight:700; letter-spacing:0.09em; text-transform:uppercase; color:var(--p-faint); }'
17124
17358
  + '.aptx-panel label { cursor:pointer; }'
17125
17359
  + '.aptx-panel input[type=checkbox],.aptx-panel input[type=radio] { accent-color:var(--p-accent); width:13px; height:13px; vertical-align:-2px; margin:0 4px 0 0; }'
17360
+ + '.aptx-panel input[type=checkbox]:disabled,.aptx-panel input[type=radio]:disabled { cursor:default; }'
17126
17361
  // checkbox/radio + label as one item (used by the Display Data grid and the inline P/A/C and search-option rows)
17127
17362
  // every item carries an invisible box, so the one that lights up
17128
17363
  // (Auto-hide Labels while it is hiding) does not shift its neighbours
@@ -17210,9 +17445,9 @@ function (root, d3, forester, phyloXml) {
17210
17445
  + '.aptx-panel .aptx-searchnav { align-items:center; gap:4px; margin:2px 0 4px; }'
17211
17446
  + '.aptx-panel .aptx-searchnav span { flex:1 1 auto; text-align:center; font-weight:600; font-size:11px; color:var(--p-ink); }'
17212
17447
  + '.aptx-panel .aptx-searchnav .aptx-gbtn:last-child { margin-right:0; }'
17213
- + '.aptx-found-halo { opacity:' + HALO_REST_OPACITY + '; transform-box:fill-box; transform-origin:center; animation:aptx-halo-pulse 1.3s ease-in-out infinite; }'
17448
+ + 'svg.' + TREE_SVG_CLASS + ' .aptx-found-halo { opacity:' + HALO_REST_OPACITY + '; transform-box:fill-box; transform-origin:center; animation:aptx-halo-pulse 1.3s ease-in-out infinite; }'
17214
17449
  + '@keyframes aptx-halo-pulse { 0%,100% { transform:scale(1); opacity:' + HALO_REST_OPACITY + '; } 50% { transform:scale(2.5); opacity:0.12; } }'
17215
- + '@media (prefers-reduced-motion: reduce) { .aptx-found-halo { animation:none; } }'
17450
+ + '@media (prefers-reduced-motion: reduce) { svg.' + TREE_SVG_CLASS + ' .aptx-found-halo { animation:none; } }'
17216
17451
  + '.aptx-panel .aptx-zoomgrid { display:flex; flex-direction:column; align-items:stretch; }'
17217
17452
  + '.aptx-panel .aptx-zoomgrid > input[type=button] { width:100%; margin-right:0; }'
17218
17453
  + '.aptx-panel .aptx-zoomrow { display:flex; }'
@@ -18909,7 +19144,7 @@ function (root, d3, forester, phyloXml) {
18909
19144
  head.className = 'aptx-about-head';
18910
19145
  let logo = document.createElement('div');
18911
19146
  logo.className = 'aptx-about-logo';
18912
- logo.innerHTML = ARCHAEOPTERYX_LOGO_SVG;
19147
+ logo.innerHTML = paintInline(ARCHAEOPTERYX_LOGO_SVG);
18913
19148
  head.appendChild(logo);
18914
19149
 
18915
19150
  let titles = document.createElement('div');
@@ -20244,7 +20479,7 @@ function (root, d3, forester, phyloXml) {
20244
20479
  .attr('height', '100%')
20245
20480
  .style('opacity', 1)
20246
20481
  .attr('class', BASE_BACKGROUND)
20247
- .attr('fill', _state.backgroundColorDefault);
20482
+ .style('fill', _state.backgroundColorDefault);
20248
20483
  }
20249
20484
 
20250
20485
 
@@ -20450,6 +20685,19 @@ function (root, d3, forester, phyloXml) {
20450
20685
  return _baseSvg ? _baseSvg.node() : null;
20451
20686
  }
20452
20687
 
20688
+ // The tree svg's size, said twice. The attributes are what an export and
20689
+ // the layout read; but an attribute gives way to any rule of the page the
20690
+ // viewer is drawn in: under "svg { width: 24px; height: 24px }", a page
20691
+ // whose only svgs were icons, the tree was 24 px square. (A rule that says
20692
+ // "height: auto" changes nothing: the attributes still give the svg its
20693
+ // own size.) The inline style does not give way.
20694
+ function sizeTreeSvg(svg, w, h) {
20695
+ svg.attr('width', w)
20696
+ .attr('height', h)
20697
+ .style('width', w + 'px')
20698
+ .style('height', h + 'px');
20699
+ }
20700
+
20453
20701
  // An export is always light, whatever the screen is set to: a dark PNG or
20454
20702
  // SVG is wrong on paper, in a slide and in a paper figure, and it is the
20455
20703
  // file that outlives the session. Only the four theme colours are swapped,
@@ -20493,6 +20741,15 @@ function (root, d3, forester, phyloXml) {
20493
20741
  }
20494
20742
 
20495
20743
  function cleanExportCopy(copy) {
20744
+ // What the live svg carries only for the page it is drawn in (its
20745
+ // class, its size said a second time): a file has no page, and an
20746
+ // inline size would hold a full-size drawing to the window's.
20747
+ copy.classList.remove(TREE_SVG_CLASS);
20748
+ if (!copy.getAttribute('class')) {
20749
+ copy.removeAttribute('class');
20750
+ }
20751
+ copy.style.removeProperty('width');
20752
+ copy.style.removeProperty('height');
20496
20753
  // Serialize a COPY with the overview taken out of it: the overview is
20497
20754
  // an on-screen navigation aid, not part of the tree, and working on a
20498
20755
  // copy leaves the live display untouched.
@@ -20509,7 +20766,57 @@ function (root, d3, forester, phyloXml) {
20509
20766
  if (glow) {
20510
20767
  glow.remove();
20511
20768
  }
20512
- return copy;
20769
+ return paintAsAttributes(copy);
20770
+ }
20771
+
20772
+ // The opposite of paintInline, for a file: a shape's paint, inline styles
20773
+ // on the page, is attributes again (FILE_PAINT). Where an element says a
20774
+ // thing both ways, the style is what painted it and has the last word.
20775
+ function paintAsAttributes(svg) {
20776
+ [svg].concat(Array.prototype.slice.call(svg.querySelectorAll('[style]'))).forEach(function (el) {
20777
+ let style = el.style;
20778
+ if (!style) {
20779
+ return;
20780
+ }
20781
+ FILE_PAINT.forEach(function (name) {
20782
+ let value = style.getPropertyValue(name);
20783
+ if (value !== '') {
20784
+ // (the browser writes url("#id"); an attribute has no quotes there)
20785
+ el.setAttribute(name, value.replace(/^url\("([^"]*)"\)$/, 'url($1)'));
20786
+ style.removeProperty(name);
20787
+ }
20788
+ });
20789
+ if (style.length === 0) {
20790
+ el.removeAttribute('style');
20791
+ }
20792
+ });
20793
+ // A colour with an alpha is the colour and, in the paint's own
20794
+ // opacity, the alpha times what the shape has of it already, stated
20795
+ // or handed down; a paint that is no paint at all is "none". Only on
20796
+ // a shape: an opacity stated on a group would reach the shapes in it
20797
+ // that have a colour of their own.
20798
+ Array.prototype.slice.call(svg.querySelectorAll('[fill], [stroke], [stop-color]')).forEach(function (el) {
20799
+ let holdsShapes = Array.prototype.some.call(el.children, function (child) {
20800
+ return child.localName !== 'title' && child.localName !== 'desc';
20801
+ });
20802
+ Object.keys(FILE_ALPHA).forEach(function (paint) {
20803
+ let c = el.hasAttribute(paint) ? d3.color(el.getAttribute(paint)) : null;
20804
+ if (!c || !(c.opacity < 1) || holdsShapes) {
20805
+ return;
20806
+ }
20807
+ let opacity = FILE_ALPHA[paint];
20808
+ if (c.opacity === 0 && paint !== 'stop-color') {
20809
+ el.setAttribute(paint, 'none');
20810
+ return;
20811
+ }
20812
+ // (a gradient's stop hands nothing down, and is handed nothing)
20813
+ let from = paint === 'stop-color' ? (el.hasAttribute(opacity) ? el : null) : el.closest('[' + opacity + ']');
20814
+ let has = from ? parseFloat(from.getAttribute(opacity)) : 1;
20815
+ el.setAttribute(paint, c.copy({opacity: 1}).formatRgb());
20816
+ el.setAttribute(opacity, String(+(c.opacity * (isNaN(has) ? 1 : has)).toFixed(4)));
20817
+ });
20818
+ });
20819
+ return svg;
20513
20820
  }
20514
20821
 
20515
20822
  // The same element of the copy as `live` is of the svg: children are