@graphty/graph-io 0.3.48 → 0.3.49

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 (570) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-Bzn3L-je.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-Bzn3L-je.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-iptrlMBb.js} +906 -164
  6. package/dist/chunks/block-iptrlMBb.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-C3p0RQxR.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-C3p0RQxR.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-DXHx7nwR.js} +16 -14
  18. package/dist/chunks/credentials-DXHx7nwR.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-DLA5kINx.js} +7 -2
  24. package/dist/chunks/descriptor-DLA5kINx.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-BwHQkZle.js} +1660 -133
  26. package/dist/chunks/emit-BwHQkZle.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-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  32. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  33. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-D2CETOMK.js} +1222 -156
  34. package/dist/chunks/exporter-D2CETOMK.js.map +1 -0
  35. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-D9yMtocA.js} +7 -7
  36. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-D9yMtocA.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-_jrBTnos.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-_jrBTnos.js.map} +1 -1
  39. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-nDfxIK5D.js} +9 -8
  40. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-nDfxIK5D.js.map} +1 -1
  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-ymWCETAJ.js} +387 -31
  44. package/dist/chunks/generic-ymWCETAJ.js.map +1 -0
  45. package/dist/chunks/{importer-e3huMZbm.js → importer-B92x1xY5.js} +14 -10
  46. package/dist/chunks/importer-B92x1xY5.js.map +1 -0
  47. package/dist/chunks/{importer-B87_fmVx.js → importer-CT9mJ1nn.js} +326 -84
  48. package/dist/chunks/importer-CT9mJ1nn.js.map +1 -0
  49. package/dist/chunks/{importer-CB_zV5_L.js → importer-DaLTjbds.js} +116 -1102
  50. package/dist/chunks/importer-DaLTjbds.js.map +1 -0
  51. package/dist/chunks/{importer-DPZDN40-.js → importer-GREKMdIf.js} +25 -17
  52. package/dist/chunks/importer-GREKMdIf.js.map +1 -0
  53. package/dist/chunks/{importer-Bji2MeWr.js → importer-PqaaKKnY.js} +13 -10
  54. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-PqaaKKnY.js.map} +1 -1
  55. package/dist/chunks/{importer-Cyy6UseH.js → importer-Vn5QvmmJ.js} +66 -13
  56. package/dist/chunks/importer-Vn5QvmmJ.js.map +1 -0
  57. package/dist/chunks/{importer-CVT-JyDE.js → importer-vRq4h3j5.js} +1469 -356
  58. package/dist/chunks/importer-vRq4h3j5.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-Clf4QIyl.js} +282 -74
  64. package/dist/chunks/lexicon-Clf4QIyl.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-Zm3_ivgq.js +79 -0
  72. package/dist/chunks/round-trip-Zm3_ivgq.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-EokFic1V.js} +5 -5
  74. package/dist/chunks/sigv4-EokFic1V.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CCs214_3.js} +1237 -138
  76. package/dist/chunks/styles-CCs214_3.js.map +1 -0
  77. package/dist/chunks/styles-Cm62yyQX.js +2812 -0
  78. package/dist/chunks/styles-Cm62yyQX.js.map +1 -0
  79. package/dist/chunks/styles-DPQQIig4.js +1928 -0
  80. package/dist/chunks/styles-DPQQIig4.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-UMFdNqze.js} +503 -172
  82. package/dist/chunks/styles-yed-UMFdNqze.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 +5 -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 +847 -193
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +232 -220
  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 +138 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +16 -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 +405 -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/importer.d.ts.map +1 -1
  137. package/dist/src/formats/dot/importer.js +108 -31
  138. package/dist/src/formats/dot/importer.js.map +1 -1
  139. package/dist/src/formats/dot/label-places.d.ts +34 -0
  140. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  141. package/dist/src/formats/dot/label-places.js +179 -0
  142. package/dist/src/formats/dot/label-places.js.map +1 -0
  143. package/dist/src/formats/dot/record.d.ts +50 -0
  144. package/dist/src/formats/dot/record.d.ts.map +1 -0
  145. package/dist/src/formats/dot/record.js +275 -0
  146. package/dist/src/formats/dot/record.js.map +1 -0
  147. package/dist/src/formats/dot/styles.d.ts +62 -2
  148. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  149. package/dist/src/formats/dot/styles.js +1454 -119
  150. package/dist/src/formats/dot/styles.js.map +1 -1
  151. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  152. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  153. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  154. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  155. package/dist/src/formats/gexf/importer.d.ts +7 -0
  156. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  157. package/dist/src/formats/gexf/importer.js +41 -5
  158. package/dist/src/formats/gexf/importer.js.map +1 -1
  159. package/dist/src/formats/gexf/preview.d.ts +77 -0
  160. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  161. package/dist/src/formats/gexf/preview.js +191 -0
  162. package/dist/src/formats/gexf/preview.js.map +1 -0
  163. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  164. package/dist/src/formats/gexf/schema.js +2 -1
  165. package/dist/src/formats/gexf/schema.js.map +1 -1
  166. package/dist/src/formats/gexf/styles.d.ts +58 -31
  167. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  168. package/dist/src/formats/gexf/styles.js +848 -225
  169. package/dist/src/formats/gexf/styles.js.map +1 -1
  170. package/dist/src/formats/gml/importer.d.ts +5 -2
  171. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  172. package/dist/src/formats/gml/importer.js +258 -71
  173. package/dist/src/formats/gml/importer.js.map +1 -1
  174. package/dist/src/formats/gml/index.d.ts +1 -1
  175. package/dist/src/formats/gml/index.d.ts.map +1 -1
  176. package/dist/src/formats/gml/index.js +1 -1
  177. package/dist/src/formats/gml/index.js.map +1 -1
  178. package/dist/src/formats/gml/positions.d.ts +7 -0
  179. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  180. package/dist/src/formats/gml/positions.js +29 -7
  181. package/dist/src/formats/gml/positions.js.map +1 -1
  182. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  183. package/dist/src/formats/gml/styles.js +753 -101
  184. package/dist/src/formats/gml/styles.js.map +1 -1
  185. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  186. package/dist/src/formats/graphml/styles-plain.js +6 -10
  187. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  188. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  189. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  190. package/dist/src/formats/graphml/styles-yed.js +692 -161
  191. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  192. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  193. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  194. package/dist/src/formats/graphml/styles-yfiles3.js +429 -64
  195. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  196. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  197. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/yfiles.js +16 -6
  199. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  200. package/dist/src/formats/json/dialect.d.ts +3 -2
  201. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  202. package/dist/src/formats/json/dialect.js +39 -5
  203. package/dist/src/formats/json/dialect.js.map +1 -1
  204. package/dist/src/formats/json/importer.d.ts +13 -2
  205. package/dist/src/formats/json/importer.d.ts.map +1 -1
  206. package/dist/src/formats/json/importer.js +42 -6
  207. package/dist/src/formats/json/importer.js.map +1 -1
  208. package/dist/src/formats/json/index.d.ts +1 -1
  209. package/dist/src/formats/json/index.d.ts.map +1 -1
  210. package/dist/src/formats/json/index.js +1 -1
  211. package/dist/src/formats/json/index.js.map +1 -1
  212. package/dist/src/formats/json/positions.d.ts +5 -2
  213. package/dist/src/formats/json/positions.d.ts.map +1 -1
  214. package/dist/src/formats/json/positions.js +20 -4
  215. package/dist/src/formats/json/positions.js.map +1 -1
  216. package/dist/src/formats/json/styles.d.ts +12 -1
  217. package/dist/src/formats/json/styles.d.ts.map +1 -1
  218. package/dist/src/formats/json/styles.js +1755 -136
  219. package/dist/src/formats/json/styles.js.map +1 -1
  220. package/dist/src/formats/pajek/draw.d.ts +96 -0
  221. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  222. package/dist/src/formats/pajek/draw.js +372 -0
  223. package/dist/src/formats/pajek/draw.js.map +1 -0
  224. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  225. package/dist/src/formats/pajek/exporter.js +70 -51
  226. package/dist/src/formats/pajek/exporter.js.map +1 -1
  227. package/dist/src/formats/pajek/importer.d.ts +9 -0
  228. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  229. package/dist/src/formats/pajek/importer.js +233 -102
  230. package/dist/src/formats/pajek/importer.js.map +1 -1
  231. package/dist/src/formats/pajek/index.d.ts +1 -0
  232. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  233. package/dist/src/formats/pajek/index.js.map +1 -1
  234. package/dist/src/formats/pajek/styles.d.ts +15 -7
  235. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  236. package/dist/src/formats/pajek/styles.js +1278 -114
  237. package/dist/src/formats/pajek/styles.js.map +1 -1
  238. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  239. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  240. package/dist/src/formats/pajek/syntax.js +26 -2
  241. package/dist/src/formats/pajek/syntax.js.map +1 -1
  242. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  243. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  244. package/dist/src/formats/xgmml/emit.js +65 -5
  245. package/dist/src/formats/xgmml/emit.js.map +1 -1
  246. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  247. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  248. package/dist/src/formats/xgmml/styles.js +277 -60
  249. package/dist/src/formats/xgmml/styles.js.map +1 -1
  250. package/dist/src/index.d.ts +4 -3
  251. package/dist/src/index.d.ts.map +1 -1
  252. package/dist/src/index.js +4 -3
  253. package/dist/src/index.js.map +1 -1
  254. package/dist/src/positions/convert.d.ts.map +1 -1
  255. package/dist/src/positions/convert.js +11 -1
  256. package/dist/src/positions/convert.js.map +1 -1
  257. package/dist/src/positions/extract.d.ts.map +1 -1
  258. package/dist/src/positions/extract.js +53 -1
  259. package/dist/src/positions/extract.js.map +1 -1
  260. package/dist/src/positions/index.d.ts +1 -1
  261. package/dist/src/positions/index.d.ts.map +1 -1
  262. package/dist/src/positions/types.d.ts +59 -0
  263. package/dist/src/positions/types.d.ts.map +1 -1
  264. package/dist/src/sources/builtins.d.ts +1 -1
  265. package/dist/src/sources/builtins.d.ts.map +1 -1
  266. package/dist/src/sources/builtins.js +8 -2
  267. package/dist/src/sources/builtins.js.map +1 -1
  268. package/dist/src/sources/context.d.ts.map +1 -1
  269. package/dist/src/sources/context.js +1 -0
  270. package/dist/src/sources/context.js.map +1 -1
  271. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  272. package/dist/src/sources/cypher/descriptor.js +5 -0
  273. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  274. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  275. package/dist/src/sources/cypher/index.js +124 -18
  276. package/dist/src/sources/cypher/index.js.map +1 -1
  277. package/dist/src/sources/cypher/params.d.ts +1 -1
  278. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  279. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  280. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  281. package/dist/src/sources/cypher/query-builder.js +31 -0
  282. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  283. package/dist/src/sources/ids.d.ts +5 -0
  284. package/dist/src/sources/ids.d.ts.map +1 -1
  285. package/dist/src/sources/ids.js +5 -0
  286. package/dist/src/sources/ids.js.map +1 -1
  287. package/dist/src/sources/params.d.ts +3 -0
  288. package/dist/src/sources/params.d.ts.map +1 -1
  289. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  290. package/dist/src/sources/rest/descriptor.js +5 -0
  291. package/dist/src/sources/rest/descriptor.js.map +1 -1
  292. package/dist/src/sources/rest/source.d.ts.map +1 -1
  293. package/dist/src/sources/rest/source.js +15 -0
  294. package/dist/src/sources/rest/source.js.map +1 -1
  295. package/dist/src/sources/sigv4.d.ts +5 -3
  296. package/dist/src/sources/sigv4.d.ts.map +1 -1
  297. package/dist/src/sources/sigv4.js +6 -4
  298. package/dist/src/sources/sigv4.js.map +1 -1
  299. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  300. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  301. package/dist/src/sources/sparql/descriptor.js +64 -0
  302. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  303. package/dist/src/sources/sparql/index.d.ts +28 -0
  304. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  305. package/dist/src/sources/sparql/index.js +314 -0
  306. package/dist/src/sources/sparql/index.js.map +1 -0
  307. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  308. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  309. package/dist/src/sources/sparql/ntriples.js +180 -0
  310. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  311. package/dist/src/sources/sparql/params.d.ts +27 -0
  312. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  313. package/dist/src/sources/sparql/params.js +5 -0
  314. package/dist/src/sources/sparql/params.js.map +1 -0
  315. package/dist/src/sources/sparql/query.d.ts +72 -0
  316. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  317. package/dist/src/sources/sparql/query.js +113 -0
  318. package/dist/src/sources/sparql/query.js.map +1 -0
  319. package/dist/src/sources/types.d.ts +31 -0
  320. package/dist/src/sources/types.d.ts.map +1 -1
  321. package/dist/src/styles/annotations.d.ts +61 -5
  322. package/dist/src/styles/annotations.d.ts.map +1 -1
  323. package/dist/src/styles/annotations.js +130 -22
  324. package/dist/src/styles/annotations.js.map +1 -1
  325. package/dist/src/styles/canon/bends.d.ts +46 -0
  326. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  327. package/dist/src/styles/canon/bends.js +123 -0
  328. package/dist/src/styles/canon/bends.js.map +1 -1
  329. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  330. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  331. package/dist/src/styles/canon/color-tables.js +10 -4
  332. package/dist/src/styles/canon/color-tables.js.map +1 -1
  333. package/dist/src/styles/canon/color.d.ts +3 -1
  334. package/dist/src/styles/canon/color.d.ts.map +1 -1
  335. package/dist/src/styles/canon/color.js +103 -3
  336. package/dist/src/styles/canon/color.js.map +1 -1
  337. package/dist/src/styles/canon/enums.d.ts +2 -2
  338. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  339. package/dist/src/styles/canon/enums.js +81 -7
  340. package/dist/src/styles/canon/enums.js.map +1 -1
  341. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  342. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  343. package/dist/src/styles/canon/graphviz.js +57 -9
  344. package/dist/src/styles/canon/graphviz.js.map +1 -1
  345. package/dist/src/styles/canon/number.d.ts +8 -0
  346. package/dist/src/styles/canon/number.d.ts.map +1 -1
  347. package/dist/src/styles/canon/number.js +18 -0
  348. package/dist/src/styles/canon/number.js.map +1 -1
  349. package/dist/src/styles/canon/text.d.ts +29 -2
  350. package/dist/src/styles/canon/text.d.ts.map +1 -1
  351. package/dist/src/styles/canon/text.js +156 -16
  352. package/dist/src/styles/canon/text.js.map +1 -1
  353. package/dist/src/styles/codes.d.ts +2 -0
  354. package/dist/src/styles/codes.d.ts.map +1 -1
  355. package/dist/src/styles/codes.js +2 -0
  356. package/dist/src/styles/codes.js.map +1 -1
  357. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  358. package/dist/src/styles/cyjs/cascade.js +1015 -79
  359. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  360. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  361. package/dist/src/styles/cyjs/defaults-table.js +3 -0
  362. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  363. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  364. package/dist/src/styles/cyjs/emit.js +202 -9
  365. package/dist/src/styles/cyjs/emit.js.map +1 -1
  366. package/dist/src/styles/cyjs/face.d.ts +20 -0
  367. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  368. package/dist/src/styles/cyjs/face.js +332 -0
  369. package/dist/src/styles/cyjs/face.js.map +1 -0
  370. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  371. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  372. package/dist/src/styles/cyjs/mappers.js +461 -26
  373. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  374. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  375. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  376. package/dist/src/styles/cyjs/routes.js +360 -0
  377. package/dist/src/styles/cyjs/routes.js.map +1 -0
  378. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  379. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  380. package/dist/src/styles/cytoscape/charts.js +560 -0
  381. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  382. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  383. package/dist/src/styles/cytoscape/emit.js +156 -48
  384. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  385. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  386. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  387. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  388. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  389. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  390. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  391. package/dist/src/styles/cytoscape/mappings.js +92 -0
  392. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  393. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  394. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  395. package/dist/src/styles/cytoscape/resolve.js +623 -17
  396. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  397. package/dist/src/styles/defaults.d.ts +46 -26
  398. package/dist/src/styles/defaults.d.ts.map +1 -1
  399. package/dist/src/styles/defaults.js +308 -71
  400. package/dist/src/styles/defaults.js.map +1 -1
  401. package/dist/src/styles/index.d.ts +4 -3
  402. package/dist/src/styles/index.d.ts.map +1 -1
  403. package/dist/src/styles/index.js +3 -2
  404. package/dist/src/styles/index.js.map +1 -1
  405. package/dist/src/styles/internal.d.ts +1 -0
  406. package/dist/src/styles/internal.d.ts.map +1 -1
  407. package/dist/src/styles/internal.js +1 -0
  408. package/dist/src/styles/internal.js.map +1 -1
  409. package/dist/src/styles/io/generic.d.ts.map +1 -1
  410. package/dist/src/styles/io/generic.js +56 -16
  411. package/dist/src/styles/io/generic.js.map +1 -1
  412. package/dist/src/styles/io/round-trip.d.ts +56 -0
  413. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  414. package/dist/src/styles/io/round-trip.js +138 -0
  415. package/dist/src/styles/io/round-trip.js.map +1 -0
  416. package/dist/src/styles/io/writer.d.ts.map +1 -1
  417. package/dist/src/styles/io/writer.js +6 -1
  418. package/dist/src/styles/io/writer.js.map +1 -1
  419. package/dist/src/styles/notes.d.ts.map +1 -1
  420. package/dist/src/styles/notes.js +1 -0
  421. package/dist/src/styles/notes.js.map +1 -1
  422. package/dist/src/styles/plan/features.d.ts +1 -1
  423. package/dist/src/styles/plan/features.d.ts.map +1 -1
  424. package/dist/src/styles/plan/features.js +22 -2
  425. package/dist/src/styles/plan/features.js.map +1 -1
  426. package/dist/src/styles/plan/index.d.ts +1 -1
  427. package/dist/src/styles/plan/index.d.ts.map +1 -1
  428. package/dist/src/styles/plan/index.js +1 -1
  429. package/dist/src/styles/plan/index.js.map +1 -1
  430. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  431. package/dist/src/styles/plan/plan.js +61 -4
  432. package/dist/src/styles/plan/plan.js.map +1 -1
  433. package/dist/src/styles/plan/resolve.d.ts +11 -0
  434. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  435. package/dist/src/styles/plan/resolve.js +40 -4
  436. package/dist/src/styles/plan/resolve.js.map +1 -1
  437. package/dist/src/styles/slots.d.ts +14 -0
  438. package/dist/src/styles/slots.d.ts.map +1 -0
  439. package/dist/src/styles/slots.js +20 -0
  440. package/dist/src/styles/slots.js.map +1 -0
  441. package/dist/src/styles/types.d.ts +622 -6
  442. package/dist/src/styles/types.d.ts.map +1 -1
  443. package/dist/src/styles/vocabulary.d.ts +3 -2
  444. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  445. package/dist/src/styles/vocabulary.js +203 -0
  446. package/dist/src/styles/vocabulary.js.map +1 -1
  447. package/dist/styles.js +30 -239
  448. package/dist/styles.js.map +1 -1
  449. package/dist/xgmml.js +3 -3
  450. package/package.json +27 -7
  451. package/src/formats/cx/importer.ts +6 -1
  452. package/src/formats/cx/styles.ts +143 -9
  453. package/src/formats/cx2/importer.ts +1 -0
  454. package/src/formats/cx2/styles.ts +3 -1
  455. package/src/formats/cys/importer.ts +1 -0
  456. package/src/formats/cys/styles.ts +27 -5
  457. package/src/formats/dot/drawing.ts +799 -0
  458. package/src/formats/dot/importer.ts +120 -35
  459. package/src/formats/dot/label-places.ts +228 -0
  460. package/src/formats/dot/record.ts +336 -0
  461. package/src/formats/dot/styles.ts +1654 -153
  462. package/src/formats/gexf/dialog-metrics.ts +61 -0
  463. package/src/formats/gexf/importer.ts +50 -5
  464. package/src/formats/gexf/preview.ts +217 -0
  465. package/src/formats/gexf/schema.ts +2 -1
  466. package/src/formats/gexf/styles.ts +1099 -245
  467. package/src/formats/gml/importer.ts +291 -76
  468. package/src/formats/gml/index.ts +1 -1
  469. package/src/formats/gml/positions.ts +29 -7
  470. package/src/formats/gml/styles.ts +851 -109
  471. package/src/formats/graphml/styles-plain.ts +6 -15
  472. package/src/formats/graphml/styles-yed.ts +762 -173
  473. package/src/formats/graphml/styles-yfiles3.ts +485 -63
  474. package/src/formats/graphml/yfiles.ts +19 -6
  475. package/src/formats/json/dialect.ts +41 -5
  476. package/src/formats/json/importer.ts +59 -7
  477. package/src/formats/json/index.ts +1 -0
  478. package/src/formats/json/positions.ts +22 -5
  479. package/src/formats/json/styles.ts +2041 -185
  480. package/src/formats/pajek/draw.ts +458 -0
  481. package/src/formats/pajek/exporter.ts +81 -49
  482. package/src/formats/pajek/importer.ts +278 -103
  483. package/src/formats/pajek/index.ts +1 -0
  484. package/src/formats/pajek/styles.ts +1381 -133
  485. package/src/formats/pajek/syntax.ts +27 -2
  486. package/src/formats/xgmml/emit.ts +73 -5
  487. package/src/formats/xgmml/styles.ts +330 -71
  488. package/src/index.ts +5 -1
  489. package/src/positions/convert.ts +12 -1
  490. package/src/positions/extract.ts +55 -1
  491. package/src/positions/index.ts +1 -0
  492. package/src/positions/types.ts +60 -0
  493. package/src/sources/builtins.ts +8 -1
  494. package/src/sources/context.ts +1 -0
  495. package/src/sources/cypher/descriptor.ts +7 -1
  496. package/src/sources/cypher/index.ts +148 -18
  497. package/src/sources/cypher/params.ts +1 -1
  498. package/src/sources/cypher/query-builder.ts +33 -0
  499. package/src/sources/ids.ts +6 -0
  500. package/src/sources/params.ts +3 -0
  501. package/src/sources/rest/descriptor.ts +5 -0
  502. package/src/sources/rest/source.ts +16 -0
  503. package/src/sources/sigv4.ts +6 -4
  504. package/src/sources/sparql/descriptor.ts +67 -0
  505. package/src/sources/sparql/index.ts +356 -0
  506. package/src/sources/sparql/ntriples.ts +219 -0
  507. package/src/sources/sparql/params.ts +28 -0
  508. package/src/sources/sparql/query.ts +162 -0
  509. package/src/sources/types.ts +35 -0
  510. package/src/styles/annotations.ts +201 -27
  511. package/src/styles/canon/bends.ts +137 -0
  512. package/src/styles/canon/color-tables.ts +10 -4
  513. package/src/styles/canon/color.ts +105 -3
  514. package/src/styles/canon/enums.ts +98 -8
  515. package/src/styles/canon/graphviz.ts +71 -10
  516. package/src/styles/canon/number.ts +19 -0
  517. package/src/styles/canon/text.ts +187 -16
  518. package/src/styles/codes.ts +2 -0
  519. package/src/styles/cyjs/cascade.ts +1160 -84
  520. package/src/styles/cyjs/defaults-table.ts +3 -0
  521. package/src/styles/cyjs/emit.ts +233 -13
  522. package/src/styles/cyjs/face.ts +372 -0
  523. package/src/styles/cyjs/mappers.ts +492 -27
  524. package/src/styles/cyjs/routes.ts +432 -0
  525. package/src/styles/cytoscape/charts.ts +684 -0
  526. package/src/styles/cytoscape/emit.ts +170 -48
  527. package/src/styles/cytoscape/lexicon.ts +431 -78
  528. package/src/styles/cytoscape/mappings.ts +108 -0
  529. package/src/styles/cytoscape/resolve.ts +741 -17
  530. package/src/styles/defaults.ts +417 -108
  531. package/src/styles/index.ts +5 -0
  532. package/src/styles/internal.ts +1 -0
  533. package/src/styles/io/generic.ts +79 -23
  534. package/src/styles/io/round-trip.ts +202 -0
  535. package/src/styles/io/writer.ts +6 -1
  536. package/src/styles/notes.ts +1 -0
  537. package/src/styles/plan/features.ts +23 -2
  538. package/src/styles/plan/index.ts +1 -1
  539. package/src/styles/plan/plan.ts +69 -5
  540. package/src/styles/plan/resolve.ts +47 -4
  541. package/src/styles/slots.ts +25 -0
  542. package/src/styles/types.ts +633 -5
  543. package/src/styles/vocabulary.ts +211 -2
  544. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  545. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  546. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  547. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  548. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  549. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  550. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  551. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  552. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  553. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  554. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  555. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  556. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  557. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  558. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  559. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  560. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  561. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  562. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  563. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  564. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  565. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  566. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  567. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  568. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  569. package/dist/chunks/writer-DvHTl36J.js +0 -449
  570. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -5,7 +5,10 @@
