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/forester.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
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3901,46 +3901,208 @@
|
|
|
3901
3901
|
// A uniform grid rather than all-pairs: a 13,246-tip tree with branch
|
|
3902
3902
|
// lengths on offers 18,511 boxes, and 171 million comparisons per redraw
|
|
3903
3903
|
// is not a redraw.
|
|
3904
|
-
|
|
3904
|
+
/**
|
|
3905
|
+
* The same first-come space-keeping as labelOccupancy, but for boxes that
|
|
3906
|
+
* are TURNED: each claim is a quadrilateral, and two of them overlap only
|
|
3907
|
+
* if their own outlines do.
|
|
3908
|
+
*
|
|
3909
|
+
* A turned label cannot use the axis-aligned map. The bounds of a rotated
|
|
3910
|
+
* rectangle grow with its length, so a long name lying near its spoke
|
|
3911
|
+
* reserves several times its own area, and the map refuses neighbours that
|
|
3912
|
+
* a reader can see are clear of it. Measured on a 48-label circular ring:
|
|
3913
|
+
* 28 pairs "overlapping" by the bounds where the outlines gave 0, and a
|
|
3914
|
+
* first-come pass over the same labels in the same order kept 48 by
|
|
3915
|
+
* outline against 32 by bounds -- sixteen names dropped for nothing.
|
|
3916
|
+
*
|
|
3917
|
+
* The grid is still used, on each quad's bounds, to find which claims are
|
|
3918
|
+
* worth testing; only the test itself is exact.
|
|
3919
|
+
*
|
|
3920
|
+
* @param cell - grid size, in the same units as the quads
|
|
3921
|
+
* @returns an object with claim(quad) and occupy(quad); a quad is four
|
|
3922
|
+
* [x, y] corners in order round the shape
|
|
3923
|
+
*/
|
|
3924
|
+
forester.orientedOccupancy = function (cell) {
|
|
3905
3925
|
let size = Math.max(4, cell);
|
|
3906
3926
|
let cells = new Map();
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3927
|
+
let boundsOf = function (quad) {
|
|
3928
|
+
let x0 = quad[0][0], x1 = quad[0][0], y0 = quad[0][1], y1 = quad[0][1];
|
|
3929
|
+
for (let i = 1; i < quad.length; ++i) {
|
|
3930
|
+
x0 = Math.min(x0, quad[i][0]);
|
|
3931
|
+
x1 = Math.max(x1, quad[i][0]);
|
|
3932
|
+
y0 = Math.min(y0, quad[i][1]);
|
|
3933
|
+
y1 = Math.max(y1, quad[i][1]);
|
|
3934
|
+
}
|
|
3935
|
+
return [x0, y0, x1, y1];
|
|
3936
|
+
};
|
|
3937
|
+
// Separating-axis test. A rectangle's own two edge normals are enough
|
|
3938
|
+
// for itself, but BOTH shapes' normals are needed: with only one
|
|
3939
|
+
// shape's, a box that clears another along the OTHER's axis is
|
|
3940
|
+
// reported as overlapping. Strict, so shapes that merely touch do not
|
|
3941
|
+
// overlap -- as in labelOccupancy.
|
|
3942
|
+
let overlaps = function (P, Q) {
|
|
3943
|
+
for (let s = 0; s < 2; ++s) {
|
|
3944
|
+
let R = s === 0 ? P : Q;
|
|
3945
|
+
for (let k = 0; k < 2; ++k) {
|
|
3946
|
+
let ax = R[k + 1][1] - R[k][1];
|
|
3947
|
+
let ay = R[k][0] - R[k + 1][0];
|
|
3948
|
+
let p0 = Infinity, p1 = -Infinity, q0 = Infinity, q1 = -Infinity;
|
|
3949
|
+
for (let i = 0; i < P.length; ++i) {
|
|
3950
|
+
let d = (P[i][0] * ax) + (P[i][1] * ay);
|
|
3951
|
+
p0 = Math.min(p0, d);
|
|
3952
|
+
p1 = Math.max(p1, d);
|
|
3953
|
+
}
|
|
3954
|
+
for (let i = 0; i < Q.length; ++i) {
|
|
3955
|
+
let d = (Q[i][0] * ax) + (Q[i][1] * ay);
|
|
3956
|
+
q0 = Math.min(q0, d);
|
|
3957
|
+
q1 = Math.max(q1, d);
|
|
3958
|
+
}
|
|
3959
|
+
if (p1 <= q0 || q1 <= p0) {
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3912
3962
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3963
|
+
}
|
|
3964
|
+
return true;
|
|
3965
|
+
};
|
|
3966
|
+
let record = function (quad, b) {
|
|
3967
|
+
let c1 = Math.floor(b[2] / size);
|
|
3968
|
+
let r1 = Math.floor(b[3] / size);
|
|
3969
|
+
for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
|
|
3970
|
+
for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
|
|
3971
|
+
let key = c + ':' + r;
|
|
3972
|
+
let bucket = cells.get(key);
|
|
3973
|
+
if (!bucket) {
|
|
3974
|
+
bucket = [];
|
|
3975
|
+
cells.set(key, bucket);
|
|
3976
|
+
}
|
|
3977
|
+
bucket.push(quad);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
};
|
|
3981
|
+
let free = function (quad, b) {
|
|
3982
|
+
let seen = null;
|
|
3983
|
+
let c1 = Math.floor(b[2] / size);
|
|
3984
|
+
let r1 = Math.floor(b[3] / size);
|
|
3985
|
+
for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
|
|
3986
|
+
for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
|
|
3987
|
+
let bucket = cells.get(c + ':' + r);
|
|
3988
|
+
if (!bucket) {
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
for (let i = 0; i < bucket.length; ++i) {
|
|
3992
|
+
// a quad spans several cells, so the same one comes up
|
|
3993
|
+
// more than once; the test is the expensive part
|
|
3994
|
+
if (seen === null) {
|
|
3995
|
+
seen = new Set();
|
|
3996
|
+
}
|
|
3997
|
+
if (seen.has(bucket[i])) {
|
|
3919
3998
|
continue;
|
|
3920
3999
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
if (x < (b[0] + b[2]) && (x + w) > b[0]
|
|
3925
|
-
&& y < (b[1] + b[3]) && (y + h) > b[1]) {
|
|
3926
|
-
return false;
|
|
3927
|
-
}
|
|
4000
|
+
seen.add(bucket[i]);
|
|
4001
|
+
if (overlaps(quad, bucket[i])) {
|
|
4002
|
+
return false;
|
|
3928
4003
|
}
|
|
3929
4004
|
}
|
|
3930
4005
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
4006
|
+
}
|
|
4007
|
+
return true;
|
|
4008
|
+
};
|
|
4009
|
+
let degenerate = function (quad) {
|
|
4010
|
+
if (!quad || quad.length !== 4) {
|
|
4011
|
+
return true;
|
|
4012
|
+
}
|
|
4013
|
+
let b = boundsOf(quad);
|
|
4014
|
+
return !((b[2] - b[0]) > 0) || !((b[3] - b[1]) > 0);
|
|
4015
|
+
};
|
|
4016
|
+
return {
|
|
4017
|
+
claim: function (quad) {
|
|
4018
|
+
if (degenerate(quad)) {
|
|
4019
|
+
return true; // reserves nothing, as a zero-size box does
|
|
4020
|
+
}
|
|
4021
|
+
let b = boundsOf(quad);
|
|
4022
|
+
if (!free(quad, b)) {
|
|
4023
|
+
return false;
|
|
4024
|
+
}
|
|
4025
|
+
record(quad, b);
|
|
4026
|
+
return true;
|
|
4027
|
+
},
|
|
4028
|
+
// record without asking, for what is drawn whatever else is there
|
|
4029
|
+
occupy: function (quad) {
|
|
4030
|
+
if (degenerate(quad)) {
|
|
4031
|
+
return true;
|
|
4032
|
+
}
|
|
4033
|
+
let b = boundsOf(quad);
|
|
4034
|
+
let clear = free(quad, b);
|
|
4035
|
+
record(quad, b);
|
|
4036
|
+
return clear;
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
};
|
|
4040
|
+
|
|
4041
|
+
forester.labelOccupancy = function (cell) {
|
|
4042
|
+
let size = Math.max(4, cell);
|
|
4043
|
+
let cells = new Map();
|
|
4044
|
+
let record = function (x, y, w, h) {
|
|
4045
|
+
let box = [x, y, w, h];
|
|
4046
|
+
let c1 = Math.floor((x + w) / size);
|
|
4047
|
+
let r1 = Math.floor((y + h) / size);
|
|
4048
|
+
for (let c = Math.floor(x / size); c <= c1; ++c) {
|
|
4049
|
+
for (let r = Math.floor(y / size); r <= r1; ++r) {
|
|
4050
|
+
let key = c + ':' + r;
|
|
4051
|
+
let bucket = cells.get(key);
|
|
4052
|
+
if (!bucket) {
|
|
4053
|
+
bucket = [];
|
|
4054
|
+
cells.set(key, bucket);
|
|
4055
|
+
}
|
|
4056
|
+
bucket.push(box);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
let free = function (x, y, w, h) {
|
|
4061
|
+
let c1 = Math.floor((x + w) / size);
|
|
4062
|
+
let r1 = Math.floor((y + h) / size);
|
|
4063
|
+
for (let c = Math.floor(x / size); c <= c1; ++c) {
|
|
4064
|
+
for (let r = Math.floor(y / size); r <= r1; ++r) {
|
|
4065
|
+
let bucket = cells.get(c + ':' + r);
|
|
4066
|
+
if (!bucket) {
|
|
4067
|
+
continue;
|
|
4068
|
+
}
|
|
4069
|
+
for (let i = 0; i < bucket.length; ++i) {
|
|
4070
|
+
let b = bucket[i];
|
|
4071
|
+
// strict: boxes that merely touch do not overlap
|
|
4072
|
+
if (x < (b[0] + b[2]) && (x + w) > b[0]
|
|
4073
|
+
&& y < (b[1] + b[3]) && (y + h) > b[1]) {
|
|
4074
|
+
return false;
|
|
3939
4075
|
}
|
|
3940
|
-
bucket.push(box);
|
|
3941
4076
|
}
|
|
3942
4077
|
}
|
|
4078
|
+
}
|
|
4079
|
+
return true;
|
|
4080
|
+
};
|
|
4081
|
+
return {
|
|
4082
|
+
// true when nothing already granted overlaps, and then recorded
|
|
4083
|
+
claim: function (x, y, w, h) {
|
|
4084
|
+
if (!(w > 0) || !(h > 0)) {
|
|
4085
|
+
return true; // reserves no space; recording it would only cost comparisons
|
|
4086
|
+
}
|
|
4087
|
+
if (!free(x, y, w, h)) {
|
|
4088
|
+
return false;
|
|
4089
|
+
}
|
|
4090
|
+
record(x, y, w, h);
|
|
3943
4091
|
return true;
|
|
4092
|
+
},
|
|
4093
|
+
// Record without asking, for what is drawn whatever else is there.
|
|
4094
|
+
// A refused CLAIM records nothing, so a label put through claim()
|
|
4095
|
+
// and refused would leave the space open and let a number print
|
|
4096
|
+
// straight through the name it lost to -- the opposite of the
|
|
4097
|
+
// point. Returns whether the space was clear, for measurement;
|
|
4098
|
+
// the box is recorded either way.
|
|
4099
|
+
occupy: function (x, y, w, h) {
|
|
4100
|
+
if (!(w > 0) || !(h > 0)) {
|
|
4101
|
+
return true;
|
|
4102
|
+
}
|
|
4103
|
+
let clear = free(x, y, w, h);
|
|
4104
|
+
record(x, y, w, h);
|
|
4105
|
+
return clear;
|
|
3944
4106
|
}
|
|
3945
4107
|
};
|
|
3946
4108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
|
|
5
5
|
"main": "archaeopteryx.js",
|
|
6
6
|
"types": "archaeopteryx.d.ts",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"format": "prettier --write .",
|
|
27
27
|
"format:check": "prettier --check .",
|
|
28
|
-
"docs:sync": "cp archaeopteryx.js forester.js docs/lib/"
|
|
28
|
+
"docs:sync": "cp archaeopteryx.js forester.js docs/lib/",
|
|
29
|
+
"test:browser": "node test/browser/run.js",
|
|
30
|
+
"test:browser:quick": "node test/browser/run.js --quick"
|
|
29
31
|
},
|
|
30
32
|
"repository": {
|
|
31
33
|
"type": "git",
|