@graphty/graph-io 0.3.48 → 0.3.50

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.
Files changed (600) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-BRXsll0T.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-BRXsll0T.js.map} +1 -1
  3. package/dist/chunks/bends-BGXjv_U4.js +129 -0
  4. package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
  5. package/dist/chunks/{block-Cu22ewuS.js → block-yl0f_qLO.js} +931 -164
  6. package/dist/chunks/block-yl0f_qLO.js.map +1 -0
  7. package/dist/chunks/choose-CczZJaX_.js +717 -0
  8. package/dist/chunks/choose-CczZJaX_.js.map +1 -0
  9. package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/{context-cgmmOxZ2.js → context-DizmppHy.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-DizmppHy.js.map} +1 -1
  15. package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
  16. package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
  17. package/dist/chunks/{credentials-BiQsnMTb.js → credentials-CmvJXYeH.js} +67 -16
  18. package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
  19. package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
  20. package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-Dhez_Cew.js} +11 -2
  24. package/dist/chunks/descriptor-Dhez_Cew.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-7Jkpg0yd.js} +1788 -133
  26. package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
  27. package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
  28. package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
  29. package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
  30. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  31. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-1Ajud15J.js} +9 -8
  32. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-1Ajud15J.js.map} +1 -1
  33. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-B6yvL3wy.js} +7 -7
  34. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-B6yvL3wy.js.map} +1 -1
  35. package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  36. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-CA5q8ROP.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-CA5q8ROP.js.map} +1 -1
  39. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-CQQZFJ2v.js} +1284 -156
  40. package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
  41. package/dist/chunks/format-BXiAb-1h.js +408 -0
  42. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  43. package/dist/chunks/{generic-CP6YD34u.js → generic-Dc21MAdg.js} +387 -31
  44. package/dist/chunks/generic-Dc21MAdg.js.map +1 -0
  45. package/dist/chunks/{importer-B87_fmVx.js → importer-C6NVpCXp.js} +327 -84
  46. package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
  47. package/dist/chunks/{importer-DPZDN40-.js → importer-CKFe4q54.js} +25 -17
  48. package/dist/chunks/importer-CKFe4q54.js.map +1 -0
  49. package/dist/chunks/{importer-Bji2MeWr.js → importer-CKbUbsJA.js} +13 -10
  50. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-CKbUbsJA.js.map} +1 -1
  51. package/dist/chunks/{importer-CB_zV5_L.js → importer-ClCsdgTJ.js} +208 -1104
  52. package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
  53. package/dist/chunks/{importer-e3huMZbm.js → importer-CyVZTrf0.js} +14 -10
  54. package/dist/chunks/importer-CyVZTrf0.js.map +1 -0
  55. package/dist/chunks/{importer-CVT-JyDE.js → importer-DgG4v1Ms.js} +1537 -355
  56. package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
  57. package/dist/chunks/{importer-Cyy6UseH.js → importer-asFUrIsj.js} +66 -13
  58. package/dist/chunks/importer-asFUrIsj.js.map +1 -0
  59. package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
  60. package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
  61. package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
  62. package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  63. package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-IVRq2Uoo.js} +282 -74
  64. package/dist/chunks/lexicon-IVRq2Uoo.js.map +1 -0
  65. package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
  66. package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  67. package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
  68. package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
  69. package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
  70. package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
  71. package/dist/chunks/round-trip-Cy3_SHRZ.js +79 -0
  72. package/dist/chunks/round-trip-Cy3_SHRZ.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-BXwVbza3.js} +5 -5
  74. package/dist/chunks/sigv4-BXwVbza3.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CAVmBLgp.js} +1237 -138
  76. package/dist/chunks/styles-CAVmBLgp.js.map +1 -0
  77. package/dist/chunks/styles-CPczw1Jz.js +1928 -0
  78. package/dist/chunks/styles-CPczw1Jz.js.map +1 -0
  79. package/dist/chunks/styles-boK6Ln54.js +2847 -0
  80. package/dist/chunks/styles-boK6Ln54.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-XeFBQ6VS.js} +506 -175
  82. package/dist/chunks/styles-yed-XeFBQ6VS.js.map +1 -0
  83. package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
  84. package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
  85. package/dist/csv.js +7 -7
  86. package/dist/cx.js +2 -2
  87. package/dist/cx2.js +2 -2
  88. package/dist/cys.js +1 -1
  89. package/dist/dot.js +8 -4
  90. package/dist/dot.js.map +1 -1
  91. package/dist/gexf.js +42 -12
  92. package/dist/gexf.js.map +1 -1
  93. package/dist/gml.js +848 -194
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +230 -193
  96. package/dist/graph-io.js.map +1 -1
  97. package/dist/graphml.js +3 -3
  98. package/dist/json.js +3 -2
  99. package/dist/neo4j.js +6 -6
  100. package/dist/obo.js +1 -1
  101. package/dist/pajek.js +1 -1
  102. package/dist/positions.js +45 -3
  103. package/dist/positions.js.map +1 -1
  104. package/dist/sources-cypher.js +141 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +32 -6
  108. package/dist/sources-rest.js.map +1 -1
  109. package/dist/sources-sparql.d.ts +1 -0
  110. package/dist/sources-sparql.js +439 -0
  111. package/dist/sources-sparql.js.map +1 -0
  112. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  113. package/dist/src/formats/cx/importer.js +5 -1
  114. package/dist/src/formats/cx/importer.js.map +1 -1
  115. package/dist/src/formats/cx/styles.d.ts +24 -1
  116. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  117. package/dist/src/formats/cx/styles.js +117 -11
  118. package/dist/src/formats/cx/styles.js.map +1 -1
  119. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  120. package/dist/src/formats/cx2/importer.js +1 -0
  121. package/dist/src/formats/cx2/importer.js.map +1 -1
  122. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  123. package/dist/src/formats/cx2/styles.js +3 -1
  124. package/dist/src/formats/cx2/styles.js.map +1 -1
  125. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cys/importer.js +1 -0
  127. package/dist/src/formats/cys/importer.js.map +1 -1
  128. package/dist/src/formats/cys/styles.d.ts +2 -0
  129. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cys/styles.js +15 -7
  131. package/dist/src/formats/cys/styles.js.map +1 -1
  132. package/dist/src/formats/dot/drawing.d.ts +98 -0
  133. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  134. package/dist/src/formats/dot/drawing.js +734 -0
  135. package/dist/src/formats/dot/drawing.js.map +1 -0
  136. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  137. package/dist/src/formats/dot/exporter.js +34 -2
  138. package/dist/src/formats/dot/exporter.js.map +1 -1
  139. package/dist/src/formats/dot/importer.d.ts +6 -1
  140. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  141. package/dist/src/formats/dot/importer.js +170 -33
  142. package/dist/src/formats/dot/importer.js.map +1 -1
  143. package/dist/src/formats/dot/index.d.ts +3 -1
  144. package/dist/src/formats/dot/index.d.ts.map +1 -1
  145. package/dist/src/formats/dot/index.js +2 -1
  146. package/dist/src/formats/dot/index.js.map +1 -1
  147. package/dist/src/formats/dot/label-places.d.ts +41 -0
  148. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  149. package/dist/src/formats/dot/label-places.js +185 -0
  150. package/dist/src/formats/dot/label-places.js.map +1 -0
  151. package/dist/src/formats/dot/names.d.ts +13 -0
  152. package/dist/src/formats/dot/names.d.ts.map +1 -1
  153. package/dist/src/formats/dot/names.js +13 -0
  154. package/dist/src/formats/dot/names.js.map +1 -1
  155. package/dist/src/formats/dot/record.d.ts +50 -0
  156. package/dist/src/formats/dot/record.d.ts.map +1 -0
  157. package/dist/src/formats/dot/record.js +275 -0
  158. package/dist/src/formats/dot/record.js.map +1 -0
  159. package/dist/src/formats/dot/styles.d.ts +72 -2
  160. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  161. package/dist/src/formats/dot/styles.js +1500 -119
  162. package/dist/src/formats/dot/styles.js.map +1 -1
  163. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  165. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  167. package/dist/src/formats/gexf/importer.d.ts +7 -0
  168. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  169. package/dist/src/formats/gexf/importer.js +41 -5
  170. package/dist/src/formats/gexf/importer.js.map +1 -1
  171. package/dist/src/formats/gexf/preview.d.ts +77 -0
  172. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  173. package/dist/src/formats/gexf/preview.js +191 -0
  174. package/dist/src/formats/gexf/preview.js.map +1 -0
  175. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  176. package/dist/src/formats/gexf/schema.js +2 -1
  177. package/dist/src/formats/gexf/schema.js.map +1 -1
  178. package/dist/src/formats/gexf/styles.d.ts +58 -31
  179. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  180. package/dist/src/formats/gexf/styles.js +848 -225
  181. package/dist/src/formats/gexf/styles.js.map +1 -1
  182. package/dist/src/formats/gml/importer.d.ts +5 -2
  183. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  184. package/dist/src/formats/gml/importer.js +258 -71
  185. package/dist/src/formats/gml/importer.js.map +1 -1
  186. package/dist/src/formats/gml/index.d.ts +1 -1
  187. package/dist/src/formats/gml/index.d.ts.map +1 -1
  188. package/dist/src/formats/gml/index.js +1 -1
  189. package/dist/src/formats/gml/index.js.map +1 -1
  190. package/dist/src/formats/gml/positions.d.ts +7 -0
  191. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  192. package/dist/src/formats/gml/positions.js +29 -7
  193. package/dist/src/formats/gml/positions.js.map +1 -1
  194. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  195. package/dist/src/formats/gml/styles.js +754 -102
  196. package/dist/src/formats/gml/styles.js.map +1 -1
  197. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.js +6 -10
  199. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  200. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  201. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  202. package/dist/src/formats/graphml/styles-yed.js +695 -164
  203. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  204. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.js +431 -64
  207. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  208. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  209. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  210. package/dist/src/formats/graphml/yfiles.js +16 -6
  211. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  212. package/dist/src/formats/json/dialect.d.ts +3 -2
  213. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  214. package/dist/src/formats/json/dialect.js +39 -5
  215. package/dist/src/formats/json/dialect.js.map +1 -1
  216. package/dist/src/formats/json/importer.d.ts +13 -2
  217. package/dist/src/formats/json/importer.d.ts.map +1 -1
  218. package/dist/src/formats/json/importer.js +42 -6
  219. package/dist/src/formats/json/importer.js.map +1 -1
  220. package/dist/src/formats/json/index.d.ts +1 -1
  221. package/dist/src/formats/json/index.d.ts.map +1 -1
  222. package/dist/src/formats/json/index.js +1 -1
  223. package/dist/src/formats/json/index.js.map +1 -1
  224. package/dist/src/formats/json/positions.d.ts +5 -2
  225. package/dist/src/formats/json/positions.d.ts.map +1 -1
  226. package/dist/src/formats/json/positions.js +20 -4
  227. package/dist/src/formats/json/positions.js.map +1 -1
  228. package/dist/src/formats/json/styles.d.ts +12 -1
  229. package/dist/src/formats/json/styles.d.ts.map +1 -1
  230. package/dist/src/formats/json/styles.js +1843 -136
  231. package/dist/src/formats/json/styles.js.map +1 -1
  232. package/dist/src/formats/pajek/draw.d.ts +104 -0
  233. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  234. package/dist/src/formats/pajek/draw.js +382 -0
  235. package/dist/src/formats/pajek/draw.js.map +1 -0
  236. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  237. package/dist/src/formats/pajek/exporter.js +70 -51
  238. package/dist/src/formats/pajek/exporter.js.map +1 -1
  239. package/dist/src/formats/pajek/importer.d.ts +9 -0
  240. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  241. package/dist/src/formats/pajek/importer.js +233 -102
  242. package/dist/src/formats/pajek/importer.js.map +1 -1
  243. package/dist/src/formats/pajek/index.d.ts +1 -0
  244. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  245. package/dist/src/formats/pajek/index.js.map +1 -1
  246. package/dist/src/formats/pajek/styles.d.ts +15 -7
  247. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  248. package/dist/src/formats/pajek/styles.js +1395 -114
  249. package/dist/src/formats/pajek/styles.js.map +1 -1
  250. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  251. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  252. package/dist/src/formats/pajek/syntax.js +26 -2
  253. package/dist/src/formats/pajek/syntax.js.map +1 -1
  254. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  255. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  256. package/dist/src/formats/xgmml/emit.js +65 -5
  257. package/dist/src/formats/xgmml/emit.js.map +1 -1
  258. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  259. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  260. package/dist/src/formats/xgmml/styles.js +277 -60
  261. package/dist/src/formats/xgmml/styles.js.map +1 -1
  262. package/dist/src/index.d.ts +6 -5
  263. package/dist/src/index.d.ts.map +1 -1
  264. package/dist/src/index.js +6 -5
  265. package/dist/src/index.js.map +1 -1
  266. package/dist/src/positions/convert.d.ts.map +1 -1
  267. package/dist/src/positions/convert.js +11 -1
  268. package/dist/src/positions/convert.js.map +1 -1
  269. package/dist/src/positions/extract.d.ts.map +1 -1
  270. package/dist/src/positions/extract.js +53 -1
  271. package/dist/src/positions/extract.js.map +1 -1
  272. package/dist/src/positions/index.d.ts +1 -1
  273. package/dist/src/positions/index.d.ts.map +1 -1
  274. package/dist/src/positions/types.d.ts +59 -0
  275. package/dist/src/positions/types.d.ts.map +1 -1
  276. package/dist/src/sources/builtins.d.ts +1 -1
  277. package/dist/src/sources/builtins.d.ts.map +1 -1
  278. package/dist/src/sources/builtins.js +8 -2
  279. package/dist/src/sources/builtins.js.map +1 -1
  280. package/dist/src/sources/context.d.ts.map +1 -1
  281. package/dist/src/sources/context.js +1 -0
  282. package/dist/src/sources/context.js.map +1 -1
  283. package/dist/src/sources/credentials.d.ts +50 -1
  284. package/dist/src/sources/credentials.d.ts.map +1 -1
  285. package/dist/src/sources/credentials.js +69 -0
  286. package/dist/src/sources/credentials.js.map +1 -1
  287. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  288. package/dist/src/sources/cypher/descriptor.js +5 -0
  289. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  290. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  291. package/dist/src/sources/cypher/index.js +125 -18
  292. package/dist/src/sources/cypher/index.js.map +1 -1
  293. package/dist/src/sources/cypher/params.d.ts +1 -1
  294. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  295. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  296. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  297. package/dist/src/sources/cypher/query-builder.js +31 -0
  298. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  299. package/dist/src/sources/ids.d.ts +5 -0
  300. package/dist/src/sources/ids.d.ts.map +1 -1
  301. package/dist/src/sources/ids.js +5 -0
  302. package/dist/src/sources/ids.js.map +1 -1
  303. package/dist/src/sources/params.d.ts +3 -0
  304. package/dist/src/sources/params.d.ts.map +1 -1
  305. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  306. package/dist/src/sources/rest/descriptor.js +9 -0
  307. package/dist/src/sources/rest/descriptor.js.map +1 -1
  308. package/dist/src/sources/rest/openalex.d.ts +4 -2
  309. package/dist/src/sources/rest/openalex.d.ts.map +1 -1
  310. package/dist/src/sources/rest/openalex.js +4 -2
  311. package/dist/src/sources/rest/openalex.js.map +1 -1
  312. package/dist/src/sources/rest/source.d.ts.map +1 -1
  313. package/dist/src/sources/rest/source.js +38 -0
  314. package/dist/src/sources/rest/source.js.map +1 -1
  315. package/dist/src/sources/run.d.ts +2 -0
  316. package/dist/src/sources/run.d.ts.map +1 -1
  317. package/dist/src/sources/run.js +30 -3
  318. package/dist/src/sources/run.js.map +1 -1
  319. package/dist/src/sources/sigv4.d.ts +5 -3
  320. package/dist/src/sources/sigv4.d.ts.map +1 -1
  321. package/dist/src/sources/sigv4.js +6 -4
  322. package/dist/src/sources/sigv4.js.map +1 -1
  323. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  324. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  325. package/dist/src/sources/sparql/descriptor.js +64 -0
  326. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  327. package/dist/src/sources/sparql/index.d.ts +28 -0
  328. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  329. package/dist/src/sources/sparql/index.js +327 -0
  330. package/dist/src/sources/sparql/index.js.map +1 -0
  331. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  332. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  333. package/dist/src/sources/sparql/ntriples.js +180 -0
  334. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  335. package/dist/src/sources/sparql/params.d.ts +27 -0
  336. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  337. package/dist/src/sources/sparql/params.js +5 -0
  338. package/dist/src/sources/sparql/params.js.map +1 -0
  339. package/dist/src/sources/sparql/query.d.ts +73 -0
  340. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  341. package/dist/src/sources/sparql/query.js +164 -0
  342. package/dist/src/sources/sparql/query.js.map +1 -0
  343. package/dist/src/sources/types.d.ts +60 -0
  344. package/dist/src/sources/types.d.ts.map +1 -1
  345. package/dist/src/styles/annotations.d.ts +61 -5
  346. package/dist/src/styles/annotations.d.ts.map +1 -1
  347. package/dist/src/styles/annotations.js +130 -22
  348. package/dist/src/styles/annotations.js.map +1 -1
  349. package/dist/src/styles/canon/bends.d.ts +46 -0
  350. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  351. package/dist/src/styles/canon/bends.js +123 -0
  352. package/dist/src/styles/canon/bends.js.map +1 -1
  353. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  354. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  355. package/dist/src/styles/canon/color-tables.js +10 -4
  356. package/dist/src/styles/canon/color-tables.js.map +1 -1
  357. package/dist/src/styles/canon/color.d.ts +3 -1
  358. package/dist/src/styles/canon/color.d.ts.map +1 -1
  359. package/dist/src/styles/canon/color.js +103 -3
  360. package/dist/src/styles/canon/color.js.map +1 -1
  361. package/dist/src/styles/canon/enums.d.ts +2 -2
  362. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  363. package/dist/src/styles/canon/enums.js +89 -7
  364. package/dist/src/styles/canon/enums.js.map +1 -1
  365. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  366. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  367. package/dist/src/styles/canon/graphviz.js +57 -9
  368. package/dist/src/styles/canon/graphviz.js.map +1 -1
  369. package/dist/src/styles/canon/number.d.ts +8 -0
  370. package/dist/src/styles/canon/number.d.ts.map +1 -1
  371. package/dist/src/styles/canon/number.js +18 -0
  372. package/dist/src/styles/canon/number.js.map +1 -1
  373. package/dist/src/styles/canon/text.d.ts +29 -2
  374. package/dist/src/styles/canon/text.d.ts.map +1 -1
  375. package/dist/src/styles/canon/text.js +156 -16
  376. package/dist/src/styles/canon/text.js.map +1 -1
  377. package/dist/src/styles/codes.d.ts +2 -0
  378. package/dist/src/styles/codes.d.ts.map +1 -1
  379. package/dist/src/styles/codes.js +2 -0
  380. package/dist/src/styles/codes.js.map +1 -1
  381. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  382. package/dist/src/styles/cyjs/cascade.js +1139 -79
  383. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  384. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  385. package/dist/src/styles/cyjs/defaults-table.js +4 -0
  386. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  387. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  388. package/dist/src/styles/cyjs/emit.js +232 -10
  389. package/dist/src/styles/cyjs/emit.js.map +1 -1
  390. package/dist/src/styles/cyjs/face.d.ts +20 -0
  391. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  392. package/dist/src/styles/cyjs/face.js +332 -0
  393. package/dist/src/styles/cyjs/face.js.map +1 -0
  394. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  395. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  396. package/dist/src/styles/cyjs/mappers.js +475 -26
  397. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  398. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  399. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  400. package/dist/src/styles/cyjs/routes.js +360 -0
  401. package/dist/src/styles/cyjs/routes.js.map +1 -0
  402. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  403. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  404. package/dist/src/styles/cytoscape/charts.js +560 -0
  405. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  406. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  407. package/dist/src/styles/cytoscape/emit.js +156 -48
  408. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  409. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  410. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  411. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  412. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  413. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  414. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  415. package/dist/src/styles/cytoscape/mappings.js +92 -0
  416. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  417. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  418. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  419. package/dist/src/styles/cytoscape/resolve.js +623 -17
  420. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  421. package/dist/src/styles/defaults.d.ts +46 -26
  422. package/dist/src/styles/defaults.d.ts.map +1 -1
  423. package/dist/src/styles/defaults.js +308 -71
  424. package/dist/src/styles/defaults.js.map +1 -1
  425. package/dist/src/styles/index.d.ts +4 -3
  426. package/dist/src/styles/index.d.ts.map +1 -1
  427. package/dist/src/styles/index.js +3 -2
  428. package/dist/src/styles/index.js.map +1 -1
  429. package/dist/src/styles/internal.d.ts +1 -0
  430. package/dist/src/styles/internal.d.ts.map +1 -1
  431. package/dist/src/styles/internal.js +1 -0
  432. package/dist/src/styles/internal.js.map +1 -1
  433. package/dist/src/styles/io/generic.d.ts.map +1 -1
  434. package/dist/src/styles/io/generic.js +56 -16
  435. package/dist/src/styles/io/generic.js.map +1 -1
  436. package/dist/src/styles/io/round-trip.d.ts +56 -0
  437. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  438. package/dist/src/styles/io/round-trip.js +138 -0
  439. package/dist/src/styles/io/round-trip.js.map +1 -0
  440. package/dist/src/styles/io/writer.d.ts.map +1 -1
  441. package/dist/src/styles/io/writer.js +6 -1
  442. package/dist/src/styles/io/writer.js.map +1 -1
  443. package/dist/src/styles/notes.d.ts.map +1 -1
  444. package/dist/src/styles/notes.js +1 -0
  445. package/dist/src/styles/notes.js.map +1 -1
  446. package/dist/src/styles/plan/features.d.ts +1 -1
  447. package/dist/src/styles/plan/features.d.ts.map +1 -1
  448. package/dist/src/styles/plan/features.js +22 -2
  449. package/dist/src/styles/plan/features.js.map +1 -1
  450. package/dist/src/styles/plan/index.d.ts +1 -1
  451. package/dist/src/styles/plan/index.d.ts.map +1 -1
  452. package/dist/src/styles/plan/index.js +1 -1
  453. package/dist/src/styles/plan/index.js.map +1 -1
  454. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  455. package/dist/src/styles/plan/plan.js +61 -4
  456. package/dist/src/styles/plan/plan.js.map +1 -1
  457. package/dist/src/styles/plan/resolve.d.ts +11 -0
  458. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  459. package/dist/src/styles/plan/resolve.js +40 -4
  460. package/dist/src/styles/plan/resolve.js.map +1 -1
  461. package/dist/src/styles/slots.d.ts +14 -0
  462. package/dist/src/styles/slots.d.ts.map +1 -0
  463. package/dist/src/styles/slots.js +20 -0
  464. package/dist/src/styles/slots.js.map +1 -0
  465. package/dist/src/styles/types.d.ts +687 -6
  466. package/dist/src/styles/types.d.ts.map +1 -1
  467. package/dist/src/styles/vocabulary.d.ts +3 -2
  468. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  469. package/dist/src/styles/vocabulary.js +224 -0
  470. package/dist/src/styles/vocabulary.js.map +1 -1
  471. package/dist/styles.js +30 -239
  472. package/dist/styles.js.map +1 -1
  473. package/dist/xgmml.js +3 -3
  474. package/package.json +27 -7
  475. package/src/formats/cx/importer.ts +6 -1
  476. package/src/formats/cx/styles.ts +143 -9
  477. package/src/formats/cx2/importer.ts +1 -0
  478. package/src/formats/cx2/styles.ts +3 -1
  479. package/src/formats/cys/importer.ts +1 -0
  480. package/src/formats/cys/styles.ts +27 -5
  481. package/src/formats/dot/drawing.ts +799 -0
  482. package/src/formats/dot/exporter.ts +37 -1
  483. package/src/formats/dot/importer.ts +192 -36
  484. package/src/formats/dot/index.ts +3 -1
  485. package/src/formats/dot/label-places.ts +241 -0
  486. package/src/formats/dot/names.ts +15 -0
  487. package/src/formats/dot/record.ts +336 -0
  488. package/src/formats/dot/styles.ts +1702 -149
  489. package/src/formats/gexf/dialog-metrics.ts +61 -0
  490. package/src/formats/gexf/importer.ts +50 -5
  491. package/src/formats/gexf/preview.ts +217 -0
  492. package/src/formats/gexf/schema.ts +2 -1
  493. package/src/formats/gexf/styles.ts +1099 -245
  494. package/src/formats/gml/importer.ts +291 -76
  495. package/src/formats/gml/index.ts +1 -1
  496. package/src/formats/gml/positions.ts +29 -7
  497. package/src/formats/gml/styles.ts +852 -110
  498. package/src/formats/graphml/styles-plain.ts +6 -15
  499. package/src/formats/graphml/styles-yed.ts +765 -176
  500. package/src/formats/graphml/styles-yfiles3.ts +487 -63
  501. package/src/formats/graphml/yfiles.ts +19 -6
  502. package/src/formats/json/dialect.ts +41 -5
  503. package/src/formats/json/importer.ts +59 -7
  504. package/src/formats/json/index.ts +1 -0
  505. package/src/formats/json/positions.ts +22 -5
  506. package/src/formats/json/styles.ts +2123 -171
  507. package/src/formats/pajek/draw.ts +469 -0
  508. package/src/formats/pajek/exporter.ts +81 -49
  509. package/src/formats/pajek/importer.ts +278 -103
  510. package/src/formats/pajek/index.ts +1 -0
  511. package/src/formats/pajek/styles.ts +1506 -133
  512. package/src/formats/pajek/syntax.ts +27 -2
  513. package/src/formats/xgmml/emit.ts +73 -5
  514. package/src/formats/xgmml/styles.ts +330 -71
  515. package/src/index.ts +12 -1
  516. package/src/positions/convert.ts +12 -1
  517. package/src/positions/extract.ts +55 -1
  518. package/src/positions/index.ts +1 -0
  519. package/src/positions/types.ts +60 -0
  520. package/src/sources/builtins.ts +8 -1
  521. package/src/sources/context.ts +1 -0
  522. package/src/sources/credentials.ts +90 -1
  523. package/src/sources/cypher/descriptor.ts +7 -1
  524. package/src/sources/cypher/index.ts +149 -18
  525. package/src/sources/cypher/params.ts +1 -1
  526. package/src/sources/cypher/query-builder.ts +33 -0
  527. package/src/sources/ids.ts +6 -0
  528. package/src/sources/params.ts +3 -0
  529. package/src/sources/rest/descriptor.ts +9 -0
  530. package/src/sources/rest/openalex.ts +4 -2
  531. package/src/sources/rest/source.ts +41 -0
  532. package/src/sources/run.ts +35 -3
  533. package/src/sources/sigv4.ts +6 -4
  534. package/src/sources/sparql/descriptor.ts +67 -0
  535. package/src/sources/sparql/index.ts +371 -0
  536. package/src/sources/sparql/ntriples.ts +219 -0
  537. package/src/sources/sparql/params.ts +28 -0
  538. package/src/sources/sparql/query.ts +216 -0
  539. package/src/sources/types.ts +68 -0
  540. package/src/styles/annotations.ts +201 -27
  541. package/src/styles/canon/bends.ts +137 -0
  542. package/src/styles/canon/color-tables.ts +10 -4
  543. package/src/styles/canon/color.ts +105 -3
  544. package/src/styles/canon/enums.ts +107 -8
  545. package/src/styles/canon/graphviz.ts +71 -10
  546. package/src/styles/canon/number.ts +19 -0
  547. package/src/styles/canon/text.ts +187 -16
  548. package/src/styles/codes.ts +2 -0
  549. package/src/styles/cyjs/cascade.ts +1303 -84
  550. package/src/styles/cyjs/defaults-table.ts +4 -0
  551. package/src/styles/cyjs/emit.ts +268 -14
  552. package/src/styles/cyjs/face.ts +372 -0
  553. package/src/styles/cyjs/mappers.ts +506 -27
  554. package/src/styles/cyjs/routes.ts +432 -0
  555. package/src/styles/cytoscape/charts.ts +684 -0
  556. package/src/styles/cytoscape/emit.ts +170 -48
  557. package/src/styles/cytoscape/lexicon.ts +431 -78
  558. package/src/styles/cytoscape/mappings.ts +108 -0
  559. package/src/styles/cytoscape/resolve.ts +741 -17
  560. package/src/styles/defaults.ts +417 -108
  561. package/src/styles/index.ts +5 -0
  562. package/src/styles/internal.ts +1 -0
  563. package/src/styles/io/generic.ts +79 -23
  564. package/src/styles/io/round-trip.ts +202 -0
  565. package/src/styles/io/writer.ts +6 -1
  566. package/src/styles/notes.ts +1 -0
  567. package/src/styles/plan/features.ts +23 -2
  568. package/src/styles/plan/index.ts +1 -1
  569. package/src/styles/plan/plan.ts +69 -5
  570. package/src/styles/plan/resolve.ts +47 -4
  571. package/src/styles/slots.ts +25 -0
  572. package/src/styles/types.ts +698 -5
  573. package/src/styles/vocabulary.ts +232 -2
  574. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  575. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  576. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  577. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  578. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  579. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  580. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  581. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  582. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  583. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  584. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  585. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  586. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  587. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  588. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  589. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  590. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  591. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  592. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  593. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  594. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  595. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  596. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  597. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  598. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  599. package/dist/chunks/writer-DvHTl36J.js +0 -449
  600. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -36,11 +36,16 @@ import {
36
36
  LABEL_ATTRIBUTE,
37
37
  PARENT_COLUMN,
38
38
  POS_ATTRIBUTE,
39
+ RANK_COLUMN,
40
+ RANK_SET_COLUMN,
39
41
  SOURCE_PORT_COLUMN,
40
42
  TARGET_PORT_COLUMN,
41
43
  } from "./names.js";