5
5
  * of elements plus the size of the style, and spends from the extraction's one budget.
6
6
  */
7
7
 
8
- import type { CanonSite } from "../canon/site.js";
8
+ import type { GraphSnapshot } from "@graphty/graph-format";
9
+
10
+ import { num6 } from "../canon/number.js";
11
+ import { type CanonSite, noteAt } from "../canon/site.js";
9
12
  import { STYLE_ISSUE } from "../codes.js";
10
13
  import { toolDefaults } from "../defaults.js";
11
14
  import { StyleNotes } from "../notes.js";
@@ -21,12 +24,22 @@ import type {
21
24
  StyleWorkBudget,
22
25
  } from "../types.js";
23
26
  import { isStyleValue } from "../vocabulary.js";
27
+ import { readCytoscapeCharts } from "./charts.js";
24
28
  import {
29
+ arrowGap,
30
+ arrowProportion,
31
+ arrowSizeParser,
25
32
  colorParser,
26
33
  CYTOSCAPE_LEXICON,
34
+ cytoscapeLabelBase,
35
+ cytoscapeLabelMove,
36
+ type CytoscapeLabelPlace,
37
+ cytoscapeLabelPlace,
27
38
  type CytoscapeValueParser,
39
+ halfBack,
28
40
  isUncarriedVp,
29
41
  NODE_SIZE_VP,
42
+ opacityParser,
30
43
  sizeParser,
31
44
  vpStep,
32
45
  } from "./lexicon.js";
