@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 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.5",
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 && /Users/mb39/homebrew/bin/yarn test"
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": "6",
37
- "jsdom": "15",
38
- "rollup": "1",
39
- "rollup-plugin-terser": "5"
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
+ }
@@ -1,69 +1,115 @@
1
- // based on https://github.com/d3/d3-plugins/blob/master/fisheye/fisheye.js
2
- // now archived d3 code?
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
- circular: () => {
6
- var radius = 200,
7
- distortion = 2,
8
- k0,
9
- k1,
10
- focus = [0, 0],
11
- scales = {};
12
-
13
- function fisheye(d) {
14
- var dx = scales.xscale(d.x) - focus[0],
15
- dy = scales.yscale(d.y) - focus[1],
16
- dd = Math.sqrt(dx * dx + dy * dy);
17
- if (!dd || dd >= radius)
18
- return {
19
- x: scales.xscale(d.x),
20
- y: scales.yscale(d.y),
21
- z: dd >= radius ? 1 : 10
22
- };
23
- var k = ((k0 * (1 - Math.exp(-dd * k1))) / dd) * .75 + .25;
24
- return {
25
- x: focus[0] + dx * k,
26
- y: focus[1] + dy * k,
27
- z: Math.min(k, 10)
28
- };
29
- }
30
-
31
- function rescale() {
32
- k0 = Math.exp(distortion);
33
- k0 = (k0 / (k0 - 1)) * radius;
34
- k1 = distortion / radius;
35
- return fisheye;
36
- }
37
-
38
- fisheye.radius = function (_) {
39
- if (!arguments.length) return radius;
40
- radius = +_;
41
- return rescale();
42
- };
43
-
44
- fisheye.distortion = function (_) {
45
- if (!arguments.length) return distortion;
46
- distortion = +_;
47
- return rescale();
48
- };
49
-
50
- fisheye.focus = function (_) {
51
- if (!arguments.length) return focus;
52
- focus = _;
53
- return fisheye;
54
- };
55
-
56
- fisheye.scales = function (_, __) {
57
- if (!arguments.length) return scales;
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
- export default phisheye
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 the arc to draw
5
- * https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
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 (x, y, radius, startAngle, endAngle) {
12
+ export default function describeArc(cx, cy, radius, startAngle, endAngle) {
13
+ let a0 = normalize(startAngle);
14
+ let a1 = normalize(endAngle);
9
15
 
10
- var start = polarToCartesian(x, y, radius, startAngle);
11
- var end = polarToCartesian(x, y, radius, endAngle);
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
- // TODO: refers to middle zero below... but no large arcs needed..?
14
- // var largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
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
- var d = [
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, 0, 0, end.x, end.y
29
+ "A", radius, radius, 0, largeArcFlag, sweepFlag, end.x, end.y
19
30
  ].join(" ");
31
+ }
20
32
 
21
- return d;
22
- }
@@ -1,61 +1,52 @@
1
- import reflectAngle from "./reflectAngle.js"
2
-
3
1
  /**
4
- * Takes parent data and returns a start value (radians),
5
- * end value (radians), and radius of circle to draw an
6
- * arc from
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
- export default function (pd) {
10
- // must return start of arc and end
11
- // these are pairs of edges that have the same parent
12
- // start is the min(angle) of the children and end is the max(angle)
13
- // radius is the radius of the parent
14
- // origin is 0, 0
15
-
16
- var data = [];
17
- var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
18
-
19
-
20
- // get the branchlength of the parentID
21
- function sisterAngle(current_parentId) {
22
- for (var i = 0; i < pd.length; i++) {
23
- if (pd[i].parentId === current_parentId) {
24
- var sister_angle = pd[i].angle;
25
- }
26
- }
27
- return sister_angle;
28
- }
29
-
30
- function parentRadius(current_parentId) {
31
- for (var i = 0; i < pd.length; i++) {
32
- if (pd[i].thisId === current_parentId) {
33
- var parent_r = pd[i].r;
34
- }
35
- }
36
- return parent_r;
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
- }
@@ -1,43 +1,37 @@
1
- import radialData from "./radialData.js"
1
+ import radialData from "./radialData.js";
2
2
 
3
3
  /**
4
- * Take a parsed tree and get the radii of each of the nodes.
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
- export default function (node) {
8
- var data = radialData(node);
9
-
10
- // for the current iteration of the loop find the matching parentId
11
- function getRadius(current_node) {
12
- for (var i = 0; i < data.length; i++) {
13
- if (data[i].thisId === current_node.parentId) {
14
- var radius = data[i].r;
15
- }
16
- }
17
- return radius;
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 co-ordinates to cartesian
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: centerX + (radius * Math.cos(angleInRadians)),
9
- y: centerY + (radius * Math.sin(angleInRadians))
6
+ x: cx + r * Math.cos(angle),
7
+ y: cy + r * Math.sin(angle)
10
8
  };
11
- }
9
+ }