@euphrasiologist/lwphylo 1.1.13 → 1.1.14

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 CHANGED
@@ -1,2 +1,2 @@
1
- const n={circular:()=>{let n=200,t=2,e=[0,0],r={},i=0,s=0,c=n*n,a=10;function o(){const e=Math.exp(t);return i=e/(e-1)*n,s=t/n,c=n*n,l}function l(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),l=t-e[0],h=o-e[1],d=l*l+h*h;if(0===d||d>=c)return{x:t,y:o,z:d>=c?1:a};const u=Math.sqrt(d),f=i*(1-Math.exp(-u*s))/u*.75+.25;return{x:e[0]+l*f,y:e[1]+h*f,z:Math.min(f,a)}}return l.radius=function(t){return arguments.length?(n=Math.max(0,+t),o()):n},l.distortion=function(n){return arguments.length?(t=Math.max(0,+n),o()):t},l.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]],l},l.scales=function(n,t){return arguments.length?(r={xscale:n,yscale:t},l):r},l.setScales=l.scales,l.clampZ=function(n){return arguments.length?(a=Math.max(1,+n),l):a},l.focusFromEvent=function(n,t){const e=t.getBoundingClientRect(),r=n.clientX-e.left,i=n.clientY-e.top;return l.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,e,r,i,s){const c=2*Math.PI,a=n=>(n%c+c)%c;let o=a(i),l=a(s),h=l-o;h<0&&(h+=c);const d=h>Math.PI?1:0,u=t(n,e,r,o),f=t(n,e,r,l);return["M",u.x,u.y,"A",r,r,0,d,1,f.x,f.y].join(" ")}function r(n,t=[]){t.push(n);for(let e=0;e<(n.children?.length||0);e++)t=r(n.children[e],t);return t}function i(n,t=!0){var e=[];for(const t of r(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 s(n){const t=2*Math.PI,e=i(n,!0),r=new Map(e.map(n=>[n.thisId,n])),s=new Map(e.map(n=>[n.thisId,n.children||[]]));let c=null;for(const n of e)if(null==n.parentId){c=n.thisId;break}const a=[];!function n(t){const e=s.get(t)||[];if(0!==e.length)for(const t of e)n(t);else a.push(t)}(c);const o=Math.max(1,a.length),l=new Map;a.forEach((n,e)=>{l.set(n,e/o*t)}),function n(e){const r=s.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=l.get(t);n+=Math.cos(e),i+=Math.sin(e)}l.set(e,(Math.atan2(i,n)%t+t)%t)}}(c);const h=new Map;h.set(c,0),function n(t){const e=s.get(t)||[],i=h.get(t)||0;for(const t of e){const e=r.get(t),s=e?.branchLength??0;h.set(t,i+s),n(t)}}(c);for(const n of e){const t=l.get(n.thisId)??0,e=h.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 c(n){const t={};return t.data=s(n),t.radii=function(n){const t=s(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 s=n.angle,c=t.r,a=n.r;i.push({parentId:t.thisId,childId:n.thisId,x0:c*Math.cos(s),y0:c*Math.sin(s),x1:a*Math.cos(s),y1:a*Math.sin(s),isTip:!!n.isTip})}return i}(n),t.arcs=function(n){const t=2*Math.PI;new Map(n.map(n=>[n.thisId,n]));const e=new Map;let r=null;for(const t of n)null==t.parentId&&(r=t.thisId),e.has(t.parentId)||e.set(t.parentId,[]),e.get(t.parentId).push(t);const i=[];for(const s of n){const n=s.thisId;if(n===r)continue;const c=e.get(n)||[];if(c.length<2)continue;const a=c.map(n=>(n.angle%t+t)%t).sort((n,t)=>n-t),o=a[0],l=a[a.length-1],h=l-o,d=t-h;let u,f,p;h<=d?(u=o,f=l,p=h):(u=l,f=o,p=d),p<1e-6||!isFinite(s.r)||s.r<=0||i.push({start:u,end:f,radius:s.r,thisId:n,parentId:s.parentId})}return i}(t.data),t}function a(n){const t=i(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 s=new Map(r.map((n,t)=>[n,t+1]));return function n(r){const i=e.get(r.id);if(!r.children||0===r.children.length){const n=s.get(r.id);return t[i].y0=n,t[i].y1=n,n}const c=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[i].y0=c,t[i].y1=c,c}(n),function n(r,i){const s=e.get(r.id),c=i??0,a=c+(t[s].branchLength??0);t[s].x0=c,t[s].x1=a,r.children&&r.children.length&&r.children.forEach(t=>n(t,a))}(n,0),t.map(n=>{const{y:t,x:e,angle:r,...i}=n;return i})}function o(n){const t=a(n),e=function(n){const t=a(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),s=Math.max(...e),c=t[0].x0;r.push({parentId:n,x0:c,x1:c,y0:i,y1:s,heights:s-i})}return r}(n),r=function(n){const t=a(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,s=n.y0,c=t.y0,a=Math.min(s,c),o=Math.max(s,c);r.push({parentId:n.parentId,childId:n.thisId,x:i,y0:a,y1:o})}return r}(n);new Map(t.map(n=>[n.thisId,n]));const i=t.filter(n=>null!=n.parentId).map(n=>({parentId:n.parentId,childId:n.thisId,thisId:n.thisId,thisLabel:n.thisLabel,isTip:n.isTip,x0:n.x0,x1:n.x1,y0:n.y0,y1:n.y0}));return{data:t,vertical_lines:e,child_vertical_lines:r,horizontal_lines:i}}function l(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 s=r.angle*Math.PI,c=r.branchLength??0;r.x=n.x+c*Math.sin(s),r.y=n.y+c*Math.cos(s),l(r)}return n}function h(n){var t={},e=i(l(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 d(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 u(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 f(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 s=[["data",Object.values(r)]],c=s[0][1].map(n=>n.thisId);var a=[];for(const t in n)"data"!==t&&a.push([t,n[t]]);var o=a.map(n=>[n[0],n[1].filter(n=>c.includes(n.thisId))]);return Object.fromEntries(s.concat(o))}export{e as describeArc,d as parentFisheye,n as phisheye,c as radialLayout,u as readTree,o as rectangleLayout,f as subTree,h as unrooted};
1
+ const n={circular:()=>{let n=200,t=2,e=[0,0],r={},i=0,s=0,c=n*n,a=10;function o(){const e=Math.exp(t);return i=e/(e-1)*n,s=t/n,c=n*n,l}function l(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),l=t-e[0],h=o-e[1],d=l*l+h*h;if(0===d||d>=c)return{x:t,y:o,z:d>=c?1:a};const u=Math.sqrt(d),f=i*(1-Math.exp(-u*s))/u*.75+.25;return{x:e[0]+l*f,y:e[1]+h*f,z:Math.min(f,a)}}return l.radius=function(t){return arguments.length?(n=Math.max(0,+t),o()):n},l.distortion=function(n){return arguments.length?(t=Math.max(0,+n),o()):t},l.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]],l},l.scales=function(n,t){return arguments.length?(r={xscale:n,yscale:t},l):r},l.setScales=l.scales,l.clampZ=function(n){return arguments.length?(a=Math.max(1,+n),l):a},l.focusFromEvent=function(n,t){const e=t.getBoundingClientRect(),r=n.clientX-e.left,i=n.clientY-e.top;return l.focus([r,i])},o()}};function t(n,e,r,i,s){const c=2*Math.PI,a=n=>(n%c+c)%c,o=a(i),l=a(s),h=(l-o+c)%c,d=(o-l+c)%c,u=d<h?1:0,f=Math.min(h,d),p=f>Math.PI?1:0;if(f<1e-9){const i=t(n,e,r,o);return`M ${i.x} ${i.y}`}const I=t(n,e,r,o),g=t(n,e,r,l);return`M ${I.x} ${I.y} A ${r} ${r} 0 ${p} ${u} ${g.x} ${g.y}`}function e(n,e,r,i,s){const c=2*Math.PI,a=n=>(n%c+c)%c;let o=a(i),l=a(s),h=l-o;h<0&&(h+=c);const d=h>Math.PI?1:0,u=t(n,e,r,o),f=t(n,e,r,l);return["M",u.x,u.y,"A",r,r,0,d,1,f.x,f.y].join(" ")}function r(n,t=[]){t.push(n);for(let e=0;e<(n.children?.length||0);e++)t=r(n.children[e],t);return t}function i(n,t=!0){var e=[];for(const t of r(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 s(n){const t=2*Math.PI,e=i(n,!0),r=new Map(e.map(n=>[n.thisId,n])),s=new Map(e.map(n=>[n.thisId,n.children||[]]));let c=null;for(const n of e)if(null==n.parentId){c=n.thisId;break}const a=[];!function n(t){const e=s.get(t)||[];if(0!==e.length)for(const t of e)n(t);else a.push(t)}(c);const o=Math.max(1,a.length),l=new Map;a.forEach((n,e)=>{l.set(n,e/o*t)}),function n(e){const r=s.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=l.get(t);n+=Math.cos(e),i+=Math.sin(e)}l.set(e,(Math.atan2(i,n)%t+t)%t)}}(c);const h=new Map;h.set(c,0),function n(t){const e=s.get(t)||[],i=h.get(t)||0;for(const t of e){const e=r.get(t),s=e?.branchLength??0;h.set(t,i+s),n(t)}}(c);for(const n of e){const t=l.get(n.thisId)??0,e=h.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 c(n){const t={};return t.data=s(n),t.radii=function(n){const t=s(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 s=n.angle,c=t.r,a=n.r;i.push({parentId:t.thisId,childId:n.thisId,x0:c*Math.cos(s),y0:c*Math.sin(s),x1:a*Math.cos(s),y1:a*Math.sin(s),isTip:!!n.isTip})}return i}(n),t.arcs=function(n){const t=2*Math.PI;new Map(n.map(n=>[n.thisId,n]));const e=new Map;let r=null;for(const t of n)null==t.parentId&&(r=t.thisId),e.has(t.parentId)||e.set(t.parentId,[]),e.get(t.parentId).push(t);const i=[];for(const s of n){const n=s.thisId;if(n===r)continue;const c=e.get(n)||[];if(c.length<2)continue;const a=c.map(n=>(n.angle%t+t)%t).sort((n,t)=>n-t),o=a[0],l=a[a.length-1],h=l-o,d=t-h;let u,f,p;h<=d?(u=o,f=l,p=h):(u=l,f=o,p=d),p<1e-6||!isFinite(s.r)||s.r<=0||i.push({start:u,end:f,radius:s.r,thisId:n,parentId:s.parentId})}return i}(t.data),t}function a(n){const t=i(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 s=new Map(r.map((n,t)=>[n,t+1]));return function n(r){const i=e.get(r.id);if(!r.children||0===r.children.length){const n=s.get(r.id);return t[i].y0=n,t[i].y1=n,n}const c=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[i].y0=c,t[i].y1=c,c}(n),function n(r,i){const s=e.get(r.id),c=i??0,a=c+(t[s].branchLength??0);t[s].x0=c,t[s].x1=a,r.children&&r.children.length&&r.children.forEach(t=>n(t,a))}(n,0),t.map(n=>{const{y:t,x:e,angle:r,...i}=n;return i})}function o(n){const t=a(n),e=function(n){const t=a(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),s=Math.max(...e),c=t[0].x0;r.push({parentId:n,x0:c,x1:c,y0:i,y1:s,heights:s-i})}return r}(n),r=function(n){const t=a(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,s=n.y0,c=t.y0,a=Math.min(s,c),o=Math.max(s,c);r.push({parentId:n.parentId,childId:n.thisId,x:i,y0:a,y1:o})}return r}(n);new Map(t.map(n=>[n.thisId,n]));const i=t.filter(n=>null!=n.parentId).map(n=>({parentId:n.parentId,childId:n.thisId,thisId:n.thisId,thisLabel:n.thisLabel,isTip:n.isTip,x0:n.x0,x1:n.x1,y0:n.y0,y1:n.y0}));return{data:t,vertical_lines:e,child_vertical_lines:r,horizontal_lines:i}}function l(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 s=r.angle*Math.PI,c=r.branchLength??0;r.x=n.x+c*Math.sin(s),r.y=n.y+c*Math.cos(s),l(r)}return n}function h(n){var t={},e=i(l(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 d(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 u(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 f(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 s=[["data",Object.values(r)]],c=s[0][1].map(n=>n.thisId);var a=[];for(const t in n)"data"!==t&&a.push([t,n[t]]);var o=a.map(n=>[n[0],n[1].filter(n=>c.includes(n.thisId))]);return Object.fromEntries(s.concat(o))}export{e as describeArc,d as parentFisheye,n as phisheye,c as radialLayout,u as readTree,o as rectangleLayout,f as subTree,h as unrooted};
2
2
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@euphrasiologist/lwphylo",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
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",
@@ -1,7 +1,37 @@
1
- export default function (centerX, centerY, radius, angleInRadians) {
2
- return {
3
- x: centerX + radius * Math.cos(angleInRadians),
4
- y: centerY - radius * Math.sin(angleInRadians)
5
- };
1
+ import polarToCartesian from "./polarToCartesian.js";
2
+
3
+ /**
4
+ * Draw the shortest arc between startAngle and endAngle on a circle.
5
+ * - Angles are radians.
6
+ * - Works with Y-inverted screen coords (our polarToCartesian already inverts Y).
7
+ * - Automatically picks CW or CCW sweep to follow the *shorter* path.
8
+ */
9
+ export default function describeArc(cx, cy, radius, startAngle, endAngle) {
10
+ const TAU = Math.PI * 2;
11
+ const norm = (t) => ((t % TAU) + TAU) % TAU;
12
+
13
+ const a0 = norm(startAngle);
14
+ const a1 = norm(endAngle);
15
+
16
+ // CCW delta (a0 -> a1), and CW delta (a0 -> a1 going the other way)
17
+ const ccw = (a1 - a0 + TAU) % TAU;
18
+ const cw = (a0 - a1 + TAU) % TAU;
19
+
20
+ // Choose the shorter sweep
21
+ const sweepFlag = cw < ccw ? 1 : 0; // 1 = CW, 0 = CCW (SVG spec)
22
+ const delta = Math.min(ccw, cw);
23
+ const largeArcFlag = delta > Math.PI ? 1 : 0;
24
+
25
+ // If degenerate, just move to the point
26
+ if (delta < 1e-9) {
27
+ const p = polarToCartesian(cx, cy, radius, a0);
28
+ return `M ${p.x} ${p.y}`;
29
+ }
30
+
31
+ // Use the original angles; sweepFlag picks direction
32
+ const p0 = polarToCartesian(cx, cy, radius, a0);
33
+ const p1 = polarToCartesian(cx, cy, radius, a1);
34
+
35
+ return `M ${p0.x} ${p0.y} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${p1.x} ${p1.y}`;
6
36
  }
7
37