@@ -60,6 +73,27 @@ export interface CytoscapeElements {
60
73
  value(column: string, i: number): unknown;
61
74
  /** Each element's bypasses, by element index. */
62
75
  readonly bypasses?: readonly CytoscapeBypass[];
76
+ /**
77
+ * Edges only: each edge's source and target node index, in the order Cytoscape created the edges. With them,
78
+ * the edges Cytoscape desktop fans apart (parallel edges without bends) are marked curved; with `nodeSize` too
79
+ * (each node's larger drawn side, by node index, as `fanNodeSizes()` reads it), they are given the `fanSpacing`
80
+ * desktop spaces them by. With `nodeBox` (each node's width, height and border width, by node index), each
81
+ * self-loop without bends is given the two handles desktop draws it round.
82
+ */
83
+ readonly ends?: {
84
+ readonly source: ArrayLike<number>;
85
+ readonly target: ArrayLike<number>;
86
+ readonly nodeSize?: ArrayLike<number | null>;
87
+ readonly nodeBox?: ArrayLike<readonly [width: number, height: number, border: number] | null>;
88
+ /** Each node's center on screen, x right and y down, by node index: what an edge's label is placed by. */
89
+ readonly nodeAt?: ArrayLike<readonly [x: number, y: number] | null>;
90
+ /**
91
+ * The order Cytoscape desktop creates the file's nodes and edges in, when it is not the file's (its CX reader
92
+ * creates them in hash order): each element's rank, lower first, by index. With it, `nodeAt` and `nodeSize`,
93
+ * each fanned edge is given desktop's own anchor as its bend, so it bows to the side desktop draws it on.
94
+ */
95
+ readonly created?: { readonly node: ArrayLike<number>; readonly edge: ArrayLike<number> };
96
+ };
63
97
  }