42
44
  import { DOT_ATTRIBUTE_CLASH_CODE, DOT_STYLE_CAPABILITIES, dotStyledExport, isHtmlString } from "./styles.js";
43
45
 
46
+ /** The node columns written as rank subgraphs rather than as node attributes. */
47
+ const RANK_COLUMNS: ReadonlySet<string> = new Set([RANK_COLUMN, RANK_SET_COLUMN]);
48
+
44
49
  /** What the writer takes: the format's options, the common ones and the styles. */
45
50
  type DotWriteOptions = DotExportOptions & CommonExportOptions & ExporterStyleOptions;
46
51
 
@@ -611,7 +616,7 @@ class ExportPlan {
611
616
  */
612
617
  private writesNodeColumn(column: Column): boolean {
613
618
  const { name, role } = column.meta;
614
- if (!isTextualDtype(column) || name === CLUSTER_COLUMN) {
619
+ if (!isTextualDtype(column) || name === CLUSTER_COLUMN || RANK_COLUMNS.has(name)) {
615
620
  return false;
616
621
  }
617
622
  if (role !== null && SKIPPED_NODE_ROLES.has(role)) {
@@ -875,10 +880,41 @@ class ExportPlan {
875
880
  for (let i = 0; i < snapshot.nodeCount; i++) {
876
881
  yield* this.writeNode(i, 1, emitted);
877
882
  }
883
+ yield* this.writeRanks();
878
884
  yield* this.writeEdges();
879
885
  yield "}\n";
880
886
  }
881
887
 
888
+ /**
889
+ * Write each rank subgraph the `graphty.rank` and `graphty.rankSet` columns hold, after the nodes: a named one
890
+ * under its name, an anonymous one (`%<n>`) as an anonymous subgraph.
891
+ * @yields the statements
892
+ * @returns nothing
893
+ */
894
+ private *writeRanks(): Generator<string> {
895
+ const { nodes } = this.snapshot;
896
+ const rank = nodes.get(RANK_COLUMN);
897
+ const set = nodes.get(RANK_SET_COLUMN);
898
+ if (rank === null || set === null) {
899
+ return;
900
+ }
901
+ const sets = new Map<string, { rank: string; members: string[] }>();
902
+ for (let i = 0; i < this.snapshot.nodeCount; i++) {
903
+ if (rank.isSet(i) && set.isSet(i)) {
904
+ const key = String(set.value(i));
905
+ const entry = sets.get(key) ?? { rank: String(rank.value(i)), members: [] };
906
+ entry.members.push(writeId(this.idText(i)));
907
+ sets.set(key, entry);
908
+ }
909
+ }
910
+ // anonymous ones in their number's order, so the file read back numbers them the same
911
+ const order = (key: string): number => (key.startsWith("%") ? Number(key.slice(1)) : -1);
912
+ for (const [key, entry] of [...sets].sort(([a], [b]) => order(a) - order(b))) {
913
+ const head = key.startsWith("%") ? "" : `subgraph ${quoteDotId(key)} `;
914
+ yield `${this.indent}${head}{ rank=${quoteDotId(entry.rank)}; ${entry.members.join("; ")}; }\n`;
915
+ }
916
+ }
917
+
882
918
  /**
883
919
  * Write one node (and, for a container, its cluster block with the members nested).
884
920
  * @param i - the node index
@@ -98,6 +98,8 @@ import {
98
98
  PIN_ATTRIBUTE,
99
99
  POS_ATTRIBUTE,
100
100
  POSITION_COLUMN,
101
+ RANK_COLUMN,
102
+ RANK_SET_COLUMN,
101
103
  SOURCE_PORT_COLUMN,
102
104
  TARGET_PORT_COLUMN,
103
105
  } from "./names.js";
@@ -177,8 +179,13 @@ export const DOT_ISSUE = Object.freeze({
177
179
  AMBIGUOUS_GRAPH_NAME: AMBIGUOUS_GRAPH_NAME_CODE,
178
180
  /** A badly delimited numeral (`1e3`) split into two tokens, as Graphviz does with a warning. */
179
181
  NUMERAL_AMBIGUITY: "W_DOT_NUMERAL_AMBIGUITY",
180
- /** Attributes of a subgraph that is not a cluster (rank=same and the like) cannot be represented. */
182
+ /**
183
+ * Attributes of a subgraph that is not a cluster cannot be represented, except its `rank`, which is kept in the
184
+ * `graphty.rank` and `graphty.rankSet` node columns.
185
+ */
181
186
  SUBGRAPH_ATTRIBUTES_DROPPED: "W_DOT_SUBGRAPH_ATTRIBUTES_DROPPED",
187
+ /** A node in two rank subgraphs (`rank=same` and the like) keeps the first one's rank. */
188
+ RANK_CONFLICT: "W_DOT_RANK_CONFLICT",
182
189
  /** A port on a node statement has no meaning and was dropped. */
183
190
  NODE_PORT_DROPPED: "W_DOT_NODE_PORT_DROPPED",
184
191
  /** A plain node and a cluster share a name and were merged into one container node. */
@@ -262,8 +269,35 @@ const EXTENSIONS: readonly string[] = Object.freeze([".dot", ".gv"]);
262
269
  const MIME_TYPES: readonly string[] = Object.freeze(["text/vnd.graphviz"]);
263
270
  const CLUSTER_PREFIX = "cluster";
264
271
  const CLUSTER_ATTRIBUTE = "cluster";
272
+ /** The subgraph attribute that puts its nodes on one rank, or on the first or last. */
273
+ const RANK_ATTRIBUTE = "rank";
274
+ /** The values of `rank` Graphviz knows (lib/common/const.h: SAMERANK ... SINKRANK). */
275
+ const RANK_KINDS: ReadonlySet<string> = new Set(["same", "min", "max", "source", "sink"]);
265
276
  const STATEMENTS_PER_ABORT_CHECK = 64;
266
277
  const MAX_ANCESTOR_WALK = 4096;
278
+ /**
279
+ * The graph attributes a cluster takes from the graphs around it when it sets none of its own: Graphviz gives a
280
+ * subgraph every graph attribute its enclosing graphs set before it opened (a root `label` set first labels every
281
+ * cluster). Only the attributes that decide how a cluster is drawn are copied.
282
+ */
283
+ const INHERITED_GRAPH_ATTRIBUTES: ReadonlySet<string> = new Set([
284
+ "bgcolor",
285
+ "color",
286
+ "colorscheme",
287
+ "fillcolor",
288
+ "fontcolor",
289
+ "fontname",
290
+ "fontsize",
291
+ "gradientangle",
292
+ "label",
293
+ "labeljust",
294
+ "labelloc",
295
+ "pencolor",
296
+ "penwidth",
297
+ "peripheries",
298
+ "style",
299
+ "tooltip",
300
+ ]);
267
301
 
268
302
  /**
269
303
  * The header keyword, ended by what may follow it in DOT: space, `{`, a quoted ID or a comment.
@@ -320,6 +354,8 @@ interface Scope {
320
354
  readonly containers: number[];
321
355
  /** Subgraph attributes buffered until the subgraph closes (the root applies them at once). */
322
356
  readonly attributes: Map<string, DotAttribute>;
357
+ /** The drawing attributes a cluster opened in this scope inherits: the enclosing scopes' and this one's so far. */
358
+ readonly graphDefaults: Map<string, DotAttribute>;
323
359
  /** Whether the subgraph is a cluster (by name or by `cluster=true`). */
324
360
  cluster: boolean;
325
361
  /** The container node index once created, or INVALID_INDEX. */
@@ -734,6 +770,12 @@ class DotParser {
734
770
  /** The parent assigned to a node by this import. */
735
771
  private readonly parentOf = new Map<number, number>();
736
772
 
773
+ /** Each cluster container's inherited drawing attributes, as its scope saw them when the cluster first opened. */
774
+ private readonly inherited = new Map<number, Map<string, DotAttribute>>();
775
+
776
+ /** The attributes each cluster container sets itself, which its inherited ones never replace. */
777
+ private readonly ownAttributes = new Map<number, Set<string>>();
778
+
737
779
  /** Nodes already warned about a cluster conflict. */
738
780
  private readonly conflictWarned = new Set<number>();
739
781
 
@@ -761,6 +803,16 @@ class DotParser {
761
803
 
762
804
  private targetPortHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
763
805
 
806
+ private rankHandle: ColumnHandle = INVALID_INDEX as ColumnHandle; // NOSONAR(S2933): stringColumn assigns it through this[field]
807
+
808
+ private rankSetHandle: ColumnHandle = INVALID_INDEX as ColumnHandle; // NOSONAR(S2933): stringColumn assigns it through this[field]
809
+
810
+ /** The rank subgraph each node was put in first, by node index. */
811
+ private readonly rankSetOf = new Map<number, string>();
812
+
813
+ /** How many anonymous rank subgraphs were read, which names the next `%<n>`. */
814
+ private anonymousRanks = 0;
815
+
764
816
  /** The current subgraph nesting depth. */
765
817
  private depth = 0;
766
818
 
@@ -851,6 +903,7 @@ class DotParser {
851
903
  });
852
904
  const root = this.newScope(null, headerLine, true, null);
853
905
  this.statementList(root);
906
+ this.applyInherited();
854
907
  return lexer.next();
855
908
  }
856
909
 
@@ -873,6 +926,7 @@ class DotParser {
873
926
  memberSet: new Set(),
874
927
  containers: [],
875
928
  attributes: new Map(),
929
+ graphDefaults: new Map(parent?.graphDefaults),
876
930
  cluster: false,
877
931
  container: INVALID_INDEX,
878
932
  };
@@ -1113,42 +1167,10 @@ class DotParser {
1113
1167
  */
1114
1168
  private closeScope(scope: Scope, parent: Scope): void {
1115
1169
  const { container } = scope;
1116
- if (container !== INVALID_INDEX) {
1117
- for (const attribute of scope.attributes.values()) {
1118
- if (attribute.name !== CLUSTER_ATTRIBUTE) {
1119
- this.setNodeAttribute(container, attribute.name, attribute.value, attribute.line);
1120
- }
1121
- }
1122
- for (const nested of scope.containers) {
1123
- if (nested !== container && !this.parentOf.has(nested)) {
1124
- this.setParent(nested, container);
1125
- }
1126
- }
1127
- for (const member of scope.members) {
1128
- if (member === container) {
1129
- continue;
1130
- }
1131
- const existing = this.parentOf.get(member);
1132
- if (existing === undefined) {
1133
- this.setParent(member, container);
1134
- } else if (existing !== container && !this.isAncestor(container, existing)) {
1135
- this.clusterConflict(member, existing, container, scope.line);
1136
- }
1137
- }
1138
- parent.containers.push(container);
1170
+ if (container === INVALID_INDEX) {
1171
+ this.closePlainSubgraph(scope, parent);
1139
1172
  } else {
1140
- if (scope.attributes.size > 0) {
1141
- const names = [...scope.attributes.keys()].join(", ");
1142
- this.report.warning(
1143
- "unsupported",
1144
- DOT_ISSUE.SUBGRAPH_ATTRIBUTES_DROPPED,
1145
- `subgraph ${scope.name === null ? "(anonymous)" : JSON.stringify(scope.name)} is not a cluster; its attributes (${names}) cannot be represented and were dropped`,
1146
- { line: scope.line, element: scope.name },
1147
- );
1148
- }
1149
- for (const nested of scope.containers) {
1150
- parent.containers.push(nested);
1151
- }
1173
+ this.closeCluster(scope, parent, container);
1152
1174
  }
1153
1175
  if (!parent.root) {
1154
1176
  for (const member of scope.members) {
@@ -1160,6 +1182,88 @@ class DotParser {
1160
1182
  }
1161
1183
  }
1162
1184
 
1185
+ /**
1186
+ * Finish a cluster: apply its attributes to its container node, assign parents to its nested
1187
+ * containers and its members, and hand the container to the enclosing scope.
1188
+ * @param scope - the closed scope
1189
+ * @param parent - the enclosing scope
1190
+ * @param container - the cluster's container node
1191
+ */
1192
+ private closeCluster(scope: Scope, parent: Scope, container: number): void {
1193
+ const own = this.ownAttributes.get(container) ?? new Set<string>();
1194
+ this.ownAttributes.set(container, own);
1195
+ for (const attribute of scope.attributes.values()) {
1196
+ if (attribute.name !== CLUSTER_ATTRIBUTE) {
1197
+ own.add(attribute.name);
1198
+ this.setNodeAttribute(container, attribute.name, attribute.value, attribute.line);
1199
+ }
1200
+ }
1201
+ for (const nested of scope.containers) {
1202
+ if (nested !== container && !this.parentOf.has(nested)) {
1203
+ this.setParent(nested, container);
1204
+ }
1205
+ }
1206
+ for (const member of scope.members) {
1207
+ if (member !== container) {
1208
+ this.adoptMember(member, container, scope.line);
1209
+ }
1210
+ }
1211
+ parent.containers.push(container);
1212
+ }
1213
+
1214
+ /**
1215
+ * Put a cluster's member in it, unless it is already in another cluster this one is not inside.
1216
+ * @param member - the member node
1217
+ * @param container - the cluster's container node
1218
+ * @param line - the cluster's line
1219
+ */
1220
+ private adoptMember(member: number, container: number, line: number): void {
1221
+ const existing = this.parentOf.get(member);
1222
+ if (existing === undefined) {
1223
+ this.setParent(member, container);
1224
+ } else if (existing !== container && !this.isAncestor(container, existing)) {
1225
+ this.clusterConflict(member, existing, container, line);
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * Finish a subgraph that is not a cluster: keep its rank, report its other attributes dropped, and hand its nested
1231
+ * containers to the enclosing scope.
1232
+ * @param scope - the closed scope
1233
+ * @param parent - the enclosing scope
1234
+ */
1235
+ private closePlainSubgraph(scope: Scope, parent: Scope): void {
1236
+ const ranked = this.recordRank(scope);
1237
+ const dropped = [...scope.attributes.keys()].filter((name) => !(ranked && name === RANK_ATTRIBUTE));
1238
+ if (dropped.length > 0) {
1239
+ const names = dropped.join(", ");
1240
+ this.report.warning(
1241
+ "unsupported",
1242
+ DOT_ISSUE.SUBGRAPH_ATTRIBUTES_DROPPED,
1243
+ `subgraph ${scope.name === null ? "(anonymous)" : JSON.stringify(scope.name)} is not a cluster; its attributes (${names}) cannot be represented and were dropped`,
1244
+ { line: scope.line, element: scope.name },
1245
+ );
1246
+ }
1247
+ for (const nested of scope.containers) {
1248
+ parent.containers.push(nested);
1249
+ }
1250
+ }
1251
+
1252
+ /**
1253
+ * Give every cluster the drawing attributes it inherits and does not set itself (Graphviz's subgraph attribute
1254
+ * inheritance), once the whole graph is read: a cluster reopened later can still set its own.
1255
+ */
1256
+ private applyInherited(): void {
1257
+ for (const [container, attributes] of this.inherited) {
1258
+ const own = this.ownAttributes.get(container);
1259
+ for (const attribute of attributes.values()) {
1260
+ if (own?.has(attribute.name) !== true) {
1261
+ this.setNodeAttribute(container, attribute.name, attribute.value, attribute.line);
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+
1163
1267
  /**
1164
1268
  * Whether `candidate` is an ancestor of `node` through the parents assigned so far.
1165
1269
  * @param candidate - the possible ancestor
@@ -1244,6 +1348,7 @@ class DotParser {
1244
1348
  if (!this.containers.has(index)) {
1245
1349
  this.containers.add(index);
1246
1350
  this.sink.setNodeValue(this.clusterColumn(), index, true);
1351
+ this.inherited.set(index, new Map(scope.graphDefaults));
1247
1352
  }
1248
1353
  scope.container = index;
1249
1354
  } catch (err) {
@@ -1261,6 +1366,9 @@ class DotParser {
1261
1366
  * @param attribute - the attribute
1262
1367
  */
1263
1368
  private scopeAttribute(scope: Scope, attribute: DotAttribute): void {
1369
+ if (INHERITED_GRAPH_ATTRIBUTES.has(attribute.name)) {
1370
+ scope.graphDefaults.set(attribute.name, attribute);
1371
+ }
1264
1372
  if (scope.root) {
1265
1373
  this.setGraphAttribute(attribute);
1266
1374
  return;
@@ -1901,6 +2009,54 @@ class DotParser {
1901
2009
  return this.positionHandle;
1902
2010
  }
1903
2011
 
2012
+ /**
2013
+ * Keep a non-cluster subgraph's `rank` on its nodes: each gets the rank and the subgraph's name (`%<n>` for an
2014
+ * anonymous one). A node already in another rank subgraph keeps that one, with a warning.
2015
+ * @param scope - the closed subgraph
2016
+ * @returns true when the subgraph has a rank Graphviz knows
2017
+ */
2018
+ private recordRank(scope: Scope): boolean {
2019
+ const rank = scope.attributes.get(RANK_ATTRIBUTE)?.value.trim();
2020
+ if (rank === undefined || !RANK_KINDS.has(rank)) {
2021
+ return false;
2022
+ }
2023
+ const set = scope.name ?? `%${this.anonymousRanks++}`;
2024
+ for (const member of scope.members) {
2025
+ const held = this.rankSetOf.get(member);
2026
+ const id = String(this.idOf.get(member) ?? member);
2027
+ if (held === undefined) {
2028
+ this.rankSetOf.set(member, set);
2029
+ try {
2030
+ this.sink.setNodeValue(this.stringColumn("rankHandle", RANK_COLUMN), member, rank);
2031
+ this.sink.setNodeValue(this.stringColumn("rankSetHandle", RANK_SET_COLUMN), member, set);
2032
+ } catch (err) {
2033
+ this.report.recordError(err, { element: id });
2034
+ }
2035
+ } else if (held !== set) {
2036
+ this.report.warning(
2037
+ "coercion",
2038
+ DOT_ISSUE.RANK_CONFLICT,
2039
+ `node ${JSON.stringify(id)} is in rank subgraph ${JSON.stringify(held)} and in rank subgraph ${JSON.stringify(set)}; the first is kept`,
2040
+ { line: scope.line, element: id },
2041
+ );
2042
+ }
2043
+ }
2044
+ return true;
2045
+ }
2046
+
2047
+ /**
2048
+ * One of the importer's string node columns, declared on first use.
2049
+ * @param field - the field holding its handle
2050
+ * @param name - the column name
2051
+ * @returns the handle
2052
+ */
2053
+ private stringColumn(field: "rankHandle" | "rankSetHandle", name: string): ColumnHandle {
2054
+ if (this[field] === INVALID_INDEX) {
2055
+ this[field] = this.declare("node", { name, dtype: "string", nullable: true, origin: DOT_ORIGIN });
2056
+ }
2057
+ return this[field];
2058
+ }
2059
+
1904
2060
  /**
1905
2061
  * The cluster marker column (bool).
1906
2062
  * @returns the handle
@@ -4,6 +4,8 @@
4
4
  * @module @graphty/graph-io/dot
5
5
  */
6
6
 
7
+ export type { BendPoint } from "../../styles/canon/bends.js";
7
8
  export { DOT_LOSS, dotExporter, type DotExportOptions } from "./exporter.js";
8
9
  export { DOT_ISSUE, dotImporter, type DotImportOptions } from "./importer.js";
9
- export { DOT_STYLE_BLOCK_ATTRIBUTE, DOT_STYLE_CAPABILITIES } from "./styles.js";
10
+ export { RANK_COLUMN, RANK_SET_COLUMN } from "./names.js";
11
+ export { DOT_STYLE_BLOCK_ATTRIBUTE, DOT_STYLE_CAPABILITIES, graphvizPosBends } from "./styles.js";
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Where Graphviz puts an edge's labels: a stored `lp` (the label's center), and the head and tail labels at
3
+ * `head_lp` / `tail_lp` -- kept only with the edge's stored `pos` (`neato -n2` routes an edge without one again, and
4
+ * places its labels again) -- or where Graphviz's `place_portlabel` (lib/common/splines.c) puts them, when the edge's
5
+ * `labelangle` or `labeldistance` is not its default: `10 * labeldistance` points from the end of the line, turned
6
+ * `labelangle` degrees (default -25) from the line. With both at their defaults Graphviz places end labels as external
7
+ * labels, which graphty leaves to its own place beside the end. Read as offsets from where graphty puts each label:
8
+ * the edge label from the middle of its route, an end label from where the line meets its node.
9
+ */
10
+
11
+ import type { GraphSnapshot } from "@graphty/graph-format";
12
+
13
+ import { type BendPoint, flattenCubic } from "../../styles/canon/bends.js";
14
+ import { num6 } from "../../styles/canon/number.js";
15
+ import type { StyleValue } from "../../styles/types.js";
16
+
17
+ /** Graphviz's `PORT_LABEL_DISTANCE`, in points. */
18
+ const PORT_LABEL_DISTANCE = 10;
19
+
20
+ /** Graphviz's `PORT_LABEL_ANGLE`, in degrees. */
21
+ const PORT_LABEL_ANGLE = -25;
22
+
23
+ /** Graphviz's default node size, in inches. */
24
+ const DEFAULT_SIZE = { w: 0.75, h: 0.5 } as const;
25
+
26
+ /** One attribute of an element as text, or null when it is unset. */
27
+ type Attr = (name: string) => string | null;
28
+
29
+ /** Receives one property's value. */
30
+ type Put = (property: string, value: StyleValue | null) => void;
31
+
32
+ /** What the label places of one graph's edges are read against. */
33
+ export interface LabelPlaceContext {
34
+ /** A node's center from its position, in points, y up. */
35
+ readonly center: (n: number) => BendPoint | null;
36
+ /** A node's attribute. */
37
+ readonly node: (n: number, name: string) => string | null;
38
+ /**
39
+ * Whether the layout engine places an edge label it was not given as an external label (every engine but `dot`,
40
+ * which ranks a label as a node of its own): lib/common/postproc.c `addXLabels` takes each unplaced edge label as
41
+ * an xlabel of the edge's middle, and lib/label/xlabels.c puts it, where nothing else is drawn, up and to the left
42
+ * of that point, its box's lower right corner on it.
43
+ */
44
+ readonly external: boolean;
45
+ }
46
+
47
+ /**
48
+ * Reads an edge's stored label places and its head and tail label places as offsets.
49
+ * @param snapshot - the graph
50
+ * @param cx - the graph's label place context
51
+ * @param e - the edge
52
+ * @param get - the edge's attributes
53
+ * @param put - receives the values
54
+ */
55
+ export function readLabelPlaces(snapshot: GraphSnapshot, cx: LabelPlaceContext, e: number, get: Attr, put: Put): void {
56
+ const s = snapshot.edgeSource(e);
57
+ const t = snapshot.edgeTarget(e);
58
+ const [source, target] = [cx.center(s), cx.center(t)];
59
+ const route = source === null || target === null ? null : edgeRoute(source, target, get("pos"));
60
+ const lp = (route?.start ?? null) === null ? null : point(get("lp"));
61
+ if (lp === null && cx.external && s !== t && (get("label") ?? get("xlabel")) !== null) {
62
+ // ponytail: the first place xlabels.c tries; one that would cover another label or node moves elsewhere
63
+ put("labelPosition", "center");
64
+ put("labelShiftX", -0.5);
65
+ put("labelShiftY", -0.5);
66
+ }
67
+ if (route === null || source === null || target === null || s === t) {
68
+ return;
69
+ }
70
+ if (lp !== null && get("label") !== null) {
71
+ const mid = routeMiddle(route.points);
72
+ put("labelOffsetX", num6(lp[0] - mid[0]));
73
+ put("labelOffsetY", num6(mid[1] - lp[1]));
74
+ }
75
+ readEndLabel(cx, route, { node: t, at: target, other: source }, "head", get, put);
76
+ readEndLabel(cx, route, { node: s, at: source, other: target }, "tail", get, put);
77
+ }
78
+
79
+ /** One end of an edge: its node, that node's center and the other end's center. */
80
+ interface EdgeEnd {
81
+ readonly node: number;
82
+ readonly at: BendPoint;
83
+ readonly other: BendPoint;
84
+ }
85
+
86
+ /**
87
+ * Reads one end's label place as an offset from where its line meets its node.
88
+ * @param cx - the graph's label place context
89
+ * @param route - the edge's route
90
+ * @param end - the end's node and the two centers
91
+ * @param which - which end
92
+ * @param get - the edge's attributes
93
+ * @param put - receives the values
94
+ */
95
+ function readEndLabel(
96
+ cx: LabelPlaceContext,
97
+ route: EdgeRoute,
98
+ end: EdgeEnd,
99
+ which: "head" | "tail",
100
+ get: Attr,
101
+ put: Put,
102
+ ): void {
103
+ if (get(`${which}label`) === null) {
104
+ return;
105
+ }
106
+ const head = which === "head";
107
+ const tip = (head ? route.end : route.start) ?? boundary(cx, end.node, end.at, end.other);
108
+ // the nearest route point back from the end that is not the end itself (a spline with no arrow ends at it);
109
+ // the route's last point is the node's center, past the end
110
+ const back = head ? route.points.slice(0, -1).reverse() : route.points.slice(1);
111
+ const toward = back.find((p) => Math.hypot(p[0] - tip[0], p[1] - tip[1]) > 1e-6) ?? end.other;
112
+ const stored = route.start === null ? null : point(get(`${which}_lp`));
113
+ const at = stored ?? portLabel(get, tip, toward);
114
+ if (at !== null) {
115
+ const prefix = head ? "target" : "source";
116
+ put(`${prefix}LabelOffset`, 0);
117
+ put(`${prefix}LabelOffsetX`, num6(at[0] - tip[0]));
118
+ put(`${prefix}LabelOffsetY`, num6(tip[1] - at[1]));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Where Graphviz's `place_portlabel` puts an end label's center: only when the edge's `labelangle` or
124
+ * `labeldistance` is not its default.
125
+ * @param get - the edge's attributes
126
+ * @param tip - the end of the line
127
+ * @param toward - a point back along the line from the end
128
+ * @returns the center, or null when Graphviz places the label as an external label
129
+ */
130
+ function portLabel(get: Attr, tip: BendPoint, toward: BendPoint): BendPoint | null {
131
+ const turn = finite(get("labelangle"), PORT_LABEL_ANGLE, -180);
132
+ const distance = finite(get("labeldistance"), 1, 0);
133
+ if (turn === PORT_LABEL_ANGLE && distance === 1) {
134
+ return null;
135
+ }
136
+ const angle = Math.atan2(toward[1] - tip[1], toward[0] - tip[0]) + (turn * Math.PI) / 180;
137
+ const dist = PORT_LABEL_DISTANCE * distance;
138
+ return [tip[0] + dist * Math.cos(angle), tip[1] + dist * Math.sin(angle)];
139
+ }
140
+
141
+ /**
142
+ * A number attribute as Graphviz's `late_double` reads it: the default when unset or not a number, at least `low`.
143
+ * @param raw - the value
144
+ * @param fallback - the default
145
+ * @param low - the least value
146
+ * @returns the number
147
+ */
148
+ function finite(raw: string | null, fallback: number, low: number): number {
149
+ const v = raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
150
+ return Number.isFinite(v) ? Math.max(low, v) : fallback;
151
+ }
152
+
153
+ /**
154
+ * An "x,y" point (a trailing `!` allowed).
155
+ * @param raw - the text
156
+ * @returns the point, or null
157
+ */
158
+ function point(raw: string | null): BendPoint | null {
159
+ const parts = (raw ?? "").replace(/!$/, "").split(",").map(Number);
160
+ return parts.length >= 2 && Number.isFinite(parts[0]) && Number.isFinite(parts[1]) ? [parts[0], parts[1]] : null;
161
+ }
162
+
163
+ /** An edge's route: its points from center to center, and the ends of its line when stored. */
164
+ interface EdgeRoute {
165
+ readonly points: BendPoint[];
166
+ readonly start: BendPoint | null;
167
+ readonly end: BendPoint | null;
168
+ }
169
+
170
+ /**
171
+ * The route an edge is drawn along: its stored spline's points (flattened) between the two centers, and the ends of
172
+ * its line where `pos` gives arrow points (`s,` / `e,`) or its spline's own ends.
173
+ * @param source - the source's center
174
+ * @param target - the target's center
175
+ * @param pos - the edge's `pos`
176
+ * @returns the route
177
+ */
178
+ function edgeRoute(source: BendPoint, target: BendPoint, pos: string | null): EdgeRoute {
179
+ const tokens = (pos ?? "").split(";")[0].trim().split(/\s+/).filter(Boolean);
180
+ const marked = (flag: string): BendPoint | null => {
181
+ const token = tokens.find((x) => x.startsWith(`${flag},`));
182
+ return token === undefined ? null : point(token.slice(2));
183
+ };
184
+ const controls = tokens.filter((x) => !/^[es],/.test(x)).map((x) => point(x));
185
+ const flat = controls.every((p) => p !== null) && controls.length >= 4 ? flattenCubic(controls) : null;
186
+ if (flat === null) {
187
+ return { points: [source, target], start: null, end: null };
188
+ }
189
+ return {
190
+ points: [source, ...flat, target],
191
+ start: marked("s") ?? flat[0],
192
+ end: marked("e") ?? flat.at(-1) ?? null,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * The point halfway along a route.
198
+ * @param points - the route
199
+ * @returns the point
200
+ */
201
+ function routeMiddle(points: readonly BendPoint[]): BendPoint {
202
+ // ponytail: halfway by length; graphty puts a bent edge's label at its middle segment, close to it
203
+ const lengths = points.slice(1).map((p, i) => Math.hypot(p[0] - points[i][0], p[1] - points[i][1]));
204
+ let left = lengths.reduce((a, b) => a + b, 0) / 2;
205
+ for (let i = 0; i < lengths.length; i++) {
206
+ if (left <= lengths[i] && lengths[i] > 0) {
207
+ const f = left / lengths[i];
208
+ return [
209
+ points[i][0] + f * (points[i + 1][0] - points[i][0]),
210
+ points[i][1] + f * (points[i + 1][1] - points[i][1]),
211
+ ];
212
+ }
213
+ left -= lengths[i];
214
+ }
215
+ return points[0];
216
+ }
217
+
218
+ /**
219
+ * Where the straight line from a node's center toward a point leaves the node, the node taken as the ellipse of its
220
+ * `width` and `height`.
221
+ * @param cx - the label place context
222
+ * @param n - the node
223
+ * @param center - its center
224
+ * @param toward - the point
225
+ * @returns the point on its outline
226
+ */
227
+ function boundary(cx: LabelPlaceContext, n: number, center: BendPoint, toward: BendPoint): BendPoint {
228
+ // ponytail: every shape taken as its ellipse; exact for the circles and ellipses the end labels sit on
229
+ const inch = (name: "width" | "height", fallback: number): number => {
230
+ const v = Number(cx.node(n, name) ?? Number.NaN);
231
+ return (Number.isFinite(v) && v >= 0 ? v : fallback) * 72;
232
+ };
233
+ const [rx, ry] = [inch("width", DEFAULT_SIZE.w) / 2, inch("height", DEFAULT_SIZE.h) / 2];
234
+ const [dx, dy] = [toward[0] - center[0], toward[1] - center[1]];
235
+ const len = Math.hypot(dx, dy);
236
+ if (len === 0 || rx === 0 || ry === 0) {
237
+ return center;
238
+ }
239
+ const r = 1 / Math.hypot(dx / len / rx, dy / len / ry);
240
+ return [center[0] + (dx / len) * r, center[1] + (dy / len) * r];
241
+ }
@@ -77,3 +77,18 @@ export const PIN_ATTRIBUTE = "pin";
77
77
  * @category Plugin helpers
78
78
  */
79
79
  export const DOT_META_KEY = "dot";
80
+
81
+ /**
82
+ * The string node column holding the `rank` of the non-cluster subgraph a node is in (`same`,
83
+ * `min`, `max`, `source` or `sink`): Graphviz's dot puts the subgraph's nodes on one rank, or on
84
+ * the first or last. Set only on nodes of such a subgraph.
85
+ * @category Plugin helpers
86
+ */
87
+ export const RANK_COLUMN = "graphty.rank";
88
+
89
+ /**
90
+ * The string node column naming the rank subgraph a node is in (`graphty.rank`): the subgraph's
91
+ * name, or `%<n>` for the n-th anonymous one, so the nodes of one subgraph share a value.
92
+ * @category Plugin helpers
93
+ */
94
+ export const RANK_SET_COLUMN = "graphty.rankSet";