@euphrasiologist/lwphylo 1.1.5 → 1.1.9
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/dist/index.js +2 -0
- package/package.json +11 -7
- package/src/phisheye/phisheye.js +111 -65
- package/src/radial/describeArc.js +22 -12
- package/src/radial/getArcs.js +47 -56
- package/src/radial/getRadii.js +32 -38
- package/src/radial/polarToCartesian.js +5 -7
- package/src/radial/radialData.js +71 -89
- package/src/radial/radialLayout.js +8 -9
- package/src/radial/reflectAngle.js +7 -17
- package/src/rectangle/getChildVerticals.js +47 -0
- package/src/rectangle/getHorizontal.js +49 -82
- package/src/rectangle/getVertical.js +29 -34
- package/src/rectangle/rectangleLayout.js +23 -10
- package/src/unrooted/equalAngleLayout.js +36 -25
- package/src/unrooted/unrooted.js +1 -1
- package/src/utils/edges.js +13 -42
- package/src/utils/indexing.js +64 -0
- package/src/utils/meanAngle.js +10 -35
- package/src/utils/numTips.js +15 -20
- package/src/utils/parentFisheye.js +9 -11
- package/src/utils/preorder.js +33 -6
- package/src/utils/readTree.js +53 -63
- package/dist/@euphrasiologist/lwphylo.js +0 -867
- package/dist/@euphrasiologist/lwphylo.min.js +0 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const n={circular:()=>{let n=200,t=2,e=[0,0],r={},i=0,c=0,a=n*n,s=10;function o(){const e=Math.exp(t);return i=e/(e-1)*n,c=t/n,a=n*n,h}function h(n){!function(){if(!r||"function"!=typeof r.xscale||"function"!=typeof r.yscale)throw new Error("phisheye.circular: call .scales(xscale, yscale) before using the fisheye.")}();const t=r.xscale(n.x),o=r.yscale(n.y),h=t-e[0],l=o-e[1],u=h*h+l*l;if(0===u||u>=a)return{x:t,y:o,z:u>=a?1:s};const d=Math.sqrt(u),f=i*(1-Math.exp(-d*c))/d*.75+.25;return{x:e[0]+h*f,y:e[1]+l*f,z:Math.min(f,s)}}return h.radius=function(t){return arguments.length?(n=Math.max(0,+t),o()):n},h.distortion=function(n){return arguments.length?(t=Math.max(0,+n),o()):t},h.focus=function(n){if(!arguments.length)return e.slice();if(!Array.isArray(n)||2!==n.length)throw new Error("focus expects [x, y] in pixels.");return e=[+n[0],+n[1]],h},h.scales=function(n,t){return arguments.length?(r={xscale:n,yscale:t},h):r},h.setScales=h.scales,h.clampZ=function(n){return arguments.length?(s=Math.max(1,+n),h):s},h.focusFromEvent=function(n,t){const e=t.getBoundingClientRect(),r=n.clientX-e.left,i=n.clientY-e.top;return h.focus([r,i])},o()}};function t(n,t,e,r){return{x:n+e*Math.cos(r),y:t+e*Math.sin(r)}}function e(n){const t=n%(2*Math.PI);return t<0?t+2*Math.PI:t}function r(n,r,i,c,a){let s=e(c),o=e(a)-s;o<=-Math.PI?o+=2*Math.PI:o>Math.PI&&(o-=2*Math.PI);const h=Math.abs(o)>Math.PI?1:0,l=o>=0?1:0,u=t(n,r,i,s),d=t(n,r,i,s+o);return["M",u.x,u.y,"A",i,i,0,h,l,d.x,d.y].join(" ")}function i(n,t=[]){t.push(n);for(let e=0;e<(n.children?.length||0);e++)t=i(n.children[e],t);return t}function c(n,t=!0){var e=[];for(const t of i(n))null===t.parent?e.push({parentId:null,parentLabel:null,thisId:t.id,thisLabel:t.label,children:t.children.map(n=>n.id),branchLength:0,isTip:!1,x:t.x,y:t.y,angle:t.angle}):e.push({parentId:t.parent.id,parentLabel:t.parent.label,thisId:t.id,thisLabel:t.label,children:t.children.map(n=>n.id),branchLength:t.branchLength,isTip:0==t.children.length,x:t.x,y:t.y,angle:t.angle});return t&&(e=e.sort(function(n,t){return n.thisId-t.thisId})),e}function a(n){const t=2*Math.PI,e=c(n,!0),r=new Map(e.map(n=>[n.thisId,n])),i=new Map(e.map(n=>[n.thisId,n.children||[]]));let a=null;for(const n of e)if(null==n.parentId){a=n.thisId;break}const s=[];!function n(t){const e=i.get(t)||[];if(0!==e.length)for(const t of e)n(t);else s.push(t)}(a);const o=Math.max(1,s.length),h=new Map;s.forEach((n,e)=>{h.set(n,e/o*t)}),function n(e){const r=i.get(e)||[];for(const t of r)n(t);if(r.length>0){let n=0,i=0;for(const t of r){const e=h.get(t);n+=Math.cos(e),i+=Math.sin(e)}h.set(e,(Math.atan2(i,n)%t+t)%t)}}(a);const l=new Map;l.set(a,0),function n(t){const e=i.get(t)||[],c=l.get(t)||0;for(const t of e){const e=r.get(t),i=e?.branchLength??0;l.set(t,c+i),n(t)}}(a);for(const n of e){const t=h.get(n.thisId)??0,e=l.get(n.thisId)??0;n.angle=t,n.r=e,n.x=e*Math.cos(t),n.y=e*Math.sin(t)}return e}function s(n){const t=n%(2*Math.PI);return t<0?t+2*Math.PI:t}function o(n,t){n=s(n);let e=(t=s(t))-n;return e<=-Math.PI?e+=2*Math.PI:e>Math.PI&&(e-=2*Math.PI),[n,n+e]}function h(n){const t={};return t.data=a(n),t.radii=function(n){const t=a(n),e=new Map(t.map(n=>[n.thisId,n])),r=t.find(n=>null==n.parentId)?.thisId,i=[];for(const n of t){if(n.thisId===r)continue;const t=e.get(n.parentId);if(!t)continue;const c=n.angle,a=t.r,s=n.r;i.push({parentId:t.thisId,childId:n.thisId,x0:a*Math.cos(c),y0:a*Math.sin(c),x1:s*Math.cos(c),y1:s*Math.sin(c),isTip:!!n.isTip})}return i}(n),t.arcs=function(n){const t=new Map(n.map(n=>[n.thisId,n])),e=n.find(n=>null==n.parentId)?.thisId,r=[];for(const i of n){if(i.thisId===e)continue;const n=t.get(i.parentId);if(!n)continue;const[c,a]=o(n.angle,i.angle);isFinite(n.r)&&0!==n.r&&c!==a&&r.push({parentId:n.thisId,childId:i.thisId,radius:n.r,start:c,end:a})}return r}(t.data),t}function l(n){const t=c(n),e=new Map(t.map((n,t)=>[n.thisId,t])),r=[];!function n(t){t.children&&0!==t.children.length?t.children.forEach(n):r.push(t.id)}(n);const i=new Map(r.map((n,t)=>[n,t+1]));return function n(r){const c=e.get(r.id);if(!r.children||0===r.children.length){const n=i.get(r.id);return t[c].y0=n,t[c].y1=n,n}const a=function(n){let t=0,e=0;for(let r of n)null!=r&&(r=+r)>=r&&(++t,e+=r);if(t)return e/t}(r.children.map(n));return t[c].y0=a,t[c].y1=a,a}(n),function n(r,i){const c=e.get(r.id),a=i??0,s=a+(t[c].branchLength??0);t[c].x0=a,t[c].x1=s,r.children&&r.children.length&&r.children.forEach(t=>n(t,s))}(n,0),t.map(n=>{const{y:t,x:e,angle:r,...i}=n;return i})}function u(n){const t=l(n),e=function(n){const t=l(n),e=new Map;for(const n of t){if(null==n.parentId)continue;const t=e.get(n.parentId);t?t.push(n):e.set(n.parentId,[n])}const r=[];for(const[n,t]of e.entries()){if(!t.length)continue;const e=t.map(n=>n.y0),i=Math.min(...e),c=Math.max(...e),a=t[0].x0;r.push({parentId:n,x0:a,x1:a,y0:i,y1:c,heights:c-i})}return r}(n),r=function(n){const t=l(n),e=new Map(t.map(n=>[n.thisId,n])),r=[];for(const n of t){if(null==n.parentId)continue;const t=e.get(n.parentId);if(!t)continue;const i=n.x0,c=n.y0,a=t.y0,s=Math.min(c,a),o=Math.max(c,a);r.push({parentId:n.parentId,childId:n.thisId,x:i,y0:s,y1:o})}return r}(n),i=t.filter(n=>null!=n.parentId).map(n=>({parentId:n.parentId,childId:n.thisId,x0:n.x0,x1:n.x1,y:n.y0}));return{data:t,vertical_lines:e,child_vertical_lines:r,horizontal_lines:i}}function d(n){null===n.parent&&(function n(t){if(!t.children||0===t.children.length)return t.ntips=1,1;let e=0;for(const r of t.children)e+=n(r);return t.ntips=e,e}(n),n.start=0,n.end=2,n.angle=0,n.ntips=function(n){var t=0;for(const e of function(n){const t=[n],e=[];for(;t.length;){const n=t.shift();e.push(n);for(const e of n.children)t.push(e)}return e}(n))0==e.children.length&&t++;return t}(n),n.x=0,n.y=0);let t=n.start;for(let e=0;e<n.children.length;e++){const r=n.children[e],i=(n.end-n.start)*(r.ntips/n.ntips);r.start=t,r.end=t+i,r.angle=r.start+(r.end-r.start)/2,t=r.end;const c=r.angle*Math.PI,a=r.branchLength??0;r.x=n.x+a*Math.sin(c),r.y=n.y+a*Math.cos(c),d(r)}return n}function f(n){var t={},e=c(d(n));return t.data=e,t.edges=function(n,t=!1){const e=[...n].sort((n,t)=>n.thisId-t.thisId),r=new Map(e.map(n=>[n.thisId,n])),i=[];for(const n of e){if(null==n.parentId)continue;const e=r.get(n.parentId);e&&(t?(i.push({x1:n.x,y1:n.y,id1:n.thisId,x2:e.x,y2:n.y,id2:void 0}),i.push({x1:e.x,y1:n.y,id1:void 0,x2:e.x,y2:e.y,id2:n.parentId})):i.push({x1:n.x,y1:n.y,id1:n.thisId,x2:e.x,y2:e.y,id2:n.parentId}))}return i}(e),t}function p(n,t){const e=function(n,t="thisId"){return new Map(n.map(n=>[n[t],n]))}(t),r=e.get(n.parentId);return r?{px:r.fisheye.x,py:r.fisheye.y}:null}function I(n){const t=(n=String(n).replace(/\s+/g,"")).split(/(;|\(|\)|,)/),e={parent:null,children:[]};let r=e,i=0;for(const n of t)if(n&&";"!==n)if("("===n){const n={parent:r,children:[]};r.children.push(n),r=n}else if(","===n){r=r.parent;const n={parent:r,children:[]};r.children.push(n),r=n}else if(")"===n){if(r=r.parent,null===r)break}else{const t=n.split(":");1===t.length?n.startsWith(":")?(r.label="",r.branchLength=parseFloat(t[0])):(r.label=t[0],r.branchLength=null):2===t.length?(r.label=t[0],r.branchLength=parseFloat(t[1])):(console.warn(n,"Unhandled token with multiple ':' characters"),r.label=t[0]||"",r.branchLength=parseFloat(t[t.length-1])),r.id=i++}return null==e.id&&(e.id=i),e}function g(n,t){let e={};n.data.forEach(n=>{e[n.thisId]={...n}});let r={};const i=function(n){e[n]&&(r[n]=e[n],e[n].children&&e[n].children.forEach(n=>{i(n)}))};i(t);const c=[["data",Object.values(r)]],a=c[0][1].map(n=>n.thisId);var s=[];for(const t in n)"data"!==t&&s.push([t,n[t]]);var o=s.map(n=>[n[0],n[1].filter(n=>a.includes(n.thisId))]);return Object.fromEntries(c.concat(o))}export{r as describeArc,p as parentFisheye,n as phisheye,h as radialLayout,I as readTree,u as rectangleLayout,g as subTree,f as unrooted};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@euphrasiologist/lwphylo",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "A lightweight, low level javascript library to plot phylogenies from Newick files.",
|
|
5
5
|
"main": "dist/@euphrasiologist/lwphylo.js",
|
|
6
6
|
"unpkg": "dist/@euphrasiologist/lwphylo.min.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"pretest": "rollup -c",
|
|
18
18
|
"test": "eslint src",
|
|
19
|
-
"prepublishOnly": "rm -rf dist &&
|
|
19
|
+
"prepublishOnly": "rm -rf dist && npm run test"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
@@ -33,9 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/Euphrasiologist/lwPhylo#readme",
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"eslint": "
|
|
37
|
-
"
|
|
38
|
-
"rollup": "
|
|
39
|
-
"rollup
|
|
36
|
+
"@eslint/js": "^9.37.0",
|
|
37
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
38
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
39
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
40
|
+
"eslint": "^9.37.0",
|
|
41
|
+
"globals": "^16.4.0",
|
|
42
|
+
"jsdom": "27",
|
|
43
|
+
"rollup": "^4.52.4"
|
|
40
44
|
}
|
|
41
|
-
}
|
|
45
|
+
}
|
package/src/phisheye/phisheye.js
CHANGED
|
@@ -1,69 +1,115 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Based on the archived d3 fisheye plugin, modernized & hardened.
|
|
2
|
+
// - Works in screen/pixel space (set xscale/yscale).
|
|
3
|
+
// - O(1) math with precomputed constants; avoids unnecessary sqrt.
|
|
4
|
+
// - Stable API: .radius(), .distortion(), .focus(), .scales()
|
|
5
|
+
// - Returns { x, y, z } where z ~= local magnification (clamped).
|
|
6
|
+
// - Adds small ergonomics: .setScales(), .focusFromEvent(), .clampZ()
|
|
3
7
|
|
|
4
8
|
const phisheye = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
scales = {
|
|
59
|
-
xscale: _,
|
|
60
|
-
yscale: __
|
|
61
|
-
};
|
|
62
|
-
return fisheye;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return rescale();
|
|
9
|
+
circular: () => {
|
|
10
|
+
let radius = 200; // pixels
|
|
11
|
+
let distortion = 2; // dimensionless
|
|
12
|
+
let focus = [0, 0]; // [fx, fy] in pixels
|
|
13
|
+
let scales = {}; // { xscale, yscale }
|
|
14
|
+
let k0 = 0, k1 = 0; // precomputed factors
|
|
15
|
+
let radius2 = radius * radius;
|
|
16
|
+
let zClamp = 10; // max z (magnification) for stability
|
|
17
|
+
|
|
18
|
+
function ensureScales() {
|
|
19
|
+
if (!scales || typeof scales.xscale !== "function" || typeof scales.yscale !== "function") {
|
|
20
|
+
throw new Error("phisheye.circular: call .scales(xscale, yscale) before using the fisheye.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function rescale() {
|
|
25
|
+
// Same functional form as the classic plugin:
|
|
26
|
+
// k = ((e^d) / (e^d - 1)) * R * (1 - exp(-d * r / R)) / r
|
|
27
|
+
// where d=distortion, R=radius, r=distance to focus.
|
|
28
|
+
const e = Math.exp(distortion);
|
|
29
|
+
k0 = (e / (e - 1)) * radius; // constant multiplier
|
|
30
|
+
k1 = distortion / radius; // exponent scale
|
|
31
|
+
radius2 = radius * radius;
|
|
32
|
+
return fisheye;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fisheye(d) {
|
|
36
|
+
ensureScales();
|
|
37
|
+
|
|
38
|
+
const x0 = scales.xscale(d.x);
|
|
39
|
+
const y0 = scales.yscale(d.y);
|
|
40
|
+
|
|
41
|
+
const dx = x0 - focus[0];
|
|
42
|
+
const dy = y0 - focus[1];
|
|
43
|
+
const dd2 = dx * dx + dy * dy;
|
|
44
|
+
|
|
45
|
+
// At the focus or outside radius -> identity mapping, z ~ 1
|
|
46
|
+
if (dd2 === 0 || dd2 >= radius2) {
|
|
47
|
+
return { x: x0, y: y0, z: dd2 >= radius2 ? 1 : zClamp };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dd = Math.sqrt(dd2);
|
|
51
|
+
// Classic mapping (with a slight blend for gentler core)
|
|
52
|
+
const k = ((k0 * (1 - Math.exp(-dd * k1))) / dd) * 0.75 + 0.25;
|
|
53
|
+
|
|
54
|
+
// New position
|
|
55
|
+
const x = focus[0] + dx * k;
|
|
56
|
+
const y = focus[1] + dy * k;
|
|
57
|
+
|
|
58
|
+
// Local magnification proxy (clamped)
|
|
59
|
+
const z = Math.min(k, zClamp);
|
|
60
|
+
|
|
61
|
+
return { x, y, z };
|
|
66
62
|
}
|
|
67
|
-
}
|
|
68
63
|
|
|
69
|
-
|
|
64
|
+
// --- Public API ---
|
|
65
|
+
|
|
66
|
+
fisheye.radius = function(_) {
|
|
67
|
+
if (!arguments.length) return radius;
|
|
68
|
+
radius = Math.max(0, +_);
|
|
69
|
+
return rescale();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
fisheye.distortion = function(_) {
|
|
73
|
+
if (!arguments.length) return distortion;
|
|
74
|
+
distortion = Math.max(0, +_);
|
|
75
|
+
return rescale();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
fisheye.focus = function(_) {
|
|
79
|
+
if (!arguments.length) return focus.slice();
|
|
80
|
+
if (!Array.isArray(_) || _.length !== 2) throw new Error("focus expects [x, y] in pixels.");
|
|
81
|
+
focus = [+_[0], +_[1]];
|
|
82
|
+
return fisheye;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
fisheye.scales = function(xscale, yscale) {
|
|
86
|
+
if (!arguments.length) return scales;
|
|
87
|
+
scales = { xscale, yscale };
|
|
88
|
+
return fisheye;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Convenience alias
|
|
92
|
+
fisheye.setScales = fisheye.scales;
|
|
93
|
+
|
|
94
|
+
// Clamp maximum z (magnification) returned; defaults to 10
|
|
95
|
+
fisheye.clampZ = function(_) {
|
|
96
|
+
if (!arguments.length) return zClamp;
|
|
97
|
+
zClamp = Math.max(1, +_);
|
|
98
|
+
return fisheye;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Convenience: set focus from a pointer event relative to an HTMLElement/SVG
|
|
102
|
+
// Example: svg.on("pointermove", (e) => fe.focusFromEvent(e, svg.node()))
|
|
103
|
+
fisheye.focusFromEvent = function(event, element) {
|
|
104
|
+
const rect = element.getBoundingClientRect();
|
|
105
|
+
const fx = event.clientX - rect.left;
|
|
106
|
+
const fy = event.clientY - rect.top;
|
|
107
|
+
return fisheye.focus([fx, fy]);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return rescale();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default phisheye;
|
|
115
|
+
|
|
@@ -1,22 +1,32 @@
|
|
|
1
|
-
import polarToCartesian from "./polarToCartesian.js"
|
|
1
|
+
import polarToCartesian from "./polarToCartesian.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Describe
|
|
5
|
-
*
|
|
4
|
+
* Describe an SVG arc path between two angles (radians).
|
|
5
|
+
* Always draws the MINOR arc (≤ π). Angles are normalized to [0, 2π).
|
|
6
6
|
*/
|
|
7
|
+
function normalize(theta) {
|
|
8
|
+
const t = theta % (2 * Math.PI);
|
|
9
|
+
return t < 0 ? t + 2 * Math.PI : t;
|
|
10
|
+
}
|
|
7
11
|
|
|
8
|
-
export default function (
|
|
12
|
+
export default function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
13
|
+
let a0 = normalize(startAngle);
|
|
14
|
+
let a1 = normalize(endAngle);
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
// Choose the minor arc direction
|
|
17
|
+
let diff = a1 - a0;
|
|
18
|
+
if (diff <= -Math.PI) diff += 2 * Math.PI;
|
|
19
|
+
else if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
const largeArcFlag = Math.abs(diff) > Math.PI ? 1 : 0; // for completeness (will be 0 with minor arc)
|
|
22
|
+
const sweepFlag = diff >= 0 ? 1 : 0;
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
const start = polarToCartesian(cx, cy, radius, a0);
|
|
25
|
+
const end = polarToCartesian(cx, cy, radius, a0 + diff);
|
|
26
|
+
|
|
27
|
+
return [
|
|
17
28
|
"M", start.x, start.y,
|
|
18
|
-
"A", radius, radius, 0,
|
|
29
|
+
"A", radius, radius, 0, largeArcFlag, sweepFlag, end.x, end.y
|
|
19
30
|
].join(" ");
|
|
31
|
+
}
|
|
20
32
|
|
|
21
|
-
return d;
|
|
22
|
-
}
|
package/src/radial/getArcs.js
CHANGED
|
@@ -1,61 +1,52 @@
|
|
|
1
|
-
import reflectAngle from "./reflectAngle.js"
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* arc
|
|
2
|
+
* Build *per-edge* minor arcs for a radial tree.
|
|
3
|
+
* For each non-root node (child), we draw a small arc at the *parent's* radius
|
|
4
|
+
* from the parent's angle to the child's angle (minor arc ≤ π).
|
|
5
|
+
*
|
|
6
|
+
* Input: a "radialData" array with fields:
|
|
7
|
+
* thisId, parentId, angle (radians), r (radius at this node)
|
|
8
|
+
*
|
|
9
|
+
* Output: [{ parentId, childId, radius, start, end }]
|
|
10
|
+
* where start/end are angles (radians), radius is parent's r
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
13
|
+
function normalize(theta) {
|
|
14
|
+
const t = theta % (2 * Math.PI);
|
|
15
|
+
return t < 0 ? t + 2 * Math.PI : t;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function minorArc(a, b) {
|
|
19
|
+
// returns [start, end] representing the minor arc from a -> b
|
|
20
|
+
a = normalize(a); b = normalize(b);
|
|
21
|
+
let diff = b - a;
|
|
22
|
+
if (diff <= -Math.PI) diff += 2 * Math.PI;
|
|
23
|
+
else if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
24
|
+
return [a, a + diff]; // draw from a to a+diff
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function getArcs(pd) {
|
|
28
|
+
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
29
|
+
// Find root thisId
|
|
30
|
+
const root = pd.find(d => d.parentId == null)?.thisId;
|
|
31
|
+
|
|
32
|
+
const arcs = [];
|
|
33
|
+
for (const d of pd) {
|
|
34
|
+
if (d.thisId === root) continue;
|
|
35
|
+
const parent = byId.get(d.parentId);
|
|
36
|
+
if (!parent) continue;
|
|
37
|
+
|
|
38
|
+
// minor arc from parent.angle -> child.angle at radius parent.r
|
|
39
|
+
const [start, end] = minorArc(parent.angle, d.angle);
|
|
40
|
+
|
|
41
|
+
if (!isFinite(parent.r) || parent.r === 0 || start === end) continue;
|
|
42
|
+
arcs.push({
|
|
43
|
+
parentId: parent.thisId,
|
|
44
|
+
childId: d.thisId,
|
|
45
|
+
radius: parent.r,
|
|
46
|
+
start,
|
|
47
|
+
end
|
|
48
|
+
});
|
|
37
49
|
}
|
|
50
|
+
return arcs;
|
|
51
|
+
}
|
|
38
52
|
|
|
39
|
-
for (let i = 0; i < pd.length; i++) {
|
|
40
|
-
if (pd[i].thisId !== root) {
|
|
41
|
-
data.push({
|
|
42
|
-
'start': reflectAngle(Math.min(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
43
|
-
'end': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
44
|
-
'radius': parentRadius(pd[i].parentId),
|
|
45
|
-
'thisId': pd[i].thisId,
|
|
46
|
-
'parentId': pd[i].parentId
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// TODO: understand why this works (and it may not in every case)... test!
|
|
52
|
-
for (let i = 0; i < data.length; i++) {
|
|
53
|
-
if (Math.sign(data[i].start) !== Math.sign(data[i].end)) {
|
|
54
|
-
data[i].end = Math.abs(data[i].end);
|
|
55
|
-
data[i].start = -Math.abs(data[i].start);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return data.filter(d => d.start !== d.end & d.radius !== 0);
|
|
60
|
-
|
|
61
|
-
}
|
package/src/radial/getRadii.js
CHANGED
|
@@ -1,43 +1,37 @@
|
|
|
1
|
-
import radialData from "./radialData.js"
|
|
1
|
+
import radialData from "./radialData.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Per-edge radial segments (for highlighting and drawing).
|
|
5
|
+
* For each non-root node, draw a radial line from the parent radius to the child radius
|
|
6
|
+
* at the CHILD'S angle.
|
|
7
|
+
*
|
|
8
|
+
* Output: [{ parentId, childId, x0, y0, x1, y1, isTip }]
|
|
5
9
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
export default function getRadii(node) {
|
|
11
|
+
const data = radialData(node);
|
|
12
|
+
const byId = new Map(data.map(d => [d.thisId, d]));
|
|
13
|
+
const root = data.find(d => d.parentId == null)?.thisId;
|
|
14
|
+
|
|
15
|
+
const segments = [];
|
|
16
|
+
for (const d of data) {
|
|
17
|
+
if (d.thisId === root) continue;
|
|
18
|
+
const parent = byId.get(d.parentId);
|
|
19
|
+
if (!parent) continue;
|
|
20
|
+
|
|
21
|
+
const theta = d.angle;
|
|
22
|
+
const r0 = parent.r;
|
|
23
|
+
const r1 = d.r;
|
|
24
|
+
|
|
25
|
+
segments.push({
|
|
26
|
+
parentId: parent.thisId,
|
|
27
|
+
childId: d.thisId,
|
|
28
|
+
x0: r0 * Math.cos(theta),
|
|
29
|
+
y0: r0 * Math.sin(theta),
|
|
30
|
+
x1: r1 * Math.cos(theta),
|
|
31
|
+
y1: r1 * Math.sin(theta),
|
|
32
|
+
isTip: !!d.isTip
|
|
33
|
+
});
|
|
18
34
|
}
|
|
35
|
+
return segments;
|
|
36
|
+
}
|
|
19
37
|
|
|
20
|
-
var arcs = [];
|
|
21
|
-
// find root
|
|
22
|
-
var root = data.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
23
|
-
|
|
24
|
-
for (var i = 0; i < data.length; i++) {
|
|
25
|
-
if (data[i].thisId !== root) {
|
|
26
|
-
|
|
27
|
-
arcs.push({
|
|
28
|
-
'thisId': data[i].thisId,
|
|
29
|
-
'thisLabel': data[i].thisLabel,
|
|
30
|
-
'x0': data[i].x,
|
|
31
|
-
// radius of the parent * cos(angle)
|
|
32
|
-
'x1': getRadius(data[i]) * Math.cos(data[i].angle),
|
|
33
|
-
'y0': data[i].y,
|
|
34
|
-
// radius of the parent * sin(angle)
|
|
35
|
-
'y1': getRadius(data[i]) * Math.sin(data[i].angle),
|
|
36
|
-
'isTip': data[i].isTip
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return arcs;
|
|
42
|
-
|
|
43
|
-
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert polar
|
|
3
|
-
* https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
|
|
2
|
+
* Convert polar to Cartesian: angle in radians, 0 at +x, CCW positive.
|
|
4
3
|
*/
|
|
5
|
-
|
|
6
|
-
export default function (centerX, centerY, radius, angleInRadians) {
|
|
4
|
+
export default function polarToCartesian(cx, cy, r, angle) {
|
|
7
5
|
return {
|
|
8
|
-
x:
|
|
9
|
-
y:
|
|
6
|
+
x: cx + r * Math.cos(angle),
|
|
7
|
+
y: cy + r * Math.sin(angle)
|
|
10
8
|
};
|
|
11
|
-
}
|
|
9
|
+
}
|