64
98
 
65
99
  /**
@@ -77,11 +111,101 @@ export interface CytoscapeResolveOptions {
77
111
  * element the style says nothing about is null. Defaults to null.
78
112
  */
79
113
  readonly defaults?: StyleTool | null;
114
+ /** `resolveCytoscapeBypasses()` only: the edges' ends, as `CytoscapeElements.ends`. */
115
+ readonly ends?: CytoscapeElements["ends"];
116
+ /**
117
+ * `resolveCytoscapeBypasses()` only: an element's value of an attribute column, as `CytoscapeElements.value`,
118
+ * for what is drawn from the element's data (a chart's values); without it no element has any.
119
+ */
120
+ readonly value?: CytoscapeElements["value"];
121
+ }
122
+
123
+ /** How closely desktop stacks the edges it fans, 0..1: its anchors are twice this times the node size apart. */
124
+ const EDGE_STACKING_DENSITY_VP = "EDGE_STACKING_DENSITY";
125
+ /** How desktop draws parallel edges: AUTO_BEND (the default) fans them, HAYSTACK draws them straight. */
126
+ const EDGE_STACKING_VP = "EDGE_STACKING";
127
+ /** Whether an edge's label is turned along its line. */
128
+ const AUTOROTATE_VP = "EDGE_LABEL_AUTOROTATE";
129
+
130
+ /**
131
+ * A map without one key.
132
+ * @param m - the map
133
+ * @param key - the key left out
134
+ * @returns a copy without it
135
+ */
136
+ function withoutKey(m: ReadonlyMap<string, unknown>, key: string): ReadonlyMap<string, unknown> {
137
+ return new Map([...m].filter(([k]) => k !== key));
138
+ }
139
+ /** Desktop's `EDGE_STACKING_DENSITY` default. */
140
+ const EDGE_STACKING_DENSITY_DEFAULT = 0.5;
141
+
142
+ /**
143
+ * A graph's edge ends for `CytoscapeElements.ends`, with each node's larger side from the nodes' resolved `width`
144
+ * and `height`: what desktop spaces the edges it fans between two nodes by.
145
+ * @param snapshot - the graph
146
+ * @param nodes - the nodes' values per property
147
+ * @returns the ends
148
+ */
149
+ export function edgeEnds(
150
+ snapshot: GraphSnapshot,
151
+ nodes: Partial<Record<string, StyleColumn>>,
152
+ ): NonNullable<CytoscapeElements["ends"]> {
153
+ const nodeSize = Array.from({ length: snapshot.nodeCount }, (_, i) => {
154
+ const sides = [nodes.width?.[i], nodes.height?.[i]].filter((v): v is number => typeof v === "number");
155
+ return sides.length === 0 ? null : Math.max(...sides);
156
+ });
157
+ const nodeBox = Array.from({ length: snapshot.nodeCount }, (_, i) => {
158
+ const [w, h, b] = [nodes.width?.[i], nodes.height?.[i], nodes.borderWidth?.[i]];
159
+ return typeof w === "number" && typeof h === "number" ? ([w, h, typeof b === "number" ? b : 0] as const) : null;
160
+ });
161
+ const position = snapshot.nodes.byRole("position");
162
+ // stored y up unless the column says otherwise; the label places are worked out y down, as Cytoscape draws
163
+ const down = (position?.meta.extra as { yAxis?: string } | undefined)?.yAxis === "down";
164
+ const nodeAt = Array.from({ length: snapshot.nodeCount }, (_, i) => {
165
+ const v = position?.isSet(i) === true ? (position.value(i) as ArrayLike<number>) : null;
166
+ return v === null ? null : ([v[0], down ? v[1] : -v[1]] as const);
167
+ });
168
+ const { src, dst } = snapshot.edgeList();
169
+ return { source: src, target: dst, nodeSize, nodeBox, nodeAt };
80
170
  }
81
171
 
82
172
  /** Locked node sizes with no `NODE_SIZE` anywhere draw at Cytoscape's lexicon default. */
83
173
  const NODE_SIZE_FALLBACK = 50;
84
174
 
175
+ /**
176
+ * The visual property whose bypass each one takes when an element does not bypass it itself: Cytoscape desktop locks
177
+ * a bypassed parent's children too, so a session's `NODE_PAINT` bypass paints the fill and the border, a `NODE_SIZE`
178
+ * bypass sizes the node whatever `nodeSizeLocked` says, and an `EDGE_PAINT` or `EDGE_UNSELECTED_PAINT` bypass paints
179
+ * the line and its arrows (Cytoscape 3.10.5's SVG of the session specimen sheets `node-fills-borders`,
180
+ * `node-sizes-opacity` and `edges-lines`).
181
+ */
182
+ const LOCK_PARENT: ReadonlyMap<string, string> = new Map([
183
+ ["NODE_FILL_COLOR", "NODE_PAINT"],
184
+ ["NODE_BORDER_PAINT", "NODE_PAINT"],
185
+ ["NODE_WIDTH", "NODE_SIZE"],
186
+ ["NODE_HEIGHT", "NODE_SIZE"],
187
+ ["EDGE_UNSELECTED_PAINT", "EDGE_PAINT"],
188
+ ["EDGE_STROKE_UNSELECTED_PAINT", "EDGE_UNSELECTED_PAINT"],
189
+ ["EDGE_SOURCE_ARROW_UNSELECTED_PAINT", "EDGE_UNSELECTED_PAINT"],
190
+ ["EDGE_TARGET_ARROW_UNSELECTED_PAINT", "EDGE_UNSELECTED_PAINT"],
191
+ ]);
192
+
193
+ /**
194
+ * An element's bypass of a visual property: its own, else the nearest bypassed parent's (`LOCK_PARENT`).
195
+ * @param b - the element's bypasses
196
+ * @param vp - the visual property
197
+ * @returns the bypass value, or undefined
198
+ */
199
+ function lockedValue(b: NonNullable<CytoscapeBypass>, vp: string): unknown {
200
+ for (let p: string | undefined = vp; p !== undefined; p = LOCK_PARENT.get(p)) {
201
+ const v = b.get(p);
202
+ if (v !== undefined && v !== null) {
203
+ return v;
204
+ }
205
+ }
206
+ return undefined;
207
+ }
208
+
85
209
  /** A visual property and the parser that reads it for one neutral property. */
86
210
  type VpChoice = readonly [vp: string, parse: CytoscapeValueParser];
87
211
 
@@ -108,6 +232,12 @@ export function vpUsage(
108
232
  bypassed.add(to);
109
233
  }
110
234
  }
235
+ // a bypassed parent bypasses its children (LOCK_PARENT lists a parent's own entry before its children's)
236
+ for (const [child, parent] of LOCK_PARENT) {
237
+ if (bypassed.has(parent)) {
238
+ bypassed.add(child);
239
+ }
240
+ }
111
241
  const used = new Set([...style.defaults.keys(), ...style.mappings.keys(), ...bypassed]);
112
242
  return { used, bypassed };
113
243
  }
@@ -137,7 +267,8 @@ export function chooseVps(
137
267
  chosen.delete("height");
138
268
  }
139
269
  if (target === "edge" && style.dependencies.get("arrowColorMatchesEdge") === true) {
140
- if (used.has("EDGE_UNSELECTED_PAINT")) {
270
+ // the line is EDGE_UNSELECTED_PAINT's, not the stroke's, even where the style sets only the stroke
271
+ if (used.has("EDGE_UNSELECTED_PAINT") || used.has("EDGE_STROKE_UNSELECTED_PAINT")) {
141
272
  chosen.set("color", ["EDGE_UNSELECTED_PAINT", colorParser]);
142
273
  }
143
274
  chosen.delete("sourceArrowColor");
@@ -299,7 +430,7 @@ function resolver(
299
430
  return b.get(from);
300
431
  }
301
432
  }
302
- return undefined;
433
+ return lockedValue(b, vp);
303
434
  };
304
435
  // a mapping parsed once per visual property and neutral property
305
436
  const mappingOf = (vp: string, parse: CytoscapeValueParser, property: string): ParsedMapping | undefined => {
@@ -337,7 +468,7 @@ function resolver(
337
468
  /**
338
469
  * Every element's value per neutral property under a Cytoscape style, the way Cytoscape draws it: per visual
339
470
  * property a bypass, else the mapping's value, else the default; `nodeSizeLocked` and `arrowColorMatchesEdge`
340
- * applied. `EDGE_CURVED` only rounds an edge's bends, so an edge without bends is `curved: false`. Visual properties
471
+ * applied (under `nodeSizeLocked` a node's own `NODE_WIDTH` or `NODE_HEIGHT` bypass still sets that dimension). `EDGE_CURVED` only rounds an edge's bends, so an edge without bends is `curved: false` unless desktop fans it out of a bundle of parallel edges (given `elements.ends`). Visual properties
341
472
  * no neutral property carries are noted as `W_STYLE_NOT_CARRIED`.
342
473
  * @param target - nodes or edges
343
474
  * @param style - the style
@@ -357,7 +488,7 @@ export function resolveCytoscape(
357
488
  const n = elements.count;
358
489
  const { used, bypassed } = vpUsage(s, elements.bypasses ?? []);
359
490
  const chosen = chooseVps(target, s, used);
360
- const { one } = resolver(target, s, elements, notes, options.budget);
491
+ const { one, bypass } = resolver(target, s, elements, notes, options.budget);
361
492
  const out: Partial<Record<string, StyleColumn>> = {};
362
493
  const column = (vp: string, parse: CytoscapeValueParser, property: string): (StyleValue | null)[] => {
363
494
  options.budget?.spend(target, n);
@@ -371,31 +502,581 @@ export function resolveCytoscape(
371
502
  out[prop] = column(vp, parse, prop);
372
503
  }
373
504
  if (target === "node" && s.dependencies.get("nodeSizeLocked") === true) {
505
+ // a node's own width or height still wins over the locked size: Cytoscape draws it
374
506
  const size = column(NODE_SIZE_VP, sizeParser, "width").map((v) => v ?? NODE_SIZE_FALLBACK);
375
- out.width = size;
376
- out.height = size.slice();
507
+ const own = (vp: string, property: string): StyleColumn =>
508
+ size.map((v, i) => {
509
+ const b = bypass(i, vp);
510
+ const parsed =
511
+ b === undefined || b === null ? null : sizeParser(b, { notes, target, property, element: i });
512
+ return parsed ?? v;
513
+ });
514
+ out.width = own("NODE_WIDTH", "width");
515
+ out.height = own("NODE_HEIGHT", "height");
516
+ }
517
+ labelDress(target, out, one, n, elements.ends);
518
+ if (target === "node") {
519
+ squashHexagons(out);
520
+ // the value as written: a chart is text, or an object in CX2; a size a number or its text
521
+ const raw: CytoscapeValueParser = (v) => {
522
+ if (typeof v === "string" || typeof v === "number") {
523
+ return String(v);
524
+ }
525
+ return typeof v === "object" && v !== null ? JSON.stringify(v) : null;
526
+ };
527
+ readCytoscapeCharts(
528
+ out,
529
+ n,
530
+ (slot, i) => {
531
+ const v = one(slot, raw, "pieColors", i);
532
+ return typeof v === "string" && v.startsWith("{") ? (JSON.parse(v) as unknown) : v;
533
+ },
534
+ (column, i) => elements.value(column, i),
535
+ s.dependencies.get("nodeCustomGraphicsSizeSync") !== false,
536
+ );
537
+ }
538
+ if (
539
+ target === "edge" &&
540
+ s.dependencies.get("arrowColorMatchesEdge") === true &&
541
+ !used.has("EDGE_UNSELECTED_PAINT") &&
542
+ used.has("EDGE_STROKE_UNSELECTED_PAINT")
543
+ ) {
544
+ // the dependency draws the line and the arrows in EDGE_UNSELECTED_PAINT, Cytoscape's own when the style has
545
+ // none: the stroke's default and its mapping are not drawn, only an element's own bypass of the stroke (below;
546
+ // Cytoscape 3.10.5 on specimen-mappings-bypass draws a passthrough of the stroke #404040, as its XGMML says)
547
+ const paint = (toolDefaults("cytoscape-fallbacks")?.edge.color?.value ?? null) as StyleValue | null;
548
+ out.color = Array.from({ length: n }, (_, i) => one("EDGE_UNSELECTED_PAINT", colorParser, "color", i) ?? paint);
377
549
  }
378
550
  if (target === "edge" && s.dependencies.get("arrowColorMatchesEdge") === true && out.color !== undefined) {
379
- out.sourceArrowColor = out.color.slice();
380
- out.targetArrowColor = out.color.slice();
551
+ // CX2's EDGE_LINE_COLOR bypass is the stroke's alone (its writer spells a desktop EDGE_UNSELECTED_PAINT
552
+ // bypass out as the line and both arrow colors): the arrows keep the style's line color (Cytoscape 3.10.5's
553
+ // SVG of the CX2 specimen-mappings-bypass, bypass-over-edge-passthrough, draws its arrow #505050)
554
+ const lineVp = chosen.get("color")?.[0];
555
+ const unbypassed = resolver(target, s, { ...elements, bypasses: [] }, notes, options.budget).one;
556
+ const arrows =
557
+ lineVp === "EDGE_LINE_COLOR"
558
+ ? Array.from({ length: n }, (_, i) => unbypassed(lineVp, colorParser, "color", i))
559
+ : out.color;
560
+ out.sourceArrowColor = arrows.slice();
561
+ out.targetArrowColor = arrows.slice();
562
+ // the dependency sets the line and the arrows from EDGE_UNSELECTED_PAINT; an element's own bypass of one of
563
+ // them still wins for that one (Cytoscape draws a bypassed EDGE_STROKE_UNSELECTED_PAINT line in its color)
564
+ const own: readonly [string, string][] = [
565
+ ["color", "EDGE_STROKE_UNSELECTED_PAINT"],
566
+ ["sourceArrowColor", "EDGE_SOURCE_ARROW_UNSELECTED_PAINT"],
567
+ ["targetArrowColor", "EDGE_TARGET_ARROW_UNSELECTED_PAINT"],
568
+ ["sourceArrowColor", "EDGE_SOURCE_ARROW_COLOR"],
569
+ ["targetArrowColor", "EDGE_TARGET_ARROW_COLOR"],
570
+ ];
571
+ for (const [prop, vp] of own) {
572
+ out[prop] = (out[prop] ?? []).map((v, i) => {
573
+ const b = bypass(i, vp);
574
+ const site = { notes, target, property: prop, element: i };
575
+ return (b === undefined || b === null ? null : colorParser(b, site)) ?? v;
576
+ });
577
+ }
381
578
  }
382
579
  noteUncarried(target, s, used, bypassed, notes);
383
- fillToolDefaults(target, out, n, options.defaults ?? null);
384
580
  if (target === "edge") {
385
- straightenBendless(out, one);
581
+ const bends = column(EDGE_BEND_VP, bendsParser, "bends");
582
+ if (bends.some((v) => v !== null)) {
583
+ out.bends = bends;
584
+ }
585
+ // before the tool's defaults: an edge the style leaves unset is curved as Cytoscape's EDGE_CURVED default is
586
+ const curvedAsSet = out.curved?.slice();
587
+ straightenBendless(out, one, n, elements.ends);
588
+ loopHandles(out, one, curvedAsSet, elements.ends);
589
+ arrowProportions(out, column);
386
590
  }
591
+ fillToolDefaults(target, out, n, options.defaults ?? null);
387
592
  return out;
388
593
  }
389
594
 
390
595
  /**
391
- * Marks straight every curved edge without bends: `EDGE_CURVED` only rounds the bends an edge has.
596
+ * A visual property's value as written, a CX2 object as its JSON.
597
+ * @param v - the value
598
+ * @returns its text, or null
599
+ */
600
+ const rawText: CytoscapeValueParser = (v) => {
601
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
602
+ return String(v);
603
+ }
604
+ return typeof v === "object" && v !== null ? JSON.stringify(v) : null;
605
+ };
606
+
607
+ /**
608
+ * Reads what Cytoscape draws of each element's label beyond its words, font and color: the box behind it (drawn only
609
+ * with a `*_LABEL_BACKGROUND_SHAPE` other than NONE, the default, in its color and transparency), where it is placed
610
+ * (`*_LABEL_POSITION` read whole: which point of the label's box is pinned to which point of the node, or of the
611
+ * edge's line, and the offset) and how its lines are justified. Each column is kept only where some element needs it;
612
+ * an edge's place is kept whenever it has one, since Cytoscape centers an edge's label on its line.
613
+ * @param target - nodes or edges
614
+ * @param out - the values per property, changed in place
615
+ * @param one - reads one visual property of one element
616
+ * @param n - the element count
617
+ * @param ends - the edges' ends and their nodes' centers, when known
618
+ */
619
+ function labelDress(
620
+ target: StyleTarget,
621
+ out: Partial<Record<string, StyleColumn>>,
622
+ one: Resolver["one"],
623
+ n: number,
624
+ ends: CytoscapeElements["ends"],
625
+ ): void {
626
+ const vp = (name: string): string => `${target === "node" ? "NODE" : "EDGE"}_LABEL_${name}`;
627
+ const columns: Record<string, (StyleValue | null)[]> = {};
628
+ const put = (property: string, i: number, v: StyleValue | null): void => {
629
+ let column = columns[property];
630
+ if (column === undefined) {
631
+ column = new Array<StyleValue | null>(n).fill(null);
632
+ columns[property] = column;
633
+ }
634
+ column[i] = v;
635
+ };
636
+ for (let i = 0; i < n; i++) {
637
+ labelBox(vp, one, i, put);
638
+ // read whole, quietly (the lexicon's reading of the same value notes a bad one), under a name of its own: a
639
+ // mapping is parsed once per visual property and neutral property
640
+ const placed = one(vp("POSITION"), rawText, "labelPlace", i);
641
+ const raw = typeof placed === "string" && placed.startsWith("{") ? (JSON.parse(placed) as unknown) : placed;
642
+ const place = cytoscapeLabelPlace(raw, { notes: new StyleNotes(), target, property: "labelPosition" });
643
+ if (place !== null) {
644
+ placeLabel(target, place, i, out, put, ends);
645
+ }
646
+ if (target === "edge") {
647
+ autorotate(one, i, out, n, ends);
648
+ }
649
+ }
650
+ const kept = (property: string, v: StyleValue | null): boolean =>
651
+ v !== null && v !== 0 && (v !== "center" || (property === "labelPosition" && target === "edge"));
652
+ for (const [property, values] of Object.entries(columns)) {
653
+ if (values.some((v) => kept(property, v))) {
654
+ out[property] = values;
655
+ }
656
+ }
657
+ }
658
+
659
+ /**
660
+ * The box Cytoscape draws behind one label, when its shape is not NONE: its color (`#B6B6B6` unset, the lexicon's
661
+ * default), its opacity, and for ROUND_RECTANGLE its corners, rounded an eighth of the box's height: 1.455 on the
662
+ * 11.64-tall box of a 10 px label (xgmml-specimen-labels.svg, label-background-shape-round-rectangle), so 0.1455 of the
663
+ * label's size. ponytail: one line's height; a box of several lines is rounded as one line's.
664
+ * @param vp - names a label visual property of the target
665
+ * @param one - reads one visual property of one element
666
+ * @param i - the element
667
+ * @param put - sets one element's value of a property
668
+ */
669
+ function labelBox(
670
+ vp: (name: string) => string,
671
+ one: Resolver["one"],
672
+ i: number,
673
+ put: (property: string, i: number, v: StyleValue | null) => void,
674
+ ): void {
675
+ const shape = one(vp("BACKGROUND_SHAPE"), rawText, "labelBackgroundShape", i);
676
+ if (typeof shape !== "string" || shape.trim().toUpperCase() === "NONE") {
677
+ return;
678
+ }
679
+ put("labelBackground", i, one(vp("BACKGROUND_COLOR"), colorParser, "labelBackground", i) ?? "#b6b6b6");
680
+ const [t, o] = [vp("BACKGROUND_TRANSPARENCY"), vp("BACKGROUND_OPACITY")];
681
+ const opacity =
682
+ one(t, opacityParser(t), "labelBackgroundOpacity", i) ?? one(o, opacityParser(o), "labelBackgroundOpacity", i);
683
+ put("labelBackgroundOpacity", i, opacity ?? 1);
684
+ // CX2 spells it round-rectangle
685
+ if (shape.trim().toUpperCase().replace("-", "_") === "ROUND_RECTANGLE") {
686
+ const size = one(vp("FONT_SIZE"), sizeParser, "labelSize", i);
687
+ // desktop's default label sizes: 12 on a node, 10 on an edge
688
+ const defaultSize = vp("FONT_SIZE").startsWith("EDGE") ? 10 : 12;
689
+ put("labelCornerRadius", i, num6(LABEL_CORNER_PER_SIZE * (typeof size === "number" ? size : defaultSize)));
690
+ }
691
+ }
692
+
693
+ /** How far Cytoscape rounds a ROUND_RECTANGLE label box's corners, as a fraction of the label's font size. */
694
+ const LABEL_CORNER_PER_SIZE = 0.1455;
695
+
696
+ /**
697
+ * Where one label is placed, from its Cytoscape label position: its neutral `labelPosition`, offsets, shifts and
698
+ * line justification. A node's label is pinned against the node's box. An edge's is pinned to a point of its drawn
699
+ * line, the middle unless the anchor's horizontal letter names an end (W the source's, E the target's; its vertical
700
+ * letter changes nothing, measured on Cytoscape 3.10.5's SVG), from the line's middle, where it is centered.
701
+ * @param target - nodes or edges
702
+ * @param place - the label position
703
+ * @param i - the element
704
+ * @param out - the values per property (the node sizes are read)
705
+ * @param put - sets one element's value of a property
706
+ * @param ends - the edges' ends and their nodes' centers, when known
707
+ */
708
+ function placeLabel(
709
+ target: StyleTarget,
710
+ place: CytoscapeLabelPlace,
711
+ i: number,
712
+ out: Partial<Record<string, StyleColumn>>,
713
+ put: (property: string, i: number, v: StyleValue | null) => void,
714
+ ends: CytoscapeElements["ends"],
715
+ ): void {
716
+ let move: ReturnType<typeof cytoscapeLabelMove>;
717
+ if (target === "node") {
718
+ const [w, h] = [out.width?.[i], out.height?.[i]];
719
+ move = cytoscapeLabelMove(place, typeof w === "number" ? w : 0, typeof h === "number" ? h : 0);
720
+ put("labelPosition", i, cytoscapeLabelBase(place));
721
+ } else {
722
+ const [ax, ay] = lineEnd(ends, i, place.node[0]);
723
+ move = {
724
+ offset: [ax + place.offset[0], ay + place.offset[1]],
725
+ shift: [halfBack(place.label[0]), halfBack(place.label[1])],
726
+ };
727
+ put("labelPosition", i, "center");
728
+ }
729
+ put("labelOffsetX", i, num6(move.offset[0]));
730
+ put("labelOffsetY", i, num6(move.offset[1]));
731
+ put("labelShiftX", i, move.shift[0]);
732
+ put("labelShiftY", i, move.shift[1]);
733
+ put("labelTextAlign", i, place.justify);
734
+ }
735
+
736
+ /**
737
+ * Turns an edge's label along its line under `EDGE_LABEL_AUTOROTATE`, as Cytoscape draws it: the line's angle from
738
+ * its source's center to its target's (kept between -90 and 90 so the words read left to right), plus its
739
+ * `EDGE_LABEL_ROTATION`.
740
+ * ponytail: the angle is the file's node places at import; a layout that moves the nodes leaves it as it was, until
741
+ * the neutral styles can say "along the line"
742
+ * @param one - reads one visual property of one element
743
+ * @param e - the edge
744
+ * @param out - the values per property, changed in place
745
+ * @param n - the edge count
746
+ * @param ends - the edges' ends and their nodes' centers
747
+ */
748
+ function autorotate(
749
+ one: Resolver["one"],
750
+ e: number,
751
+ out: Partial<Record<string, StyleColumn>>,
752
+ n: number,
753
+ ends: CytoscapeElements["ends"],
754
+ ): void {
755
+ const on = one(AUTOROTATE_VP, rawText, "labelRotation", e);
756
+ const [si, ti] = [ends?.source[e], ends?.target[e]];
757
+ const [s, t] = [si === undefined ? null : ends?.nodeAt?.[si], ti === undefined ? null : ends?.nodeAt?.[ti]];
758
+ if (String(on).toLowerCase() !== "true" || !s || !t || (s[0] === t[0] && s[1] === t[1])) {
759
+ return;
760
+ }
761
+ const a = (Math.atan2(t[1] - s[1], t[0] - s[0]) * 180) / Math.PI;
762
+ let upright = a;
763
+ if (a > 90) {
764
+ upright = a - 180;
765
+ } else if (a <= -90) {
766
+ upright = a + 180;
767
+ }
768
+ out.labelRotation ??= new Array<StyleValue | null>(n).fill(null);
769
+ const column = out.labelRotation as (StyleValue | null)[];
770
+ const own = column[e];
771
+ column[e] = num6((((upright + (typeof own === "number" ? own : 0)) % 360) + 360) % 360);
772
+ }
773
+
774
+ /**
775
+ * How far from the middle of an edge's drawn line one of its ends is, on screen (y down): the line between its two
776
+ * nodes' centers, cut where it meets each node's ellipse; 0 for the middle, or where the centers are not known.
777
+ * @param ends - the edges' ends, their nodes' centers and boxes
778
+ * @param e - the edge
779
+ * @param side - -1 for the source's end, 1 for the target's, 0 for the middle
780
+ * @returns the offset from the line's middle
781
+ */
782
+ function lineEnd(ends: CytoscapeElements["ends"], e: number, side: number): readonly [number, number] {
783
+ const [si, ti] = [ends?.source[e], ends?.target[e]];
784
+ const [s, t] = [si === undefined ? null : ends?.nodeAt?.[si], ti === undefined ? null : ends?.nodeAt?.[ti]];
785
+ if (side === 0 || s === null || s === undefined || t === null || t === undefined) {
786
+ return [0, 0];
787
+ }
788
+ const length = Math.hypot(t[0] - s[0], t[1] - s[1]);
789
+ if (length === 0) {
790
+ return [0, 0];
791
+ }
792
+ const [ux, uy] = [(t[0] - s[0]) / length, (t[1] - s[1]) / length];
793
+ // where the line leaves a node's ellipse, along it from the center
794
+ const reach = (node: number | undefined): number => {
795
+ const box = node === undefined ? null : ends?.nodeBox?.[node];
796
+ if (box === null || box === undefined) {
797
+ return 0;
798
+ }
799
+ const [a, b] = [box[0] / 2, box[1] / 2];
800
+ return a === 0 || b === 0 ? 0 : (a * b) / Math.hypot(b * ux, a * uy);
801
+ };
802
+ const from = reach(si);
803
+ const to = length - reach(ti);
804
+ // the drawn line's middle and the end asked for, both measured along the line from the source's center
805
+ const along = (side < 0 ? from : to) - (from + to) / 2;
806
+ return [ux * along, uy * along];
807
+ }
808
+
809
+ /** How tall a regular hexagon is for its width. */
810
+ const HEXAGON_ASPECT = Math.sqrt(3) / 2;
811
+
812
+ /**
813
+ * Gives every hexagon node the height Cytoscape desktop draws it at. Its HEXAGON is a regular hexagon squashed to
814
+ * the node's box rather than one filling it: the corners reach the box's sides, the flat top and bottom only
815
+ * `sqrt(3) / 2` of the way up and down, so a 40 x 40 node with a border of 1 is a 39 x 33.8 path (its border is
816
+ * inside its size, `borderPosition` inside). The neutral hexagon fills its box, so the node is that much shorter.
817
+ * @param out - the node values per property, changed in place
818
+ */
819
+ function squashHexagons(out: Partial<Record<string, StyleColumn>>): void {
820
+ const { shape, height } = out;
821
+ if (shape === undefined || height === undefined || !shape.includes("hexagon")) {
822
+ return;
823
+ }
824
+ const border = out.borderWidth;
825
+ out.height = height.map((h, i) => {
826
+ if (shape[i] !== "hexagon" || typeof h !== "number") {
827
+ return h;
828
+ }
829
+ const b = border?.[i];
830
+ const w = typeof b === "number" ? Math.min(b, h) : 0;
831
+ return (h - w) * HEXAGON_ASPECT + w;
832
+ });
833
+ }
834
+
835
+ /**
836
+ * What Cytoscape draws of each end's arrow beyond its shape and the one neutral `arrowSize`: the source end's own
837
+ * size where it differs from the target's (an unset size is Cytoscape's 6), and the length and width of the short and
838
+ * crossed arrows against their neutral shape's, and how far off its node each arrow stands. Each is kept only where
839
+ * some edge needs it.
840
+ * @param out - the values per property, changed in place
841
+ * @param column - reads one visual property of every element
842
+ */
843
+ function arrowProportions(
844
+ out: Partial<Record<string, StyleColumn>>,
845
+ column: (vp: string, parse: CytoscapeValueParser, property: string) => (StyleValue | null)[],
846
+ ): void {
847
+ const size = out.arrowSize;
848
+ const own = column("EDGE_SOURCE_ARROW_SIZE", arrowSizeParser, "sourceArrowSize").map((v) => v ?? 1);
849
+ if (own.some((v, i) => v !== (size?.[i] ?? 1))) {
850
+ out.sourceArrowSize = own;
851
+ }
852
+ for (const [prop, vp, side] of [
853
+ ["sourceArrowLength", "EDGE_SOURCE_ARROW_SHAPE", "length"],
854
+ ["targetArrowLength", "EDGE_TARGET_ARROW_SHAPE", "length"],
855
+ ["sourceArrowWidth", "EDGE_SOURCE_ARROW_SHAPE", "width"],
856
+ ["targetArrowWidth", "EDGE_TARGET_ARROW_SHAPE", "width"],
857
+ ] as const) {
858
+ const values = column(vp, arrowProportion(side), prop).map((v) => (v === 1 ? null : v));
859
+ if (values.some((v) => v !== null)) {
860
+ out[prop] = values;
861
+ }
862
+ }
863
+ // the arrows desktop draws standing off their node (T, the half circles, HALF_TOP, the squares)
864
+ for (const [prop, vp] of [
865
+ ["sourceArrowGap", "EDGE_SOURCE_ARROW_SHAPE"],
866
+ ["targetArrowGap", "EDGE_TARGET_ARROW_SHAPE"],
867
+ ] as const) {
868
+ const values = column(vp, arrowGap, prop).map((v) => (v === 0 ? null : v));
869
+ if (values.some((v) => v !== null)) {
870
+ out[prop] = values;
871
+ }
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Marks straight every curved edge without bends, except where Cytoscape desktop fans it: `EDGE_CURVED` only
877
+ * rounds the bends an edge has, and the bends Cytoscape adds itself. Among the edges joining the same two nodes
878
+ * (either way round, not loops), it draws the last it created straight and bends each earlier one without bends
879
+ * of its own round an anchor beside the line (`DEdgeDetails.getAnchors`, `EDGE_STACKING` `AUTO_BEND`, the default;
880
+ * it walks the pair's edges newest first, measured on Cytoscape 3.10.5's SVG of edges joining two nodes both ways)
881
+ * -- unless the last has bends, when every anchor falls on the line. Its anchors are the larger side of the two
882
+ * nodes times twice `EDGE_STACKING_DENSITY` apart, so the curves' middles half that: each fanned edge's
883
+ * `fanSpacing`, when the node sizes are known. A fanned edge is drawn curved even under `EDGE_CURVED` false, and an
884
+ * edge whose `EDGE_STACKING` is HAYSTACK is neither fanned nor counted: desktop draws it straight (both measured on
885
+ * Cytoscape 3.10.5's SVG of the edges-lines specimen sheet).
886
+ * @param out - the values per property, changed in place
887
+ * @param one - reads one visual property of one element
888
+ * @param n - the edge count
889
+ * @param ends - each edge's source and target node index, in creation order, when known
890
+ */
891
+ function straightenBendless(
892
+ out: Partial<Record<string, StyleColumn>>,
893
+ one: Resolver["one"],
894
+ n: number,
895
+ ends: CytoscapeElements["ends"],
896
+ ): void {
897
+ // an edge the style leaves unset takes EDGE_CURVED's default, true
898
+ const curved = out.curved ?? new Array<StyleValue | null>(n).fill(null);
899
+ const bent = (i: number): boolean => one(EDGE_BEND_VP, bendParser, "curved", i) === true;
900
+ const haystack = (i: number): boolean =>
901
+ String(one(EDGE_STACKING_VP, rawText, "fanned", i) ?? "").toUpperCase() === "HAYSTACK";
902
+ // each fanned edge's place in its pair's walk (1 for the first fanned)
903
+ const place = ends === undefined ? new Map<number, number>() : fanPlaces(ends, curved.length, bent, haystack);
904
+ const fanned = new Set(place.keys());
905
+ const rounds = (i: number): boolean => fanned.has(i) || bent(i);
906
+ const rounded = curved.map((c, i) => {
907
+ if (c === null) {
908
+ return rounds(i) ? true : null;
909
+ }
910
+ return c === true || fanned.has(i) ? rounds(i) : c;
911
+ });
912
+ if (out.curved !== undefined || rounded.includes(true)) {
913
+ out.curved = rounded;
914
+ }
915
+ if (ends?.nodeSize === undefined || fanned.size === 0) {
916
+ return;
917
+ }
918
+ const { source, target, nodeSize } = ends;
919
+ out.fanSpacing = curved.map((_, i) => {
920
+ const size = Math.max(nodeSize[source[i]] ?? Number.NaN, nodeSize[target[i]] ?? Number.NaN);
921
+ if (out.curved?.[i] !== true || !fanned.has(i) || bent(i) || !Number.isFinite(size)) {
922
+ return null;
923
+ }
924
+ const read = one(EDGE_STACKING_DENSITY_VP, sizeParser, "fanSpacing", i);
925
+ const density = Math.min(1, typeof read === "number" ? read : EDGE_STACKING_DENSITY_DEFAULT);
926
+ return size * density;
927
+ });
928
+ fanAnchors(out, ends, place);
929
+ }
930
+
931
+ /**
932
+ * The edges desktop fans, each with its place in its node pair's walk (1 for the first fanned). Desktop walks a node
933
+ * pair's edges newest first (its adjacency lists take each new edge at the head); the first it meets is drawn
934
+ * straight, and each later one fanned unless that first one has bends. Loops and haystack edges are left out.
935
+ * @param ends - each edge's source and target node index, and the order they were created in
936
+ * @param count - the edge count
937
+ * @param bent - whether an edge has bends of its own
938
+ * @param haystack - whether an edge is a haystack edge
939
+ * @returns each fanned edge's place, in walk order
940
+ */
941
+ function fanPlaces(
942
+ ends: NonNullable<CytoscapeElements["ends"]>,
943
+ count: number,
944
+ bent: (i: number) => boolean,
945
+ haystack: (i: number) => boolean,
946
+ ): Map<number, number> {
947
+ const place = new Map<number, number>();
948
+ const first = new Map<string, { f: number; k: number }>();
949
+ for (const i of newestFirst(count, ends.created?.edge)) {
950
+ const [s, t] = [ends.source[i], ends.target[i]];
951
+ if (s === t || haystack(i)) {
952
+ continue;
953
+ }
954
+ const key = `${Math.min(s, t)},${Math.max(s, t)}`;
955
+ const f = first.get(key);
956
+ if (f === undefined) {
957
+ first.set(key, { f: i, k: 0 });
958
+ } else if (!bent(f.f)) {
959
+ f.k++;
960
+ place.set(i, f.k);
961
+ }
962
+ }
963
+ return place;
964
+ }
965
+
966
+ /**
967
+ * The edges newest first: by the order they were created in when it is known, else last index first.
968
+ * @param count - the edge count
969
+ * @param created - each edge's place in the creation order, when known
970
+ * @returns the edge indices in that order
971
+ */
972
+ function newestFirst(count: number, created: ArrayLike<number> | undefined): number[] {
973
+ const walk = Array.from({ length: count }, (_, i) => i);
974
+ walk.sort(created === undefined ? (a, b) => b - a : (a, b) => created[b] - created[a]);
975
+ return walk;
976
+ }
977
+
978
+ /**
979
+ * Gives each fanned edge its place in desktop's fan as a bend, when the order desktop created the nodes in is known:
980
+ * the fan's side is then desktop's, not graphty's. Desktop measures each pair's fan from the node whose view it made
981
+ * first, and it makes views newest node first (`DEdgeDetails.getAnchors`: the pair's lower view SUID is its source;
982
+ * `SimpleNetwork.getNodeList` lists the newest node first); the `k`-th fanned edge bows `ceil(k / 2)` fan spacings
983
+ * off the middle of the line, to the right of the fan's direction (y down) for odd `k`, to the left for even. The
984
+ * bend is the control point of a quadratic Bezier whose middle lies there, twice as far off.
985
+ * @param out - the values per property, with `fanSpacing` set; changed in place
986
+ * @param ends - the edges' ends, their nodes' centers and creation order
987
+ * @param place - each fanned edge's place in its pair's walk, from 1
988
+ */
989
+ function fanAnchors(
990
+ out: Partial<Record<string, StyleColumn>>,
991
+ ends: NonNullable<CytoscapeElements["ends"]>,
992
+ place: ReadonlyMap<number, number>,
993
+ ): void {
994
+ const { created, nodeAt, source, target } = ends;
995
+ const spacing = out.fanSpacing?.slice();
996
+ if (created === undefined || nodeAt === undefined || spacing === undefined) {
997
+ return;
998
+ }
999
+ const bends = out.bends?.slice() ?? new Array<StyleValue | null>(source.length).fill(null);
1000
+ const bendCurve = out.bendCurve?.slice() ?? new Array<StyleValue | null>(source.length).fill(null);
1001
+ let changed = false;
1002
+ for (const [i, k] of place) {
1003
+ const [s, t] = [source[i], target[i]];
1004
+ const [ps, pt, f] = [nodeAt[s], nodeAt[t], spacing[i]];
1005
+ if (ps === null || ps === undefined || pt === null || pt === undefined || typeof f !== "number") {
1006
+ continue;
1007
+ }
1008
+ const len = Math.hypot(pt[0] - ps[0], pt[1] - ps[1]);
1009
+ if (len === 0) {
1010
+ continue;
1011
+ }
1012
+ // the fan's direction: from the node desktop made a view of first, the one created last
1013
+ const sign = created.node[s] > created.node[t] ? 1 : -1;
1014
+ const [nx, ny] = [(sign * (pt[0] - ps[0])) / len, (sign * (pt[1] - ps[1])) / len];
1015
+ const offset = Math.ceil(k / 2) * (k % 2 === 0 ? 1 : -1) * 2 * f;
1016
+ const [vx, vy] = [offset * ny, -offset * nx];
1017
+ // to the left of source -> target, y up, as a fraction of the length
1018
+ const across = (vx * (pt[1] - ps[1]) - vy * (pt[0] - ps[0])) / (len * len);
1019
+ bends[i] = `0.5,${String(Math.round(across * 1e6) / 1e6 || 0)}`;
1020
+ bendCurve[i] = "bezier";
1021
+ spacing[i] = null;
1022
+ changed = true;
1023
+ }
1024
+ if (changed) {
1025
+ out.bends = bends;
1026
+ out.bendCurve = bendCurve;
1027
+ out.fanSpacing = spacing;
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Gives each self-loop without bends of its own the two handles Cytoscape desktop draws it round
1033
+ * (`DEdgeDetails.getAnchors`): one left of the node's center and one above it, each `(i + 3) / 2` of the node's
1034
+ * size times twice `EDGE_STACKING_DENSITY` away, where the size is the larger side of the node with its border
1035
+ * (its extents in desktop's spatial index) and `i` counts the node's earlier loops without bends, newest first as
1036
+ * desktop walks them. A loop is curved through them unless `EDGE_CURVED` says false. Measured on Cytoscape
1037
+ * 3.10.5's SVG of the edges-lines specimen sheet (within 0.3 px of its loops).
392
1038
  * @param out - the values per property, changed in place
393
1039
  * @param one - reads one visual property of one element
1040
+ * @param curvedAsSet - each edge's `curved` as the style set it, before bendless edges were straightened
1041
+ * @param ends - each edge's source and target node index, in creation order, and each node's box, when known
394
1042
  */
395
- function straightenBendless(out: Partial<Record<string, StyleColumn>>, one: Resolver["one"]): void {
396
- const { curved } = out;
397
- if (curved !== undefined) {
398
- out.curved = curved.map((c, i) => (c === true ? one(EDGE_BEND_VP, bendParser, "curved", i) === true : c));
1043
+ function loopHandles(
1044
+ out: Partial<Record<string, StyleColumn>>,
1045
+ one: Resolver["one"],
1046
+ curvedAsSet: readonly (StyleValue | null)[] | undefined,
1047
+ ends: CytoscapeElements["ends"],
1048
+ ): void {
1049
+ const box = ends?.nodeBox;
1050
+ if (ends === undefined || box === undefined) {
1051
+ return;
1052
+ }
1053
+ const bends = out.bends?.slice() ?? new Array<StyleValue | null>(ends.source.length).fill(null);
1054
+ const curved = out.curved?.slice() ?? new Array<StyleValue | null>(ends.source.length).fill(null);
1055
+ const earlier = new Map<number, number>();
1056
+ const fraction = (v: number): string => String(Math.round(v * 1e4) / 1e4 || 0);
1057
+ let changed = false;
1058
+ for (let i = ends.source.length - 1; i >= 0; i--) {
1059
+ const node = ends.source[i];
1060
+ const b = box[node];
1061
+ if (node !== ends.target[i] || bends[i] !== null) {
1062
+ continue;
1063
+ }
1064
+ const rank = earlier.get(node) ?? 0;
1065
+ earlier.set(node, rank + 1);
1066
+ if (b === null || b === undefined || b[0] <= 0 || b[1] <= 0) {
1067
+ continue;
1068
+ }
1069
+ const read = one(EDGE_STACKING_DENSITY_VP, sizeParser, "fanSpacing", i);
1070
+ const density = Math.min(1, typeof read === "number" ? read : EDGE_STACKING_DENSITY_DEFAULT);
1071
+ const reach = (((rank + 3) * (Math.max(b[0], b[1]) + b[2])) / 2) * 2 * density;
1072
+ // as `x,y` fractions of the node's half width and half height, y up: left of the center, then above it
1073
+ bends[i] = `${fraction(-reach / (b[0] / 2))},0 0,${fraction(reach / (b[1] / 2))}`;
1074
+ curved[i] = curvedAsSet?.[i] !== false;
1075
+ changed = true;
1076
+ }
1077
+ if (changed) {
1078
+ out.bends = bends;
1079
+ out.curved = curved;
399
1080
  }
400
1081
  }
401
1082
 
@@ -415,6 +1096,41 @@ export const bendParser: CytoscapeValueParser = (raw) => {
415
1096
  return Array.isArray(raw) ? raw.length > 0 : raw !== null && raw !== undefined && typeof raw === "object";
416
1097
  };
417
1098
 
1099
+ /**
1100
+ * An `EDGE_BEND` value as the neutral `bends`: Cytoscape keeps each handle as `cos,sin,ratio`, the handle at the
1101
+ * source center plus `ratio` times the source-to-target vector turned by the angle (y down), handles separated by
1102
+ * `|`. Relative to the same line, y up, that is `ratio * cos` along it and `-ratio * sin` across it.
1103
+ * @param input - the value: an `EDGE_BEND` text, or CX2's `EDGE_CONTROL_POINTS` objects
1104
+ * @param site - where it was read
1105
+ * @returns the bends, or null for an edge without handles or a value that is not handles
1106
+ */
1107
+ const bendsParser: CytoscapeValueParser = (input, site) => {
1108
+ // CX2's EDGE_CONTROL_POINTS: one object per handle, whose `sin` Cytoscape fills with EDGE_BEND's first number and
1109
+ // `cos` with its second (it reads them back the same way: specimen-edges-lines bend-one-handle bends)
1110
+ const raw = Array.isArray(input)
1111
+ ? input
1112
+ .map((h: unknown) => {
1113
+ const o = (h ?? {}) as Record<string, unknown>;
1114
+ return `${Number(o.sin)},${Number(o.cos)},${Number(o.ratio)}`;
1115
+ })
1116
+ .join("|")
1117
+ : input;
1118
+ if (typeof raw !== "string" || !bendParser(raw, site)) {
1119
+ return null;
1120
+ }
1121
+ const points: string[] = [];
1122
+ for (const handle of raw.trim().split("|")) {
1123
+ const [c, sin, ratio] = handle.split(",").map((t) => Number(t.trim()));
1124
+ if (![c, sin, ratio].every((v) => Number.isFinite(v))) {
1125
+ noteAt(site, STYLE_ISSUE.BAD_VALUE, raw.slice(0, 64));
1126
+ return null;
1127
+ }
1128
+ const fraction = (v: number): string => String(Math.round(v * 1e4) / 1e4 || 0);
1129
+ points.push(`${fraction(ratio * c)},${fraction(-ratio * sin)}`);
1130
+ }
1131
+ return points.join(" ");
1132
+ };
1133
+
418
1134
  /** Default values that mean "nothing drawn": a VP left at one of these is not worth a note. */
419
1135
  const INERT_DEFAULT = /NullCustomGraphics|^255$|^0(?:\.0+)?$|^$|^C,C,c,0\.00,0\.00$|^false$/;
420
1136
 
@@ -510,7 +1226,15 @@ export function resolveCytoscapeBypasses(
510
1226
  ): Partial<Record<string, StyleColumn>> {
511
1227
  const notes = options.notes ?? new StyleNotes();
512
1228
  const style = emptyCytoscapeStyle();
513
- const elements: CytoscapeElements = { count: maps.length, value: () => null, bypasses: maps };
1229
+ const elements: CytoscapeElements = {
1230
+ count: maps.length,
1231
+ value: options.value ?? (() => null),
1232
+ // Cytoscape 3.10.5 draws an XGMML edge's EDGE_LABEL_AUTOROTATE true level, where it turns the same edge read
1233
+ // from CX, CX2 or a session along its line (the label-autorotate-true edge of the edge-labels and
1234
+ // mappings-bypass-p13 sheets in its SVG): its XGMML reader does not apply it, so neither does graph-io
1235
+ bypasses: maps.map((m) => (m?.has(AUTOROTATE_VP) === true ? withoutKey(m, AUTOROTATE_VP) : m)),
1236
+ ends: options.ends,
1237
+ };
514
1238
  const out = resolveCytoscape(target, style, elements, { notes, budget: options.budget });
515
1239
  if (target === "node" && out.width === undefined) {
516
1240
  const size = maps.map((m) => {