@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
@@ -1,29 +1,38 @@
1
1
  /**
2
2
  * Pajek styles: each vertex's shape keyword and drawing parameters (`ic`, `bc`, `bw`, `x_fact`, `y_fact`, `lc`, `fos`)
3
- * and each line's `c`, `w` and `p`, read the way Pajek draws them, with Pajek's stated defaults beneath (an ellipse,
4
- * black solid lines, an arrow at the target of every arc). Colors are Pajek's: the dvips names, `GrayNN`,
5
- * `RGBrrggbb`, `RGB(r,g,b)` and the CMYK forms. `x_fact` and `y_fact` are factors of Pajek's normal vertex size, so the
6
- * lengths are relative.
3
+ * and each line's `c`, `w` and `p`, read the way Pajek 6.02 draws them (parameters carried from line to line, `CARRIED`),
4
+ * with Pajek's own defaults beneath (a #ffd9bc ellipse with a #660000 border, grey arcs with an arrow at the target,
5
+ * teal edges). Colors are Pajek's: the dvips names and the ten Pajek adds, `GrayNN`, `RGBrrggbb`, `RGB(r,g,b)` and the
6
+ * CMYK forms. Lengths are in PostScript points, the unit of Pajek's EPS and SVG export, where the drawing's [0,1] square
7
+ * is [0,500] (`PAJEK_POINTS_PER_UNIT`): a vertex is Pajek's normal size 20 times its `x_fact` and `y_fact`,
8
+ * `bw` and `fos` are points as written, an arc is `w` wide and an edge twice `w`.
7
9
  *
8
10
  * Writing: the same parameters from what graphty draws, leaving out what Pajek draws by default; a color that is a
9
11
  * dvips name is written by name, any other as `RGBrrggbb`, so colors are exact. The `graphty.styles` block, with the
10
12
  * per-element values its rules read inside it (Pajek has no other place for them), is one comment line,
11
13
  * `% graphty.styles <json>`, which Pajek skips and graph-io reads back.
12
14
  */
15
+ import { explicitWeights } from "../../common/weights.js";
13
16
  import { canonColor, colorOpacity } from "../../styles/canon/color.js";
14
17
  import { DVIPS_COLORS } from "../../styles/canon/color-tables.js";
15
18
  import { canonEnum } from "../../styles/canon/enums.js";
16
19
  import { canonNumber } from "../../styles/canon/number.js";
20
+ import { noteAt } from "../../styles/canon/site.js";
17
21
  import { canonText } from "../../styles/canon/text.js";
22
+ import { STYLE_ISSUE } from "../../styles/codes.js";
23
+ import { toolDefaults } from "../../styles/defaults.js";
18
24
  import { StyleApproximations } from "../../styles/io/approximations.js";
19
- import { blockStyles, parseStyleBlock, styleBlockLosses, writeStyleBlock } from "../../styles/io/block.js";
25
+ import { styleBlockLosses, writeStyleBlock } from "../../styles/io/block.js";
26
+ import { elementValues, takeFromBlock } from "../../styles/io/round-trip.js";
20
27
  import { StyleNotes } from "../../styles/notes.js";
21
- import { styleFingerprint } from "../../styles/plan/features.js";
22
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
28
+ import { plainCell } from "../../styles/plan/resolve.js";
29
+ import { graphvizFontName, postscriptFont } from "../dot/drawing.js";
23
30
  import { valueColumns } from "../graphml/styles-yed.js";
24
- import { LABEL_COLUMN, PAJEK_STYLE_COMMENT, SHAPE_COLUMN } from "./syntax.js";
31
+ import { drawLines, drawsLineLabels, drawsStraightLines, drawVertices } from "./draw.js";
32
+ import { LABEL_COLUMN, PAJEK_STYLE_COMMENT, SHAPE_COLUMN, VALUE_COLUMN } from "./syntax.js";
25
33
  /**
26
- * What the Pajek writer holds natively: the vertex label, the vertex shape keyword and drawing parameters, the line
34
+ * What the Pajek writer holds natively: the vertex label, the vertex shape keyword (a polygon only as Pajek's diamond,
35
+ * `shapePoints` with its `q`) and drawing parameters, the line
27
36
  * parameters, and the arrows Pajek draws on arcs. The `graphty.styles` block is a comment line the writer places itself.
28
37
  * @category Built-in formats
29
38
  */
@@ -37,23 +46,139 @@ export const PAJEK_STYLE_CAPABILITIES = Object.freeze({
37
46
  "borderColor",
38
47
  "borderWidth",
39
48
  "shape",
49
+ "shapePoints",
40
50
  "width",
41
51
  "height",
42
52
  "labelColor",
43
53
  "labelSize",
54
+ "labelFont",
55
+ "labelPosition",
56
+ "labelOffsetX",
57
+ "labelOffsetY",
58
+ "labelShiftX",
59
+ ]),
60
+ edge: Object.freeze([
61
+ "color",
62
+ "opacity",
63
+ "width",
64
+ "lineStyle",
65
+ "sourceArrow",
66
+ "targetArrow",
67
+ "arrowSize",
68
+ "arrowPosition",
69
+ "label",
70
+ "labelColor",
71
+ "labelSize",
72
+ "labelFont",
44
73
  ]),
45
- edge: Object.freeze(["color", "opacity", "width", "lineStyle", "sourceArrow", "targetArrow"]),
46
74
  block: "native",
47
75
  });
48
76
  /**
49
- * Pajek's default vertex size, in the units of a border width `bw`: `x_fact` and `y_fact` are factors of it, so a
50
- * border width is read as a factor of it too (`bw 1`, Pajek's default, is a fifth of a normal vertex).
77
+ * Pajek's normal vertex size, in points: what `x_fact` and `y_fact` multiply (the manual, p. 89, names `x_fact` and
78
+ * `y_fact` "magnification" without stating the value).
79
+ * Measured in Pajek 6.02's own SVG export (`test/fidelity/oracles/pajek-*.svg`): a vertex with no size parameter is
80
+ * drawn 20 across, and `x_fact 1.54` 30.8, on the drawing's [0,500] square.
81
+ */
82
+ const PAJEK_VERTEX_SIZE = 20;
83
+ /**
84
+ * How far past its vertex size Pajek draws its man and woman figures, across and up: Pajek 6.02's SVG export draws
85
+ * them as paths that reach beyond the vertex's own square, a man 2.5 times its size across and 6.49 times its size
86
+ * tall, a woman (her skirt and arms wider) 3 times across and 6.48 tall (`pajek-specimen-node-shapes`, vertices 9 and
87
+ * 10 at `x_fact 0.5`: the man's path spans x -12.51..12.51 and y -32.74..32.13, the woman's -15.02..15.02 and
88
+ * -32.99..31.79, around a size of 10).
89
+ */
90
+ const PAJEK_FIGURES = new Map([
91
+ ["other:man", [2.502, 6.487]],
92
+ ["other:woman", [3.003, 6.478]],
93
+ ]);
94
+ /**
95
+ * The factor Pajek draws a man or a woman at along an axis whose `x_fact` or `y_fact` the line leaves unset: 0.3, not
96
+ * the 1 of every other shape (`pajek-synth-vertices`: a man that sets neither, vertices 24, 48 and 81, is drawn 15.02
97
+ * across, 0.3 of the 50.05 of `x_fact 1`, and 30.55 tall from his feet to his head, 0.3 of `y_fact 1`'s 101.83; a woman
98
+ * that sets neither, vertices 9, 33 and 57, 16.30 across, 0.3 of `x_fact 1`'s 54.35).
99
+ */
100
+ const PAJEK_FIGURE_UNSET_FACTOR = 0.3;
101
+ /**
102
+ * Points per coordinate unit: "When Postscript picture is obtained, the square [0,1]x[0,1] is transformed to
103
+ * [0,500]x[0,500]" (Pajek, "Pajek to EPS", http://mrvar.fdv.uni-lj.si/pajek/DrawEPS.htm).
51
104
  */
52
- const VERTEX_SIZE = 5;
105
+ export const PAJEK_POINTS_PER_UNIT = 500;
53
106
  /** The vertex parameters read as styles. */
54
- const NODE_KEYS = ["ic", "bc", "bw", "x_fact", "y_fact", "lc", "fos"];
107
+ const NODE_KEYS = [
108
+ "ic",
109
+ "bc",
110
+ "bw",
111
+ "s_size",
112
+ "x_fact",
113
+ "y_fact",
114
+ "phi",
115
+ "q",
116
+ "lc",
117
+ "fos",
118
+ "font",
119
+ "lr",
120
+ "lphi",
121
+ "la",
122
+ ];
55
123
  /** The line parameters read as styles. */
56
- const EDGE_KEYS = ["c", "w", "p"];
124
+ const EDGE_KEYS = ["c", "w", "p", "s", "ap", "l", "lc", "fos", "font"];
125
+ /**
126
+ * What Pajek carries from one line of a section to the next. A line (arc or edge) that does not set one of these
127
+ * parameters is drawn with the value the line before it set. A vertex line that sets no parameter and names no shape
128
+ * is drawn exactly as the vertex line before it; a vertex line that sets any starts again from Pajek's defaults.
129
+ * Measured in Pajek 6.02's own SVG export: `test/fidelity/oracles/pajek-kdorola.svg` (vertices 2 to 59, which set
130
+ * nothing, drawn with vertex 1's `lr 11 lphi 285 fos 9`, and 61 on as vertex 60's `box`), `pajek-1crn.svg` (vertex 3
131
+ * drawn as vertex 2), `pajek-synth-lines.svg` (arc 3, which sets `w 3` but no `c`, drawn in arc 2's `c Blue`), and
132
+ * the generated `shapes-and-lines.net` (vertex 5 at the default size after vertex 4's `x_fact 8`). A line's label `l`
133
+ * is its own; its label's color, size and font are not carried here, as they only show with a label.
134
+ */
135
+ const CARRIED = {
136
+ node: [SHAPE_COLUMN, ...NODE_KEYS],
137
+ edge: ["c", "w", "p", "s", "ap"],
138
+ };
139
+ /**
140
+ * What Pajek 6.02 carries from line to line as it reads: not the pattern `p` nor the arrowhead's place `ap`, which the
141
+ * writer still restates (`pajek-synth-lines`: edge `3 10` after `1 8 ... p Dots` and arc `5 6 -1` after a `p Solid`
142
+ * are drawn as if no line before set one; arcs `11 12` after `10 11 ... ap 0.5` and `18 19` after `17 18 ... ap 0.25`
143
+ * have their arrowheads at their targets).
144
+ */
145
+ const CARRIED_LINE = CARRIED.edge.filter((key) => key !== "p" && key !== "ap");
146
+ /** The width Pajek draws an edge (undirected) at for each unit of `w`; an arc is drawn `w` wide. */
147
+ const EDGE_WIDTH_PER_W = 2;
148
+ /** The color and width Pajek draws an arc with when it sets no `c` or `w` (an edge's are the tool defaults). */
149
+ const ARC_DEFAULTS = { color: "#999999", width: 1 };
150
+ /**
151
+ * A getter that gives an element the parameters an element before it set, as Pajek reads them: for a line each
152
+ * parameter it leaves out, for a vertex all of them when it sets none (`CARRIED`).
153
+ * @param keys - the parameters carried
154
+ * @param whole - true for vertices: carried only to an element that sets none, and then all together
155
+ * @returns for each element in order, its getter
156
+ */
157
+ function carriedParams(keys, whole) {
158
+ let last = new Map();
159
+ return (get) => {
160
+ const own = new Map();
161
+ for (const key of keys) {
162
+ const v = get(key);
163
+ if (v !== null) {
164
+ own.set(key, v);
165
+ }
166
+ }
167
+ if (whole) {
168
+ if (own.size > 0) {
169
+ last = own;
170
+ return get;
171
+ }
172
+ }
173
+ else {
174
+ for (const [key, v] of own) {
175
+ last.set(key, v);
176
+ }
177
+ }
178
+ const carried = last;
179
+ return (key) => get(key) ?? (keys.includes(key) ? (carried.get(key) ?? null) : null);
180
+ };
181
+ }
57
182
  /** The style column marking arcs, which Pajek draws with an arrow at the target. */
58
183
  const DIRECTED_KEY = "directed";
59
184
  /** The properties the reader reads, per target: what a file graphty wrote can bring back from its block. */
@@ -61,6 +186,8 @@ const READ = {
61
186
  node: [
62
187
  "label",
63
188
  "shape",
189
+ "shapePoints",
190
+ "shapePointsStretch",
64
191
  "fill",
65
192
  "fillOpacity",
66
193
  "borderColor",
@@ -69,8 +196,25 @@ const READ = {
69
196
  "height",
70
197
  "labelColor",
71
198
  "labelSize",
199
+ "labelFont",
200
+ "labelPosition",
201
+ "labelOffsetX",
202
+ "labelOffsetY",
203
+ "labelShiftX",
204
+ "labelRotation",
205
+ ],
206
+ edge: [
207
+ "color",
208
+ "opacity",
209
+ "width",
210
+ "lineStyle",
211
+ "arrowSize",
212
+ "arrowPosition",
213
+ "label",
214
+ "labelColor",
215
+ "labelSize",
216
+ "labelFont",
72
217
  ],
73
- edge: ["color", "opacity", "width", "lineStyle"],
74
218
  };
75
219
  /**
76
220
  * Reads a Pajek color and its alpha.
@@ -86,6 +230,13 @@ function readColor(raw, property, opacity, put, at) {
86
230
  }
87
231
  const c = canonColor(raw, "dvips", at(property));
88
232
  if (c === null) {
233
+ // a name Pajek does not know, or a known one in another case (`red`), fills a vertex white and draws a line
234
+ // black (`pajek-specimen-node-fills-borders-p3` `ic-unknown-name`, `ic-lowercase-name`); ponytail: measured
235
+ // on names in `ic` and `c` only, so a border or label color and an unreadable RGB / CMYK form keep the default
236
+ const unknown = UNKNOWN_COLORS.get(property);
237
+ if (unknown !== undefined && typeof raw === "string" && /^[A-Za-z]\w*$/.test(raw.trim())) {
238
+ put(property, unknown);
239
+ }
89
240
  return;
90
241
  }
91
242
  put(property, c.color);
@@ -94,6 +245,11 @@ function readColor(raw, property, opacity, put, at) {
94
245
  put(opacity, alpha);
95
246
  }
96
247
  }
248
+ /** What Pajek draws for a color it cannot read, by property. */
249
+ const UNKNOWN_COLORS = new Map([
250
+ ["fill", "#ffffff"],
251
+ ["color", "#000000"],
252
+ ]);
97
253
  /**
98
254
  * Reads a length or factor.
99
255
  * @param raw - the parameter's value, or null
@@ -111,41 +267,721 @@ function readNumber(raw, property, put, at) {
111
267
  * @param get - the vertex's parameters
112
268
  * @param put - receives the values
113
269
  * @param at - the sites
270
+ * @param place - the vertex's place in the file's coordinates, or null when it has none
114
271
  */
115
- function readVertex(get, put, at) {
272
+ function readVertex(get, put, at, place = null) {
116
273
  // an empty label draws nothing
117
274
  const label = get(LABEL_COLUMN);
118
275
  if (label !== null && label !== "") {
119
276
  put("label", canonText(label, at("label")));
120
277
  }
121
- const shape = get(SHAPE_COLUMN);
122
- if (shape !== null) {
123
- put("shape", canonEnum("shape", shape, "dvips", at("shape")));
278
+ const raw = get(SHAPE_COLUMN);
279
+ const shape = raw === null ? null : canonEnum("shape", raw, "dvips", at("shape"));
280
+ if (raw !== null) {
281
+ put("shape", shape);
124
282
  }
125
283
  readColor(get("ic"), "fill", "fillOpacity", put, at);
126
284
  readColor(get("bc"), "borderColor", null, put, at);
127
- const bw = get("bw");
128
- if (bw !== null) {
129
- const width = canonNumber(bw, at("borderWidth"));
130
- put("borderWidth", width === null ? null : width / VERTEX_SIZE);
131
- }
132
- readNumber(get("x_fact"), "width", put, at);
133
- readNumber(get("y_fact"), "height", put, at);
285
+ readNumber(get("bw"), "borderWidth", put, at);
286
+ readSize(get, put, at, PAJEK_FIGURES.get(String(shape)));
287
+ readOutline(get, put, shape);
134
288
  readColor(get("lc"), "labelColor", null, put, at);
135
289
  readNumber(get("fos"), "labelSize", put, at);
290
+ readFont(get("font"), put, at);
291
+ readLabelPosition(get, put, at);
292
+ readLabelAngle(get("la"), put, at, place);
293
+ if (shape === "none") {
294
+ // an `empty` vertex's label is drawn white at size 1 on its center, whatever the line sets
295
+ // (`pajek-synth-vertices` `empty *`, `pajek-lin-rel4` v226 to v233)
296
+ for (const [property, value] of EMPTY_LABEL) {
297
+ put(property, value);
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * The corners Pajek 6.02's SVG export draws a box, a triangle and a house with, as `shapePoints` (fractions of the half
303
+ * width and height, y up): `pajek-specimen-node-shapes`, `shape-box`, `shape-triangle`, `shape-house`.
304
+ */
305
+ const PAJEK_OUTLINES = new Map([
306
+ [
307
+ "rectangle",
308
+ [
309
+ [-1, 1],
310
+ [1, 1],
311
+ [1, -1],
312
+ [-1, -1],
313
+ ],
314
+ ],
315
+ [
316
+ "triangle",
317
+ [
318
+ [1, -1],
319
+ [0, 1],
320
+ [-1, -1],
321
+ ],
322
+ ],
323
+ [
324
+ "other:house",
325
+ [
326
+ [-0.7, -1],
327
+ [0.7, -1],
328
+ [0.7, 0.3],
329
+ [1, 0.3],
330
+ [1, 0.4],
331
+ [0, 1],
332
+ [-1, 0.4],
333
+ [-1, 0.3],
334
+ [-0.7, 0.3],
335
+ ],
336
+ ],
337
+ ]);
338
+ /** A diamond's `q` where the line sets none (Pajek's shipped SHAPES.CFG). */
339
+ const PAJEK_DIAMOND_Q = 0.001;
340
+ /**
341
+ * A vertex's outline where Pajek draws one no named shape is: a diamond is Pajek's six-cornered one, its top and bottom
342
+ * cut `q` of its half width wide (`pajek-specimen-node-shapes`: `q 0.001` by default, `q 0.5` a hexagon, `q 1` a
343
+ * square, `q 2` a bow tie twice as wide), and `phi` turns a box, a triangle, a house or a diamond `phi` degrees
344
+ * counterclockwise about its center (`phi-30-box`, `phi-90-triangle`: Pajek draws the shape at its size, then turns
345
+ * it). ponytail: the corners are fractions of the size the line gives (`x_fact`, `y_fact`); a turned shape whose width
346
+ * and height the Draw window's Size of Vertices or vectors scale apart is turned in the wrong box. A turned ellipse,
347
+ * man or woman is drawn unturned: an outline of corners would draw an ellipse as a polygon.
348
+ * @param get - the vertex's parameters
349
+ * @param put - receives the values
350
+ * @param shape - its shape
351
+ */
352
+ function readOutline(get, put, shape) {
353
+ const phi = numberOrNull(get("phi")) ?? 0;
354
+ const q = numberOrNull(get("q")) ?? PAJEK_DIAMOND_Q;
355
+ const diamond = [
356
+ [-1, 0],
357
+ [-q, 1],
358
+ [q, 1],
359
+ [1, 0],
360
+ [q, -1],
361
+ [-q, -1],
362
+ ];
363
+ const corners = shape === "diamond" ? diamond : PAJEK_OUTLINES.get(String(shape));
364
+ if (corners === undefined || (shape !== "diamond" && phi % 360 === 0)) {
365
+ return;
366
+ }
367
+ // turned in the drawn box, x_fact by y_fact, and back to fractions of it
368
+ const [w, h] = [numberOrNull(get("x_fact")) || 1, numberOrNull(get("y_fact")) || 1];
369
+ const [c, s] = [Math.cos((phi * Math.PI) / 180), Math.sin((phi * Math.PI) / 180)];
370
+ const round = (v) => String(Number(v.toFixed(4)) + 0);
371
+ const points = corners.map(([x, y]) => {
372
+ const [px, py] = [x * w, y * h];
373
+ return `${round((px * c - py * s) / w)},${round((px * s + py * c) / h)}`;
374
+ });
375
+ put("shape", "polygon");
376
+ put("shapePoints", points.join(" "));
377
+ put("shapePointsStretch", false);
378
+ }
379
+ /** How Pajek 6.02's SVG export draws an `empty` vertex's label. */
380
+ const EMPTY_LABEL = [
381
+ ["labelSize", 1],
382
+ ["labelColor", "#ffffff"],
383
+ ["labelPosition", "center"],
384
+ ["labelOffsetX", 0],
385
+ ["labelOffsetY", 0],
386
+ ["labelShiftX", 0],
387
+ ];
388
+ /**
389
+ * A label's `font`: Pajek writes it into its SVG export as the family, as given, so a PostScript name (`Courier-Bold`,
390
+ * `Helvetica-Oblique`) is drawn in its family, with a generic one after it, and upright, as the pinned fonts draw it
391
+ * (`pajek-specimen-labels` `font-courier-bold` monospace, `font-helvetica-oblique` sans-serif and upright); any other
392
+ * name is the family as written. Pajek draws every label bold whatever the name says.
393
+ * @param raw - the parameter's value, or null
394
+ * @param put - receives the values
395
+ * @param at - the sites
396
+ */
397
+ function readFont(raw, put, at) {
398
+ if (raw === null || raw === "") {
399
+ return;
400
+ }
401
+ const ps = typeof raw === "string" ? postscriptFont(raw.trim()) : null;
402
+ if (ps === null) {
403
+ readText(raw, "labelFont", put, at);
404
+ return;
405
+ }
406
+ put("labelFont", ps.family);
407
+ }
408
+ /**
409
+ * A vertex label's angle `la` (manual p. 90), degrees counterclockwise, as a clockwise rotation: Pajek 6.02's SVG export
410
+ * turns the label by `rotate(-la)` (`pajek-specimen-labels`, `la-30` .. `la-270`). An angle of 360 or more turns the
411
+ * label away from the middle of the file's square, (0.5, 0.5) in its own coordinates, and then `la - 360` further
412
+ * (`la-360` at (0.4533, 0.3706) is drawn at -109.83, the direction from the middle; `la-400` at (0.5467, 0.3706) at
413
+ * -110.17, that direction, 70.17, and 40 more); without the vertex's place it is left unread.
414
+ * @param raw - the parameter's value, or null
415
+ * @param put - receives the value
416
+ * @param at - the sites
417
+ * @param place - the vertex's place in the file's coordinates, or null
418
+ */
419
+ function readLabelAngle(raw, put, at, place) {
420
+ let a = raw === null ? null : canonNumber(raw, at("labelRotation"));
421
+ if (a !== null && a >= 360) {
422
+ a = place === null ? null : (Math.atan2(0.5 - place[1], place[0] - 0.5) * 180) / Math.PI + a - 360;
423
+ }
424
+ if (a !== null) {
425
+ put("labelRotation", Number(((((360 - a) % 360) + 360) % 360).toPrecision(12)));
426
+ }
427
+ }
428
+ /**
429
+ * A vertex's width and height in points: the normal size times its `x_fact` and `y_fact`, each 1 when unset; a vertex
430
+ * that sets neither is left to the default beneath. The manual (p. 89) calls `s_size` "default size", but Pajek 6.02
431
+ * draws a vertex the same size whatever its `s_size` (`pajek-specimen-node-sizes-opacity`: `s_size` 0.5, 2 and 3 all
432
+ * drawn 36 across at `x_fact 1.8`), so it is not read. A man or a woman is the box of the figure Pajek draws for
433
+ * that size (`PAJEK_FIGURES`), set or not, at `PAJEK_FIGURE_UNSET_FACTOR` along an axis the line sets no factor for.
434
+ * @param get - the vertex's parameters
435
+ * @param put - receives the values
436
+ * @param at - the sites
437
+ * @param figure - a figure's width and height over its size, or undefined for another shape
438
+ */
439
+ function readSize(get, put, at, figure) {
440
+ for (const [key, property, k] of [
441
+ ["x_fact", "width", 0],
442
+ ["y_fact", "height", 1],
443
+ ]) {
444
+ const f = get(key);
445
+ if (f === null && figure === undefined) {
446
+ continue;
447
+ }
448
+ const unset = figure === undefined ? 1 : PAJEK_FIGURE_UNSET_FACTOR;
449
+ const factor = f === null ? unset : canonNumber(f, at(property));
450
+ const over = figure?.[k] ?? 1;
451
+ put(property, factor === null ? null : Number((PAJEK_VERTEX_SIZE * factor * over).toPrecision(15)));
452
+ }
453
+ }
454
+ /**
455
+ * A text parameter (a PostScript font name).
456
+ * @param raw - the parameter's value, or null
457
+ * @param property - the property
458
+ * @param put - receives the value
459
+ * @param at - the sites
460
+ */
461
+ function readText(raw, property, put, at) {
462
+ if (raw !== null && raw !== "") {
463
+ put(property, canonText(raw, at(property)));
464
+ }
465
+ }
466
+ /** The `lphi` written for each side graphty puts a label on, and the side written for each Pajek has no angle for. */
467
+ const SIDE_ANGLES = new Map([
468
+ ["right", 0],
469
+ ["top-right", 90],
470
+ ["left", 180],
471
+ ["bottom-right", 285],
472
+ ]);
473
+ const NEAREST_SIDES = new Map([
474
+ ["top", "top-right"],
475
+ ["top-left", "top-right"],
476
+ ["bottom", "bottom-right"],
477
+ ["bottom-left", "bottom-right"],
478
+ ]);
479
+ /**
480
+ * A vertex's label place from Pajek's polar label parameters (manual p. 90): `lr` is the distance of the label from
481
+ * the vertex center, 0 centering it, and `lphi` its angle in degrees counterclockwise. Pajek 6.02's SVG export starts
482
+ * the text at that point, its baseline through it (`text-anchor` start at y = the point), so the label's left edge is
483
+ * at the point's x (`labelShiftX` 0.5) and its middle `PAJEK_BASELINE_TO_MIDDLE` of its size above the point's y
484
+ * (`pajek-specimen-labels` `lr-*`, `lphi-*`; every corpus label). A vertex that sets none of `lr`, `lphi` and `fos`
485
+ * is left to the default beneath, the same place at the default size.
486
+ * @param get - the vertex's parameters
487
+ * @param put - receives the values
488
+ * @param at - the sites
489
+ */
490
+ function readLabelPosition(get, put, at) {
491
+ const [lr, lphi, fos] = [get("lr"), get("lphi"), get("fos")];
492
+ if (lr === null && lphi === null && fos === null) {
493
+ return;
494
+ }
495
+ const radius = lr === null ? PAJEK_LABEL_RADIUS : canonNumber(lr, at("labelPosition"));
496
+ const angle = lphi === null ? PAJEK_LABEL_ANGLE : canonNumber(lphi, at("labelPosition"));
497
+ const size = fos === null ? PAJEK_LABEL_SIZE : canonNumber(fos, at("labelSize"));
498
+ if (radius === null || angle === null || size === null) {
499
+ return;
500
+ }
501
+ const [x, y] = labelOffset(radius, angle, size);
502
+ put("labelPosition", "center");
503
+ put("labelOffsetX", x);
504
+ put("labelOffsetY", y);
505
+ put("labelShiftX", radius === 0 ? 0 : 0.5);
506
+ }
507
+ /**
508
+ * Where Pajek's polar label parameters put a label, from the vertex center, y down: the middle of its left edge, or
509
+ * its middle when `lr` is 0, which centers it.
510
+ * @param radius - `lr`
511
+ * @param angle - `lphi`, degrees counterclockwise
512
+ * @param size - the label's size
513
+ * @returns the offset across and down
514
+ */
515
+ function labelOffset(radius, angle, size) {
516
+ if (radius === 0) {
517
+ return [0, 0];
518
+ }
519
+ const t = (angle * Math.PI) / 180;
520
+ const round = (v) => Number(v.toFixed(9)) + 0;
521
+ return [round(radius * Math.cos(t)), round(-radius * Math.sin(t) - PAJEK_BASELINE_TO_MIDDLE * size)];
136
522
  }
137
523
  /**
138
- * One line's style: its color, width and pattern.
524
+ * The label radius and angle Pajek uses when a vertex sets none: 20 and 285, below and to the right of the vertex.
525
+ * Measured in Pajek 6.02's own SVG export (`test/fidelity/oracles/pajek-football.svg`, `pajek-specimen-labels`
526
+ * `default`: every such label starts 20 from its vertex at 285 degrees); the size is the default label size.
527
+ */
528
+ const PAJEK_LABEL_RADIUS = 20;
529
+ const PAJEK_LABEL_ANGLE = 285;
530
+ const PAJEK_LABEL_SIZE = 15;
531
+ /**
532
+ * How far above its baseline a label's middle is, per unit of its size: half the ascent less the descent of Arial
533
+ * (Liberation Sans: 0.905 and 0.212), the face Pajek draws its labels in by default.
534
+ */
535
+ const PAJEK_BASELINE_TO_MIDDLE = 0.3465;
536
+ /**
537
+ * One line's style: its color, width and pattern. Pajek draws an edge (undirected) twice as wide as an arc of the
538
+ * same `w`.
139
539
  * @param get - the line's parameters
140
540
  * @param put - receives the values
141
541
  * @param at - the sites
542
+ * @param directed - whether the line is an arc
543
+ * @param labels - whether its label is drawn (`drawsLineLabels`)
544
+ * @param run - the line from its source's center to its target's, in drawn units, y down, or null when unplaced
545
+ * @param loop - for a loop on a placed node, receives its control points; null for any other line
142
546
  */
143
- function readLine(get, put, at) {
547
+ function readLine(get, put, at, directed, labels = true, run = null, loop = null) {
144
548
  readColor(get("c"), "color", "opacity", put, at);
145
- readNumber(get("w"), "width", put, at);
549
+ readNumber(get("w"), "width", (property, value) => {
550
+ put(property, typeof value === "number" && !directed ? value * EDGE_WIDTH_PER_W : value);
551
+ }, at);
146
552
  const p = get("p");
147
- if (p !== null) {
148
- put("lineStyle", canonEnum("lineStyle", p, "dvips", at("lineStyle")));
553
+ if (p === "Dots") {
554
+ put("lineStyle", pajekDash(get, directed));
555
+ }
556
+ else if (p === "Solid") {
557
+ put("lineStyle", "solid");
558
+ }
559
+ else if (p !== null) {
560
+ // Pajek knows Solid and Dots, case for case; any other pattern (`Dashed`, `dots`) draws a solid line
561
+ noteAt(at("lineStyle"), STYLE_ISSUE.UNKNOWN_VALUE, typeof p === "string" ? p : JSON.stringify(p));
562
+ put("lineStyle", "solid");
563
+ }
564
+ // an arrowhead is 6 x `s` long and 5 x `s` wide; `s` 1, Pajek's normal size, is 6 long (the pajek tool arrowhead)
565
+ readNumber(get("s"), "arrowSize", put, at);
566
+ readArrowPosition(get("ap"), put, at, run);
567
+ // a loop is drawn as the Bezier route of a line PAJEK_POINTS_PER_UNIT long to the right, its two ends on the node
568
+ const line = loop === null ? get : loopParam(get);
569
+ const controls = loop === null ? null : readLoop(line, put, loop);
570
+ if (labels) {
571
+ readText(get("l"), "label", put, at);
572
+ if (controls === null) {
573
+ readLineLabelPlace(get, put, at, run);
574
+ }
575
+ else {
576
+ readLineLabelPlace(line, put, at, [PAJEK_POINTS_PER_UNIT, 0], loopTop(controls));
577
+ }
578
+ }
579
+ if (loop === null) {
580
+ readBezier(get, put, run);
581
+ }
582
+ readColor(get("lc"), "labelColor", null, put, at);
583
+ readNumber(get("fos"), "labelSize", put, at);
584
+ readFont(get("font"), put, at);
585
+ }
586
+ /**
587
+ * The route parameters Pajek 6.02's SVG export draws a loop with where the line sets none (`pajek-specimen-edges-lines-p2`
588
+ * `loop` and `loop-arc`, `pajek-specimen-edge-labels` `on-loop`: a cubic from the node's center up and back, its control
589
+ * points 74.9 out at 70 and 110 degrees up from the right).
590
+ */
591
+ const LOOP_DEFAULTS = { a1: -70, k1: 0.3, a2: -70, k2: 0.3 };
592
+ /**
593
+ * A loop's route parameters: the line's own, Pajek's loop defaults for any it leaves out.
594
+ * @param get - the line's parameters
595
+ * @returns the parameters with the defaults
596
+ */
597
+ function loopParam(get) {
598
+ return (key) => get(key) ?? (key in LOOP_DEFAULTS ? LOOP_DEFAULTS[key] : null);
599
+ }
600
+ /**
601
+ * A loop's route (manual p. 90: "if arc is a loop (arc to itself) bezier loop will be drawn"), as Pajek 6.02's SVG
602
+ * export draws it: the Bezier route of a line PAJEK_POINTS_PER_UNIT long running right, both its ends on the node
603
+ * (`a1 -130 k1 0.6 a2 -130 k2 0.6`: control points 150 out at 130 and 50 degrees up from the right, measured on
604
+ * `pajek-specimen-edges-lines-p2` `loop-bezier`). Its bends are written once the nodes' sizes are known
605
+ * (`layLoops`). ponytail: measured in one Draw window (1200 x 900) and one vertex size; whether Pajek scales a loop
606
+ * with either was not measured.
607
+ * @param get - the line's parameters, with the loop defaults
608
+ * @param put - receives the values
609
+ * @param loop - receives the control points
610
+ * @returns the control points, or null when the line's parameters give none
611
+ */
612
+ function readLoop(get, put, loop) {
613
+ const run = [PAJEK_POINTS_PER_UNIT, 0];
614
+ const controls = bezierControls(get, run);
615
+ if (controls === null) {
616
+ return null;
617
+ }
618
+ const [[a1, l1], [a2, l2]] = controls;
619
+ const L = PAJEK_POINTS_PER_UNIT;
620
+ // along the line is right, left of it up: the second point is measured back from the line's far end, the node
621
+ const [c1, c2] = [
622
+ [a1 * L, -l1 * L],
623
+ [(a2 - 1) * L, -l2 * L],
624
+ ];
625
+ loop([c1, c2]);
626
+ put("curved", true);
627
+ put("bendCurve", "bezier");
628
+ // an arc's head is drawn where the loop comes back to its node's center, under the node
629
+ // (`pajek-specimen-edges-lines-p2` `loop-arc`, `s 1 a A`: no head shows)
630
+ put("targetArrow", "none");
631
+ return [c1, c2];
632
+ }
633
+ /**
634
+ * The way a loop runs at its top, where its label is drawn (`lp` 0.5): from its first control point to its second
635
+ * (`pajek-specimen-edge-labels` `on-loop`: 179.97). ponytail: a label elsewhere on the loop (`lp`) is turned as at
636
+ * its top.
637
+ * @param controls - the loop's control points
638
+ * @returns the direction, in degrees clockwise on screen
639
+ */
640
+ function loopTop(controls) {
641
+ const [c1, c2] = controls;
642
+ return (Math.atan2(c2[1] - c1[1], c2[0] - c1[0]) * 180) / Math.PI;
643
+ }
644
+ /**
645
+ * Writes each loop's control points as its bends, fractions of its node's half width and half height, y up, once the
646
+ * nodes are sized, and draws it round whatever the Draw window does to the other lines.
647
+ * @param snapshot - the graph
648
+ * @param loops - each loop's control points by edge
649
+ * @param node - the node values, sized
650
+ * @param edge - the edge values; changed in place
651
+ */
652
+ function layLoops(snapshot, loops, node, edge) {
653
+ const fraction = (x) => String(Math.round(x * 1e4) / 1e4 || 0);
654
+ for (const [e, controls] of loops) {
655
+ const v = snapshot.edgeSource(e);
656
+ const half = (property) => {
657
+ const size = node[property]?.[v];
658
+ return (typeof size === "number" && size > 0 ? size : PAJEK_VERTEX_SIZE) / 2;
659
+ };
660
+ const [hw, hh] = [half("width"), half("height")];
661
+ const column = (property) => (edge[property] ?? (edge[property] = new Array(snapshot.edgeCount).fill(null)));
662
+ column("bends")[e] = controls.map(([x, y]) => `${fraction(x / hw)},${fraction(-y / hh)}`).join(" ");
663
+ // Export Line Default / Straight Lines straightens every other line; a loop is still drawn round
664
+ // (`pajek-specimen-edges-lines-p4`)
665
+ column("curved")[e] = true;
666
+ column("bendCurve")[e] = "bezier";
667
+ }
668
+ }
669
+ /**
670
+ * Where Pajek 6.02's SVG export draws a line's label (`pajek-specimen-edge-labels`): its middle at `lp` from the
671
+ * terminal vertex, a fraction of the line up to 1 (0.5 by default, 0.2 near the target) and a distance beyond it
672
+ * (`lp 20` 20 from the target), moved by `lr` at `lphi` degrees counterclockwise (`lr 12 lphi 90` 12 above), its
673
+ * baseline through that point, and turned along the line and `la` degrees counterclockwise from it (`la 45` drawn
674
+ * rotate(-45) on a level line; `la 405` as `la 45`). graphty hangs a line's label on the line's middle, so the place is
675
+ * an offset from there, the words' middle above the baseline in the label's own turned frame.
676
+ * @param get - the line's parameters
677
+ * @param put - receives the values
678
+ * @param at - the sites
679
+ * @param run - the line, or null when unplaced
680
+ * @param direction - the line's direction at the label, in degrees clockwise on screen, where `run` does not give it
681
+ * (a loop)
682
+ */
683
+ function readLineLabelPlace(get, put, at, run, direction) {
684
+ const length = run === null ? 0 : Math.hypot(run[0], run[1]);
685
+ if (run === null || length === 0) {
686
+ return;
687
+ }
688
+ const n = (key, fallback, property) => {
689
+ const v = get(key);
690
+ return v === null ? fallback : canonNumber(v, at(property));
691
+ };
692
+ const [lp, lr, lphi, la] = [
693
+ n("lp", 0.5, "labelOffsetX"),
694
+ n("lr", 0, "labelOffsetX"),
695
+ n("lphi", 0, "labelOffsetX"),
696
+ n("la", 0, "labelRotation"),
697
+ ];
698
+ const size = n("fos", PAJEK_LABEL_SIZE, "labelSize");
699
+ if (lp === null || lr === null || lphi === null || la === null || size === null) {
700
+ return;
701
+ }
702
+ // from the line's middle to the point lp from its target, then out by lr at lphi
703
+ const along = 0.5 - (lp <= 1 ? lp : lp / length);
704
+ const t = (lphi * Math.PI) / 180;
705
+ const round = (v) => Number(v.toFixed(9)) + 0;
706
+ const turn = (((direction ?? lineDirection(get, run, along + 0.5)) - la) % 360) + 360;
707
+ // the baseline runs through the point; the words' middle is above it in the label's own turned frame
708
+ const up = (turn * Math.PI) / 180;
709
+ const rise = PAJEK_BASELINE_TO_MIDDLE * size;
710
+ put("labelPosition", "center");
711
+ put("labelOffsetX", round(along * run[0] + lr * Math.cos(t) + rise * Math.sin(up)));
712
+ put("labelOffsetY", round(along * run[1] - lr * Math.sin(t) - rise * Math.cos(up)));
713
+ put("labelRotation", round(turn % 360));
714
+ }
715
+ /**
716
+ * A line's Bezier route (manual p. 90: `a1`, `k1` at its source, `a2`, `k2` at its target), as Pajek 6.02's SVG export
717
+ * draws it (`pajek-specimen-edges-lines-p2`, `pajek-specimen-edge-labels`): one cubic Bezier curve whose first control
718
+ * point is `k1` / 2 of the line's length from the source, turned `a1` degrees clockwise on screen from the line, and
719
+ * whose second is `k2` / 2 of it back from the target, turned `a2` degrees counterclockwise (`a1 40 k1 1.5 a2 -40 k2
720
+ * 1.5` an S, `a2 40` a bow). A negative `k1` is one of Pajek's oval, circular arc or double routes, which its SVG export
721
+ * draws straight. A loop is `readLoop`'s. ponytail: a line that sets only some of the four reads the
722
+ * others as 0 and 1; Pajek's values for them were not measured.
723
+ * @param get - the line's parameters
724
+ * @param put - receives the values
725
+ * @param run - the line, or null when unplaced
726
+ */
727
+ function readBezier(get, put, run) {
728
+ const controls = bezierControls(get, run);
729
+ if (controls === null) {
730
+ return;
731
+ }
732
+ const fraction = (x) => String(Math.round(x * 1e4) / 1e4 || 0);
733
+ put("curved", true);
734
+ put("bendCurve", "bezier");
735
+ put("bends", controls.map(([along, across]) => `${fraction(along)},${fraction(across)}`).join(" "));
736
+ }
737
+ /** How far back from its target's outline, as the file sizes it, Pajek 6.02 draws a Bezier arc's arrowhead tip. */
738
+ const PAJEK_TIP_BACK = 1.5;
739
+ /**
740
+ * Where Pajek 6.02's SVG export ends an arc's Bezier route (`readBezier`): at its arrowhead's back, not at its target's
741
+ * center. It places the arrowhead as on a straight arc, its tip 1.5 short of where the straight line meets the
742
+ * target's outline, turns it `-a2` degrees about that point, and runs the curve from the source's center to the
743
+ * arrowhead's back, its control points measured on that shorter line (`pajek-specimen-edges-lines-p2`
744
+ * `route-bezier-opposite-arc`: the arrowhead's back at (347.02, 124.32), 6.5 back from the outline at (352, 128.5),
745
+ * turned 40 degrees; `route-bezier-same-arc` at (571.02, 132.68), turned -40). The curve's control points are written
746
+ * in the frame of the line between the two centers, which graphty places bends in, and the route ends at a target port
747
+ * on the arrowhead's tip, 1.5 back from the outline along the turned arrowhead, so graphty's curve runs through the
748
+ * same control points to where Pajek's arrowhead ends: drawn to the target's center instead, the curve bowed 0.135 of
749
+ * its chord where Pajek's bows 0.196 (`pajek-specimen-arrows` `arrow-bezier`). ponytail: the outline is the ellipse
750
+ * of the vertex's width and height, as the file sizes it (before the Draw window's Size of Vertices or vectors),
751
+ * whatever its shape.
752
+ * @param get - the line's parameters
753
+ * @param put - receives the values
754
+ * @param run - the line from its source's center to its target's, in drawn units, y down
755
+ * @param target - the target vertex's width and height
756
+ * @returns whether the arc's route ends at its arrowhead's tip (a target port)
757
+ */
758
+ function readArcEnd(get, put, run, target) {
759
+ const ap = numberOrNull(get("ap")) ?? 0;
760
+ const length = Math.hypot(run[0], run[1]);
761
+ if (ap !== 0 || length === 0 || bezierControls(get, run) === null) {
762
+ return false;
763
+ }
764
+ const [ux, uy] = [run[0] / length, run[1] / length];
765
+ // the straight line meets the outline r from the target's center; the arrowhead's back is 1.5 + 5 s before it
766
+ const r = 1 / Math.hypot(ux / (target[0] / 2), uy / (target[1] / 2));
767
+ const back = PAJEK_TIP_BACK + 5 * (numberOrNull(get("s")) ?? 1);
768
+ const turn = (-(numberOrNull(get("a2")) ?? 0) * Math.PI) / 180;
769
+ const [c, s] = [Math.cos(turn), Math.sin(turn)];
770
+ // the arrowhead's back, from the source's center
771
+ const end = [run[0] - r * ux - back * (ux * c - uy * s), run[1] - r * uy - back * (ux * s + uy * c)];
772
+ const controls = bezierControls(get, end);
773
+ if (controls === null) {
774
+ return false;
775
+ }
776
+ // each control point on screen from the source's center, then along and to the left of the line to the target
777
+ const L2 = length * length;
778
+ const fraction = (x) => String(Math.round(x * 1e4) / 1e4 || 0);
779
+ const bends = controls.map(([along, left]) => {
780
+ const [x, y] = [along * end[0] + left * end[1], along * end[1] - left * end[0]];
781
+ return `${fraction((x * run[0] + y * run[1]) / L2)},${fraction((x * run[1] - y * run[0]) / L2)}`;
782
+ });
783
+ put("bends", bends.join(" "));
784
+ // the tip, from the target's center: 1.5 back from the outline along the turned arrowhead; a port is a fraction of
785
+ // the half width and half height, y up
786
+ const tip = [-r * ux - PAJEK_TIP_BACK * (ux * c - uy * s), -r * uy - PAJEK_TIP_BACK * (ux * s + uy * c)];
787
+ put("targetPort", `${fraction(tip[0] / (target[0] / 2))},${fraction(-tip[1] / (target[1] / 2))}`);
788
+ return true;
789
+ }
790
+ /** How far short of its target's outline, border and all, Pajek 6.02 stops an arc's arrowhead, in px. */
791
+ const PAJEK_TIP_GAP = 1;
792
+ /**
793
+ * Where an arc's arrowhead stops: PAJEK_TIP_GAP short of its target's outline, whatever the arrowhead's size `s` (6
794
+ * x `s` long), so a share of its length of 1 / (6 `s`) (`pajek-synth-lines`: every arc's tip 1.25 from the center
795
+ * of a vertex 20 across with a border 0.5 wide, `s` 1 to 5; the specimen sheets' 1.5 with a border 1 wide).
796
+ * @param get - the line's parameters
797
+ * @param put - receives the value
798
+ */
799
+ function readTipGap(get, put) {
800
+ const s = numberOrNull(get("s")) ?? 1;
801
+ if (s > 0) {
802
+ put("targetArrowGap", Number((PAJEK_TIP_GAP / (6 * s)).toPrecision(6)));
803
+ }
804
+ }
805
+ /**
806
+ * A line's two Bezier control points (`readBezier`), each `[along, across]` as fractions of the line from its source:
807
+ * along it, and to its left on screen.
808
+ * @param get - the line's parameters
809
+ * @param run - the line, or null when unplaced
810
+ * @returns the points, or null for a line Pajek draws straight or a loop
811
+ */
812
+ function bezierControls(get, run) {
813
+ const [a1, k1, a2, k2] = ["a1", "k1", "a2", "k2"].map((key) => numberOrNull(get(key)));
814
+ if ((a1 === null && k1 === null && a2 === null && k2 === null) || run === null || (run[0] === 0 && run[1] === 0)) {
815
+ return null;
816
+ }
817
+ const [s, t] = [k1 ?? 1, k2 ?? 1];
818
+ if (s < 0 || t < 0) {
819
+ return null;
820
+ }
821
+ const rad = Math.PI / 180;
822
+ const [u, v] = [(a1 ?? 0) * rad, (a2 ?? 0) * rad];
823
+ return [
824
+ [(s / 2) * Math.cos(u), (-s / 2) * Math.sin(u)],
825
+ [1 - (t / 2) * Math.cos(v), (-t / 2) * Math.sin(v)],
826
+ ];
827
+ }
828
+ /**
829
+ * A line's direction on screen, in degrees clockwise, where its label is drawn: along a straight line, or along its
830
+ * Bezier curve (`bezierControls`) at the label's place, `along` of the way from the source.
831
+ * @param get - the line's parameters
832
+ * @param run - the line
833
+ * @param along - the label's place, 0 at the source and 1 at the target
834
+ * @returns the direction
835
+ */
836
+ function lineDirection(get, run, along) {
837
+ const controls = bezierControls(get, run);
838
+ let [dx, dy] = run;
839
+ if (controls !== null) {
840
+ // the cubic's tangent at `along`, in the line's own frame (along, left), then on screen
841
+ const t = Math.min(1, Math.max(0, along));
842
+ const p = [[0, 0], ...controls, [1, 0]];
843
+ const d = (k) => 3 * (1 - t) ** 2 * (p[1][k] - p[0][k]) +
844
+ 6 * (1 - t) * t * (p[2][k] - p[1][k]) +
845
+ 3 * t ** 2 * (p[3][k] - p[2][k]);
846
+ const [a, l] = [d(0), d(1)];
847
+ // left of (dx, dy) on screen, y down, is (dy, -dx)
848
+ [dx, dy] = [a * run[0] + l * run[1], a * run[1] - l * run[0]];
849
+ }
850
+ return (Math.atan2(dy, dx) * 180) / Math.PI;
851
+ }
852
+ /**
853
+ * What a line's value draws (manual 5.3 p. 90): a line of value 0 is not drawn, and a negative one is drawn as Dots
854
+ * unless the line sets a pattern.
855
+ * @param value - the line's value, or null
856
+ * @param get - the line's parameters
857
+ * @param put - receives the values
858
+ * @param directed - whether the line is an arc
859
+ */
860
+ function readLineValue(value, get, put, directed = true) {
861
+ if (value === 0) {
862
+ put("visible", false);
863
+ }
864
+ else if (value !== null && value < 0 && get("p") === null) {
865
+ // drawn as Dots are (`pajek-specimen-edges-lines` `value-negative`)
866
+ put("lineStyle", pajekDash(get, directed));
867
+ }
868
+ }
869
+ /** The dash and the gap of Pajek 6.02's Dots, in px whatever the line's width (`stroke-dasharray:5`). */
870
+ const PAJEK_DASH = 5;
871
+ /**
872
+ * How a line drawn as Pajek's Dots looks: Pajek 6.02's SVG export draws it `stroke-dasharray:5`, dashes and gaps 5 px
873
+ * long whatever the line's width, so a line 10/3 px wide or wider, its dashes no longer than one and a half widths,
874
+ * shows dots and a thinner one dashes (`pajek-specimen-edges-lines` `p-dots` at 1.5: dashes; `pajek-synth-lines`
875
+ * arc `12 13` at w 5 and edge `1 8` at w 3, drawn 6 wide: dots).
876
+ * @param get - the line's parameters
877
+ * @param directed - whether the line is an arc, drawn `w` wide (an edge twice `w`)
878
+ * @returns the line style
879
+ */
880
+ function pajekDash(get, directed) {
881
+ const w = numberOrNull(get("w"));
882
+ // without a `w`, Pajek's arc is 1 wide and its edge 2
883
+ const width = (w ?? 1) * (directed ? 1 : EDGE_WIDTH_PER_W);
884
+ return PAJEK_DASH <= 1.5 * Math.max(1, width) ? "dotted" : "dashed";
885
+ }
886
+ /**
887
+ * A line's hooks `h1` and `h2` (manual p. 90): the points of its source's and its target's outline it runs between,
888
+ * numbered by Pajek's shipped SHAPES.CFG for the vertex's shape. An ellipse's hook k is at k degrees counterclockwise
889
+ * from the right (CIRC `10 1`), a box's runs round its border in 400 steps from the middle of its right side,
890
+ * counterclockwise (CART), and a cross, which Pajek draws as an ellipse, has an ellipse's; `pajek-specimen-edges-lines-p2`:
891
+ * `h1 90 h2 270` runs from the top of the source to the bottom of the target, `h1 1 h2 1` ends on the far side of the
892
+ * target, `h1 1` on a box leaves at its right side. 0 (the center) and -1 (the nearest hook) are where graphty runs a
893
+ * line anyway. ponytail: the hooks of the other shapes (a diamond's 360 polar radii, a triangle's, a figure's) are
894
+ * not read; Pajek's box hook 100 was measured 0.22 of the half width left of the top's middle, where SHAPES.CFG puts
895
+ * it.
896
+ * @param get - the line's parameters
897
+ * @param put - receives the values
898
+ * @param source - the source vertex's shape keyword, or null
899
+ * @param target - the target vertex's shape keyword, or null
900
+ */
901
+ function readHooks(get, put, source, target) {
902
+ for (const [key, property, shape] of [
903
+ ["h1", "sourcePort", source],
904
+ ["h2", "targetPort", target],
905
+ ]) {
906
+ const hook = numberOrNull(get(key));
907
+ const port = hook === null ? null : hookPort(shape, hook);
908
+ if (port !== null) {
909
+ const round = (v) => String(Number(v.toFixed(4)) + 0);
910
+ put(property, `${round(port[0])},${round(port[1])}`);
911
+ }
912
+ }
913
+ }
914
+ /**
915
+ * One hook's point on its vertex (`readHooks`), as fractions of the half width and height, y up.
916
+ * @param shape - the vertex's shape keyword, null for Pajek's default ellipse
917
+ * @param hook - the hook's number
918
+ * @returns the point, or null where Pajek's hook is not read
919
+ */
920
+ function hookPort(shape, hook) {
921
+ if (!Number.isInteger(hook) || hook < 1) {
922
+ return null;
923
+ }
924
+ const keyword = typeof shape === "string" ? shape.toLowerCase() : "ellipse";
925
+ // Pajek 6.02 draws a cross as an ellipse, with an ellipse's hooks (`h-cross-1-3`: hook 3 at 3 degrees)
926
+ if (keyword === "ellipse" || keyword === "cross") {
927
+ const a = (hook * Math.PI) / 180;
928
+ return [Math.cos(a), Math.sin(a)];
929
+ }
930
+ if (keyword !== "box" || hook > 400) {
931
+ return null;
932
+ }
933
+ // 0.02 of the half size a step: up the right side's top half, across the top, down the left, across the
934
+ // bottom and up the right side's bottom half
935
+ const d = hook * 0.02;
936
+ if (hook <= 50) {
937
+ return [1, d];
938
+ }
939
+ if (hook <= 150) {
940
+ return [2 - d, 1];
941
+ }
942
+ if (hook <= 250) {
943
+ return [-1, 4 - d];
944
+ }
945
+ return hook <= 350 ? [d - 6, -1] : [1, d - 8];
946
+ }
947
+ /**
948
+ * A cell's number: a number as is, a numeric text parsed, anything else null.
949
+ * @param raw - the cell
950
+ * @returns the number, or null
951
+ */
952
+ function numberOrNull(raw) {
953
+ if (typeof raw === "string" && raw.trim() !== "") {
954
+ raw = Number(raw);
955
+ }
956
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
957
+ }
958
+ /**
959
+ * Where a line's arrowhead sits, Pajek's `ap` (manual p. 90): a fraction of the line from its end up to 1 (0.5 is the
960
+ * middle); a value above 1 is a distance from the end in px, the arrowhead's back that far from the target's center
961
+ * (`pajek-synth-lines` arc `3 4 ... ap 20`), read as its share of a placed line and noted on an unplaced one.
962
+ * @param raw - the parameter's value, or null
963
+ * @param put - receives the value
964
+ * @param at - the sites
965
+ * @param run - the line from its source's center to its target's, in drawn units, or null when unplaced
966
+ */
967
+ function readArrowPosition(raw, put, at, run = null) {
968
+ if (raw === null) {
969
+ return;
970
+ }
971
+ const ap = canonNumber(raw, at("arrowPosition"));
972
+ const length = run === null ? 0 : Math.hypot(run[0], run[1]);
973
+ if (ap !== null && ap > 1) {
974
+ if (length > 0) {
975
+ put("arrowPosition", Number(Math.min(1, ap / length).toPrecision(6)));
976
+ }
977
+ else {
978
+ noteAt(at("arrowPosition"), STYLE_ISSUE.NOT_CARRIED, `ap ${String(ap)}`);
979
+ }
980
+ return;
981
+ }
982
+ // 0 is Pajek's own place for the arrow, at the line's end, which a line restates after one that moved it
983
+ if (ap !== 0) {
984
+ put("arrowPosition", ap);
149
985
  }
150
986
  }
151
987
  /**
@@ -155,8 +991,18 @@ function readLine(get, put, at) {
155
991
  * @returns the getter
156
992
  */
157
993
  function cells(table, i) {
158
- return (key) => (table.has(key) ? plainCell(table.value(key, i)) : null);
994
+ return (key) => {
995
+ const v = table.has(key) ? plainCell(table.value(key, i)) : null;
996
+ // a color written as numbers (`c 0.700 0.700 0.700`) is no color to Pajek 6.02: the line keeps the color it
997
+ // carries from the line before, or Pajek's default (`pajek-1crn`, `pajek-specimen-edges-lines`
998
+ // `c-three-fractions`). ponytail: measured on lines; vertex colors are read the same way
999
+ return COLOR_PARAMS.has(key) && typeof v === "string" && NUMERIC_COLOR.test(v) ? null : v;
1000
+ };
159
1001
  }
1002
+ /** The parameters that hold a color. */
1003
+ const COLOR_PARAMS = new Set(["ic", "bc", "lc", "c"]);
1004
+ /** A color written as numbers rather than a name or an RGB / CMYK form. */
1005
+ const NUMERIC_COLOR = /^[\d.]+(?: [\d.]+)*$/;
160
1006
  /**
161
1007
  * Whether each edge is an arc (directed).
162
1008
  * @param snapshot - the graph
@@ -172,6 +1018,57 @@ function directedEdges(snapshot) {
172
1018
  }
173
1019
  return out;
174
1020
  }
1021
+ /** The Bezier route Pajek 6.02 draws an arc on that shares its two vertices with another arc. */
1022
+ const SHARED_ARC_BOW = { a1: 15, k1: 0.25, a2: 15, k2: 0.25 };
1023
+ /**
1024
+ * The arcs Pajek 6.02 bows apart, and to which side: an arc that shares its two vertices with another arc, either way
1025
+ * round, is drawn as `a1 15 k1 0.25 a2 15 k2 0.25` (`pajek-synth-lines`: 20 -> 30 and 30 -> 20, each bowed to the
1026
+ * same side of its own way, so apart; `pajek-iliad` 14 -> 72 and 72 -> 14), and of two that run the same way the first is bowed the
1027
+ * other way, `a1 -15 a2 -15` (`pajek-iliad`: the two arcs 250 -> 123). ponytail: three or more arcs the same way
1028
+ * alternate sides, the last at `a1 15`; Pajek's drawing of them was not measured.
1029
+ * @param snapshot - the graph
1030
+ * @param directed - whether each edge is an arc
1031
+ * @returns each bowed arc's side: 1 for `a1 15 a2 15`, -1 for `a1 -15 a2 -15`
1032
+ */
1033
+ function sharedArcBows(snapshot, directed) {
1034
+ const byPair = new Map();
1035
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1036
+ const [u, v] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
1037
+ if (directed[e] && u !== v) {
1038
+ const key = u < v ? `${String(u)} ${String(v)}` : `${String(v)} ${String(u)}`;
1039
+ byPair.set(key, [...(byPair.get(key) ?? []), e]);
1040
+ }
1041
+ }
1042
+ const out = new Map();
1043
+ for (const arcs of byPair.values()) {
1044
+ if (arcs.length < 2) {
1045
+ continue;
1046
+ }
1047
+ for (const e of arcs) {
1048
+ const same = arcs.filter((x) => snapshot.edgeSource(x) === snapshot.edgeSource(e));
1049
+ out.set(e, (same.length - 1 - same.indexOf(e)) % 2 === 0 ? 1 : -1);
1050
+ }
1051
+ }
1052
+ return out;
1053
+ }
1054
+ /**
1055
+ * A bowed arc's parameters (`sharedArcBows`): its own Bezier route when it sets one, else Pajek's bow to its side.
1056
+ * @param get - the line's parameters
1057
+ * @param side - 1 for `a1 15 a2 15`, -1 for `a1 -15 a2 -15`
1058
+ * @returns the parameters
1059
+ */
1060
+ function bowedParams(get, side) {
1061
+ if (Object.keys(SHARED_ARC_BOW).some((key) => get(key) !== null)) {
1062
+ return get;
1063
+ }
1064
+ return (key) => {
1065
+ const bow = SHARED_ARC_BOW[key];
1066
+ if (bow === undefined) {
1067
+ return get(key);
1068
+ }
1069
+ return key.startsWith("a") ? side * bow : bow;
1070
+ };
1071
+ }
175
1072
  /**
176
1073
  * The columns of a table that hold styles.
177
1074
  * @param table - the node or edge table
@@ -182,37 +1079,97 @@ function styleColumns(table, keys) {
182
1079
  return keys.filter((k) => table.has(k));
183
1080
  }
184
1081
  /**
185
- * The styles of a Pajek file, read the way Pajek draws them, with Pajek's stated defaults beneath: a labeled ellipse
186
- * with a black border and a black centered label, black solid lines, and an arrow at the target of every arc. A file graphty wrote gives back its `graphty.styles` block.
1082
+ * The styles of a Pajek file, read the way Pajek draws them, with Pajek's own defaults beneath: a labeled #ffd9bc
1083
+ * ellipse with a #660000 border, teal edges, and grey arcs with an arrow at the target. A file graphty wrote gives back its `graphty.styles` block.
187
1084
  * @param snapshot - the graph the import built
188
1085
  * @param context - the work budget and the block text the import kept
189
1086
  * @returns the styles
190
1087
  */
191
1088
  export function pajekStyles(snapshot, context) {
192
1089
  const notes = new StyleNotes();
1090
+ const { block, draw, voided } = keptOf(context.kept);
1091
+ const labels = drawsLineLabels(draw);
1092
+ const place = places(snapshot);
1093
+ // Pajek stretches the file's 0..1 to its Draw window one axis at a time, 500 x 500 without one
1094
+ const window = draw ?? { width: PAJEK_POINTS_PER_UNIT, height: PAJEK_POINTS_PER_UNIT };
1095
+ const loops = new Map();
1096
+ const runOf = (e) => {
1097
+ const [s, t] = [place(snapshot.edgeSource(e)), place(snapshot.edgeTarget(e))];
1098
+ return s === null || t === null ? null : [(t[0] - s[0]) * window.width, (t[1] - s[1]) * window.height];
1099
+ };
193
1100
  const read = (target) => {
194
1101
  const table = target === "node" ? snapshot.nodes : snapshot.edges;
195
1102
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
196
1103
  const out = valueColumns(n);
1104
+ const carry = carriedParams(target === "node" ? CARRIED.node : CARRIED_LINE, target === "node");
197
1105
  for (let i = 0; i < n; i++) {
198
1106
  context.budget.spend(target, 1);
199
1107
  const at = (property) => ({ notes, target, property, element: i, budget: context.budget });
200
- (target === "node" ? readVertex : readLine)(cells(table, i), (property, value) => {
1108
+ const put = (property, value) => {
201
1109
  out.put(i, property, value);
202
- }, at);
1110
+ };
1111
+ const own = cells(table, i);
1112
+ const get = target === "node" && voided.has(i) ? voidedParams(own, keywords[i - 1] ?? null) : carry(own);
1113
+ if (target === "node") {
1114
+ readVertex(get, put, at, place(i));
1115
+ keywords[i] = get(SHAPE_COLUMN);
1116
+ }
1117
+ else {
1118
+ readEdge(i, get, put, at);
1119
+ }
203
1120
  }
204
1121
  return out.values;
205
1122
  };
1123
+ const readEdge = (i, get, put, at) => {
1124
+ // a loop on a placed node: an unplaced one is laid out by graphty, which draws its own loop, and a
1125
+ // file graphty wrote keeps its routes in its block
1126
+ const run = runOf(i);
1127
+ const loop = run !== null && block === null && snapshot.edgeSource(i) === snapshot.edgeTarget(i)
1128
+ ? (controls) => {
1129
+ loops.set(i, controls);
1130
+ }
1131
+ : null;
1132
+ const bow = bows.get(i);
1133
+ const line = bow === undefined ? get : bowedParams(get, bow);
1134
+ readLine(line, put, at, directed[i], labels, run, loop);
1135
+ // an arc's arrowhead stops short of its target: at the port on its tip of a Bezier route, else by its gap (a
1136
+ // file graphty wrote keeps its gaps in its block)
1137
+ const ported = directed[i] && run !== null && readArcEnd(line, put, run, targetSize(snapshot.edgeTarget(i)));
1138
+ if (directed[i] && loop === null && !ported && block === null) {
1139
+ readTipGap(get, put);
1140
+ }
1141
+ readLineValue(lineValue(i), get, put, directed[i]);
1142
+ readHooks(get, put, keywords[snapshot.edgeSource(i)], keywords[snapshot.edgeTarget(i)]);
1143
+ };
1144
+ const directed = directedEdges(snapshot);
1145
+ // Straight Lines draws the arcs Pajek would bow apart straight, and a file graphty wrote keeps its routes in its
1146
+ // block
1147
+ const bows = drawsStraightLines(draw) || block !== null ? new Map() : sharedArcBows(snapshot, directed);
1148
+ const keywords = [];
1149
+ const weights = explicitWeights(snapshot);
1150
+ // the line's value: its weight, or the `value` column when the import kept the value as an attribute
1151
+ const lineValue = (e) => weights.isExplicit(e) ? weights.value(e) : numberOrNull(cells(snapshot.edges, e)(VALUE_COLUMN));
206
1152
  const node = read("node");
1153
+ // a vertex's width and height as the file draws it, for where an arc's arrowhead meets it
1154
+ const targetSize = (v) => {
1155
+ const size = (property) => {
1156
+ const s = node[property]?.[v];
1157
+ return typeof s === "number" && s > 0 ? s : PAJEK_VERTEX_SIZE;
1158
+ };
1159
+ return [size("width"), size("height")];
1160
+ };
207
1161
  const edge = read("edge");
208
- const block = typeof context.kept === "string" ? context.kept : null;
1162
+ if (draw !== null) {
1163
+ drawVertices(draw, snapshot, node);
1164
+ drawLines(draw, snapshot, node, edge);
1165
+ }
1166
+ layLoops(snapshot, loops, node, edge);
209
1167
  if (block !== null) {
210
- exactFromBlock(snapshot, block, { node, edge });
1168
+ exactFromBlock(snapshot, block, { node, edge }, labels);
211
1169
  }
212
- // Pajek's default that is a rule: an arc has an arrow at its target, an edge none
1170
+ // Pajek's defaults that are a rule: an arc has an arrow at its target and is drawn grey and thinner, an edge not
213
1171
  const rules = [];
214
1172
  const columns = new Map();
215
- const directed = directedEdges(snapshot);
216
1173
  if (directed.some(Boolean)) {
217
1174
  columns.set(DIRECTED_KEY, directed.map((d) => (d ? true : null)));
218
1175
  rules.push({
@@ -221,14 +1178,14 @@ export function pajekStyles(snapshot, context) {
221
1178
  role: "group",
222
1179
  origin: "tool",
223
1180
  where: { has: { style: DIRECTED_KEY } },
224
- set: { targetArrow: "triangle" },
1181
+ set: { targetArrow: "triangle", color: ARC_DEFAULTS.color, width: ARC_DEFAULTS.width },
225
1182
  });
226
1183
  }
227
1184
  return {
228
1185
  canonical: true,
229
1186
  dialect: "dvips",
230
1187
  tool: "pajek",
231
- units: "relative",
1188
+ units: "px",
232
1189
  node: { values: node },
233
1190
  edge: { values: edge, rules, columns },
234
1191
  notes: notes.list(),
@@ -239,6 +1196,60 @@ export function pajekStyles(snapshot, context) {
239
1196
  },
240
1197
  };
241
1198
  }
1199
+ /**
1200
+ * Each vertex's place in the file's own coordinates, as the position column holds them.
1201
+ * @param snapshot - the graph
1202
+ * @returns a getter of a vertex's place, null when it has none
1203
+ */
1204
+ function places(snapshot) {
1205
+ const col = snapshot.nodes.byRole("position");
1206
+ return (i) => {
1207
+ const v = col === null || col === undefined ? null : col.value(i);
1208
+ if (v === null || typeof v !== "object" || !("length" in v)) {
1209
+ return null;
1210
+ }
1211
+ const [x, y] = v;
1212
+ return typeof x === "number" && typeof y === "number" ? [x, y] : null;
1213
+ };
1214
+ }
1215
+ /**
1216
+ * What the import kept for the styles: the `graphty.styles` block, the Draw window of the `pajekDraw` option, and the
1217
+ * vertices whose line Pajek draws with none of its parameters.
1218
+ * @param kept - the kept value: the block's text, or `{ block, pajekDraw, voided }`
1219
+ * @returns the block and the draw settings, each null when absent, and the voided vertices
1220
+ */
1221
+ function keptOf(kept) {
1222
+ if (typeof kept === "string") {
1223
+ return { block: kept, draw: null, voided: new Set() };
1224
+ }
1225
+ if (typeof kept === "object" && kept !== null) {
1226
+ const { block, pajekDraw, voided } = kept;
1227
+ return {
1228
+ block: typeof block === "string" ? block : null,
1229
+ draw: pajekDraw ?? null,
1230
+ voided: new Set(voided ?? []),
1231
+ };
1232
+ }
1233
+ return { block: null, draw: null, voided: new Set() };
1234
+ }
1235
+ /**
1236
+ * The parameters Pajek 6.02 draws a voided vertex line with (`VOIDING_KEYS` in the importer): its label, and none of
1237
+ * its own parameters, so the vertex is Pajek's default size and colors, in the shape of the vertex line before it
1238
+ * (`pajek-specimen-node-sizes-opacity` `s-size-no-shape`, after a box: a box 20 across in #ffd9bc). ponytail: probes
1239
+ * showed the shape is not always the line before's (after `box ic Green` a voided line drew an ellipse in one file and
1240
+ * the triangle of two lines before in another); the rule Pajek follows was not found.
1241
+ * @param own - the line's own parameters
1242
+ * @param before - the shape keyword of the vertex line before it, or null
1243
+ * @returns the getter
1244
+ */
1245
+ function voidedParams(own, before) {
1246
+ return (key) => {
1247
+ if (key === LABEL_COLUMN) {
1248
+ return own(key);
1249
+ }
1250
+ return key === SHAPE_COLUMN ? before : null;
1251
+ };
1252
+ }
242
1253
  /** Pajek's shape keywords for graphty's shapes; a shape not listed is written as its nearest. */
243
1254
  const SHAPES = new Map([
244
1255
  ["ellipse", "ellipse"],
@@ -297,12 +1308,57 @@ function shapeKeyword(get, approximate) {
297
1308
  if (keyword !== undefined) {
298
1309
  return keyword;
299
1310
  }
1311
+ if (shape === "polygon" && diamondQ(get) !== null) {
1312
+ return "diamond";
1313
+ }
300
1314
  approximate("shape", shape);
1315
+ const points = get("shapePoints");
1316
+ if (shape === "polygon" && points !== null) {
1317
+ approximate("shapePoints", points);
1318
+ }
301
1319
  return NEAREST_SHAPES.get(shape) ?? "ellipse";
302
1320
  }
303
1321
  /**
304
- * A vertex's parameters from its values, leaving out what Pajek draws by default: an ellipse with a black border 1
305
- * wide and a black label.
1322
+ * The `q` of a polygon that is Pajek's unturned diamond (`readOutline`), or null for any other.
1323
+ * @param get - the vertex's values
1324
+ * @returns the diamond's `q`, or null
1325
+ */
1326
+ function diamondQ(get) {
1327
+ const points = get("shapePoints");
1328
+ const corners = typeof points === "string"
1329
+ ? points
1330
+ .trim()
1331
+ .split(/\s+/)
1332
+ .map((p) => p.split(",").map(Number))
1333
+ : [];
1334
+ if (corners.length !== 6) {
1335
+ return null;
1336
+ }
1337
+ const q = corners[2][0];
1338
+ const want = [
1339
+ [-1, 0],
1340
+ [-q, 1],
1341
+ [q, 1],
1342
+ [1, 0],
1343
+ [q, -1],
1344
+ [-q, -1],
1345
+ ];
1346
+ return want.every(([x, y], k) => corners[k][0] === x && corners[k][1] === y) ? q : null;
1347
+ }
1348
+ /**
1349
+ * What Pajek draws by default for a property (`styles/defaults.ts`), which the writer leaves out.
1350
+ * @param target - nodes or edges
1351
+ * @param property - the property
1352
+ * @returns the default, or undefined
1353
+ */
1354
+ function pajekDefault(target, property) {
1355
+ const values = toolDefaults("pajek")?.[target];
1356
+ return values?.[property]?.value;
1357
+ }
1358
+ /**
1359
+ * A vertex's parameters from its values, leaving out the ellipse and the black border 1 wide Pajek draws by default;
1360
+ * the inverse of `readVertex`: `x_fact` and `y_fact` are the width and height over the normal size, the other lengths
1361
+ * points as they are.
306
1362
  * @param get - the vertex's values
307
1363
  * @param approximate - records approximations
308
1364
  * @returns the parameters, the shape keyword under `shape`
@@ -314,43 +1370,139 @@ function vertexParams(get, approximate) {
314
1370
  if (keyword !== "ellipse") {
315
1371
  out.set(SHAPE_COLUMN, keyword);
316
1372
  }
1373
+ const q = diamondQ(get);
1374
+ if (q !== null && q !== PAJEK_DIAMOND_Q) {
1375
+ out.set("q", q);
1376
+ }
1377
+ vertexColorParams(get, out);
1378
+ vertexSizeParams(get, out);
1379
+ const bw = get("borderWidth");
1380
+ if (typeof bw === "number" && bw !== pajekDefault("node", "borderWidth")) {
1381
+ out.set("bw", bw);
1382
+ }
1383
+ fontParam(get, out, pajekDefault("node", "labelFont"));
1384
+ labelPositionParams(get, out, approximate);
1385
+ return out;
1386
+ }
1387
+ /**
1388
+ * A vertex's colors as parameters (`ic`, `bc`, `lc`), leaving out Pajek's defaults.
1389
+ * @param get - the vertex's values
1390
+ * @param out - receives the parameters
1391
+ */
1392
+ function vertexColorParams(get, out) {
317
1393
  const colors = [
318
1394
  ["fill", "ic"],
319
1395
  ["borderColor", "bc"],
320
1396
  ["labelColor", "lc"],
321
1397
  ];
322
- const numbers = [
323
- ["width", "x_fact"],
324
- ["height", "y_fact"],
325
- ["labelSize", "fos"],
326
- ];
327
1398
  for (const [property, key] of colors) {
328
1399
  const v = get(property);
329
- if (typeof v === "string" && (property === "fill" || v !== "#000000")) {
1400
+ if (typeof v === "string" && v !== pajekDefault("node", property)) {
330
1401
  out.set(key, pajekColorText(v));
331
1402
  }
332
1403
  }
333
- for (const [property, key] of numbers) {
1404
+ }
1405
+ /**
1406
+ * A vertex's sizes as parameters (`x_fact`, `y_fact`, `fos`), leaving out Pajek's defaults.
1407
+ * @param get - the vertex's values
1408
+ * @param out - receives the parameters
1409
+ */
1410
+ function vertexSizeParams(get, out) {
1411
+ const numbers = [
1412
+ ["width", "x_fact", PAJEK_VERTEX_SIZE],
1413
+ ["height", "y_fact", PAJEK_VERTEX_SIZE],
1414
+ ["labelSize", "fos", 1],
1415
+ ];
1416
+ // a man's or a woman's box is the figure's, which reaches past the size its x_fact and y_fact give (readSize)
1417
+ const figure = PAJEK_FIGURES.get(String(get("shape")));
1418
+ for (const [property, key, per] of numbers) {
334
1419
  const v = get(property);
335
- if (typeof v === "number") {
336
- out.set(key, v);
1420
+ let over = 1;
1421
+ if (figure !== undefined && key !== "fos") {
1422
+ over = figure[key === "x_fact" ? 0 : 1];
1423
+ }
1424
+ if (typeof v === "number" && (over !== 1 || v !== pajekDefault("node", property))) {
1425
+ out.set(key, Number((v / (per * over)).toPrecision(15)));
337
1426
  }
338
1427
  }
339
- const bw = get("borderWidth");
340
- if (typeof bw === "number") {
341
- const written = Number((bw * VERTEX_SIZE).toPrecision(15));
342
- if (written === 1) {
343
- out.delete("bw");
1428
+ }
1429
+ /**
1430
+ * A label's `font`, the inverse of `readFont`: a PostScript family with a generic one after it is written by its
1431
+ * PostScript name (`Courier, monospace` as `Courier`), any other family as it is; Pajek draws every label bold and
1432
+ * upright, so neither the weight nor the slant is written.
1433
+ * @param get - the element's values
1434
+ * @param out - the parameters, changed in place
1435
+ * @param skip - the font left out, Pajek's default, or undefined
1436
+ */
1437
+ function fontParam(get, out, skip) {
1438
+ const font = get("labelFont");
1439
+ const name = typeof font === "string" && font !== "" ? graphvizFontName(font, false, false) : null;
1440
+ if (name !== null && name !== skip) {
1441
+ out.set("font", name);
1442
+ }
1443
+ }
1444
+ /**
1445
+ * A label position as Pajek's polar label parameters, leaving out the default (20 from the center at 285 degrees): a
1446
+ * label centered with its left edge's middle offset from the center is `lr` and `lphi` (the inverse of
1447
+ * `readLabelPosition`), one centered on the vertex `lr 0`, and a side its angle `lphi`; a side Pajek cannot put a
1448
+ * label on is written as the nearest it can, and noted.
1449
+ * @param get - the vertex's values
1450
+ * @param out - the parameters, changed in place
1451
+ * @param approximate - records approximations
1452
+ */
1453
+ function labelPositionParams(get, out, approximate) {
1454
+ const position = get("labelPosition");
1455
+ if (typeof position !== "string") {
1456
+ return;
1457
+ }
1458
+ if (position === "center") {
1459
+ const polar = polarOf(get);
1460
+ if (polar === null) {
1461
+ out.set("lr", 0);
344
1462
  }
345
- else {
346
- out.set("bw", written);
1463
+ else if (polar[0] !== PAJEK_LABEL_RADIUS || polar[1] !== PAJEK_LABEL_ANGLE) {
1464
+ out.set("lr", polar[0]);
1465
+ out.set("lphi", polar[1]);
347
1466
  }
1467
+ return;
348
1468
  }
349
- return out;
1469
+ const nearest = NEAREST_SIDES.get(position);
1470
+ if (nearest !== undefined) {
1471
+ approximate("labelPosition", position);
1472
+ }
1473
+ const angle = SIDE_ANGLES.get(nearest ?? position);
1474
+ if (angle !== undefined && angle !== PAJEK_LABEL_ANGLE) {
1475
+ out.set("lphi", angle);
1476
+ }
1477
+ }
1478
+ /**
1479
+ * The `lr` and `lphi` that put a centered label where its offsets say (`labelOffset`, inverted), 6 places each.
1480
+ * @param get - the vertex's values
1481
+ * @returns `[lr, lphi]`, or null for a label on the vertex's center (no offset, no shift)
1482
+ */
1483
+ function polarOf(get) {
1484
+ const [x, y, shift] = [get("labelOffsetX"), get("labelOffsetY"), get("labelShiftX")];
1485
+ if (typeof x !== "number" || typeof y !== "number" || shift !== 0.5) {
1486
+ return null;
1487
+ }
1488
+ const size = get("labelSize");
1489
+ const up = y + PAJEK_BASELINE_TO_MIDDLE * (typeof size === "number" ? size : PAJEK_LABEL_SIZE);
1490
+ const round = (v) => Number(v.toFixed(6)) + 0;
1491
+ const lr = round(Math.hypot(x, up));
1492
+ return lr === 0 ? null : [lr, round(((((Math.atan2(-up, x) * 180) / Math.PI) % 360) + 360) % 360)];
350
1493
  }
351
1494
  /**
352
- * A line's parameters from its values, leaving out Pajek's black solid line. Arrows are Pajek's own: an arc has one
353
- * at its target and an edge none, so any other arrow is noted.
1495
+ * A line's color and width when it sets neither: an arc's, or an edge's (the tool defaults).
1496
+ * @param directed - whether the line is an arc
1497
+ * @returns the color and width
1498
+ */
1499
+ function lineDefaults(directed) {
1500
+ return directed ? ARC_DEFAULTS : { color: pajekDefault("edge", "color"), width: pajekDefault("edge", "width") };
1501
+ }
1502
+ /**
1503
+ * A line's parameters from its values, leaving out the solid line Pajek draws by default (grey and 1 wide for an arc,
1504
+ * teal and 2 wide for an edge; `w` is half an edge's width). Arrows are Pajek's own: an arc has one at its target and
1505
+ * an edge none, so any other arrow is noted.
354
1506
  * @param get - the line's values
355
1507
  * @param directed - whether the line is an arc
356
1508
  * @param approximate - records approximations
@@ -359,18 +1511,21 @@ function vertexParams(get, approximate) {
359
1511
  function lineParams(get, directed, approximate) {
360
1512
  const out = new Map();
361
1513
  opaque(get, ["opacity"], approximate);
1514
+ const defaults = lineDefaults(directed);
362
1515
  const color = get("color");
363
- if (typeof color === "string" && color !== "#000000") {
1516
+ if (typeof color === "string" && color !== defaults.color) {
364
1517
  out.set("c", pajekColorText(color));
365
1518
  }
366
1519
  const width = get("width");
367
- if (typeof width === "number") {
368
- out.set("w", width);
1520
+ if (typeof width === "number" && width !== defaults.width) {
1521
+ out.set("w", directed ? width : Number((width / EDGE_WIDTH_PER_W).toPrecision(15)));
369
1522
  }
370
1523
  const lineStyle = get("lineStyle");
371
- if (typeof lineStyle === "string" && lineStyle !== "solid" && pajekDots(lineStyle, approximate)) {
1524
+ if (typeof lineStyle === "string" && lineStyle !== "solid" && pajekDots(lineStyle, out, directed, approximate)) {
372
1525
  out.set("p", "Dots");
373
1526
  }
1527
+ arrowParams(get, out);
1528
+ lineLabelParams(get, out, approximate);
374
1529
  const target = get("targetArrow");
375
1530
  if (target !== null && target !== (directed ? "triangle" : "none")) {
376
1531
  approximate("targetArrow", target);
@@ -382,77 +1537,103 @@ function lineParams(get, directed, approximate) {
382
1537
  return out;
383
1538
  }
384
1539
  /**
385
- * Whether a line other than a solid one is written as Pajek's Dots: Pajek draws Solid and Dots only; a dashed kind is
1540
+ * A line's arrowhead parameters: its size `s`, which also places a Bezier arc's end (`readArcEnd`), and its place `ap`.
1541
+ * @param get - the line's values
1542
+ * @param out - the parameters, changed in place
1543
+ */
1544
+ function arrowParams(get, out) {
1545
+ const arrowSize = get("arrowSize");
1546
+ if (typeof arrowSize === "number" && arrowSize !== 1) {
1547
+ out.set("s", arrowSize);
1548
+ }
1549
+ const position = get("arrowPosition");
1550
+ if (typeof position === "number" && position > 0) {
1551
+ out.set("ap", position);
1552
+ }
1553
+ }
1554
+ /**
1555
+ * A line's label parameters: its text `l` (a line break or a double quote written as Pajek can hold it, noted), its
1556
+ * color `lc`, size `fos` and font `font`.
1557
+ * @param get - the line's values
1558
+ * @param out - the parameters, changed in place
1559
+ * @param approximate - records approximations
1560
+ */
1561
+ function lineLabelParams(get, out, approximate) {
1562
+ const label = get("label");
1563
+ if (typeof label === "string" && label !== "") {
1564
+ const text = pajekLabelText(label);
1565
+ if (text !== label) {
1566
+ approximate("label", label);
1567
+ }
1568
+ out.set("l", text);
1569
+ }
1570
+ const labelColor = get("labelColor");
1571
+ if (typeof labelColor === "string") {
1572
+ out.set("lc", pajekColorText(labelColor));
1573
+ }
1574
+ const labelSize = get("labelSize");
1575
+ if (typeof labelSize === "number") {
1576
+ out.set("fos", labelSize);
1577
+ }
1578
+ fontParam(get, out);
1579
+ }
1580
+ /**
1581
+ * Whether a line other than a solid one is written as Pajek's Dots: Pajek draws Solid and Dots only, and its Dots
1582
+ * shows dots on a wide line and dashes on a thin one (`pajekDash`); the other of the two and any other dashed kind is
386
1583
  * nearest to Dots, any other line to Solid.
387
1584
  * @param lineStyle - the line style
1585
+ * @param params - the line's parameters written so far, its `w` among them
1586
+ * @param directed - whether the line is an arc
388
1587
  * @param approximate - records approximations
389
1588
  * @returns true for Dots
390
1589
  */
391
- function pajekDots(lineStyle, approximate) {
392
- if (lineStyle === "dotted") {
1590
+ function pajekDots(lineStyle, params, directed, approximate) {
1591
+ if (lineStyle === pajekDash((key) => params.get(key) ?? null, directed)) {
393
1592
  return true;
394
1593
  }
395
1594
  approximate("lineStyle", lineStyle);
396
- return lineStyle === "dashed" || lineStyle === "dash-dot" || lineStyle === "long-dash";
397
- }
398
- /**
399
- * Each element's values of the properties the writer writes, as a getter per element.
400
- * @param picture - each element's value of a property
401
- * @param target - nodes or edges
402
- * @returns the getter
403
- */
404
- function getter(picture, target) {
405
- const columns = new Map();
406
- for (const p of PAJEK_STYLE_CAPABILITIES[target]) {
407
- columns.set(p, picture(target, p));
408
- }
409
- return (i) => (property) => columns.get(property)?.[i] ?? null;
1595
+ return ["dashed", "dotted", "dash-dot", "long-dash"].includes(lineStyle);
410
1596
  }
411
1597
  /**
412
1598
  * For a file graphty wrote: where a value read from the parameters is what the writer writes for the block's own
413
- * value (a hexagon written as an ellipse, a dashed line as Dots), the block's exact value is the truth, so a re-import
1599
+ * value (a hexagon written as an ellipse, a dotted line as Dots), the block's exact value is the truth, so a re-import
414
1600
  * gives back the same rules; a value another program changed since stays the truth.
415
1601
  * @param snapshot - the graph
416
1602
  * @param text - the block's text
417
1603
  * @param read - the values read, changed in place
418
1604
  * @param read.node - the node values
419
1605
  * @param read.edge - the edge values
1606
+ * @param labels - whether the lines' labels are drawn
420
1607
  */
421
- function exactFromBlock(snapshot, text, read) {
422
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
423
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "pajek") : null;
424
- if (ex === null) {
425
- return;
426
- }
1608
+ function exactFromBlock(snapshot, text, read, labels) {
427
1609
  const notes = new StyleNotes();
428
1610
  const directed = directedEdges(snapshot);
429
- for (const target of ["node", "edge"]) {
430
- const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
431
- const get = getter((t, p) => resolveStyles(ex, t, p, snapshot), target);
432
- const values = read[target];
433
- for (let i = 0; i < n; i++) {
434
- const want = get(i);
435
- const back = writtenAndReadBack(target, want, directed[i], notes);
436
- for (const property of READ[target]) {
437
- const native = values[property]?.[i] ?? null;
438
- const w = want(property);
439
- if (w !== null && native !== w && native === (back.get(property) ?? null)) {
440
- values[property] ?? (values[property] = new Array(n).fill(null));
441
- values[property][i] = w;
442
- }
443
- }
444
- }
445
- }
1611
+ takeFromBlock(snapshot, text, "pajek", (target) => ({
1612
+ values: read[target],
1613
+ properties: READ[target],
1614
+ writes: PAJEK_STYLE_CAPABILITIES[target],
1615
+ readBack: (i, want) => {
1616
+ const back = writtenAndReadBack(target, want, directed[i], notes, labels);
1617
+ return (property) => back.get(property) ?? null;
1618
+ },
1619
+ // a label the block places by a side clears the offsets its `lphi` reads back as
1620
+ take: (property, wanted) => wanted !== null || LABEL_PLACE.has(property) || OUTLINE.has(property),
1621
+ }));
446
1622
  }
1623
+ /** The outline a shape written as `diamond` reads back as, which the block's own shape clears. */
1624
+ const OUTLINE = new Set(["shapePoints", "shapePointsStretch"]);
1625
+ /** The label place properties a side written as `lphi` reads back as. */
1626
+ const LABEL_PLACE = new Set(["labelOffsetX", "labelOffsetY", "labelShiftX"]);
447
1627
  /**
448
1628
  * An element's values written as the writer writes them and read back as the reader reads them.
449
1629
  * @param target - nodes or edges
450
1630
  * @param want - its values
451
1631
  * @param directed - for a line, whether it is an arc
452
1632
  * @param notes - a scratch for the notes the round trip makes
1633
+ * @param labels - whether the lines' labels are drawn
453
1634
  * @returns the values read back, by property
454
1635
  */
455
- function writtenAndReadBack(target, want, directed, notes) {
1636
+ function writtenAndReadBack(target, want, directed, notes, labels) {
456
1637
  const ignore = () => undefined;
457
1638
  const params = target === "node" ? vertexParams(want, ignore) : lineParams(want, directed, ignore);
458
1639
  const back = new Map();
@@ -460,11 +1641,28 @@ function writtenAndReadBack(target, want, directed, notes) {
460
1641
  if (typeof label === "string") {
461
1642
  params.set(LABEL_COLUMN, pajekLabelText(label));
462
1643
  }
463
- (target === "node" ? readVertex : readLine)((key) => params.get(key) ?? null, (property, value) => {
1644
+ // what the writer restates so that nothing is carried to this element (`restateCarried`)
1645
+ if (target === "node" && [...params.keys()].every((key) => key === LABEL_COLUMN)) {
1646
+ params.set(SHAPE_COLUMN, "ellipse");
1647
+ }
1648
+ for (const key of target === "edge" ? CARRIED.edge : []) {
1649
+ if (!params.has(key)) {
1650
+ params.set(key, lineRestated(key, directed));
1651
+ }
1652
+ }
1653
+ const get = (key) => params.get(key) ?? null;
1654
+ const put = (property, value) => {
464
1655
  if (value !== null) {
465
1656
  back.set(property, value);
466
1657
  }
467
- }, (property) => ({ notes, target, property }));
1658
+ };
1659
+ const at = (property) => ({ notes, target, property });
1660
+ if (target === "node") {
1661
+ readVertex(get, put, at);
1662
+ }
1663
+ else {
1664
+ readLine(get, put, at, directed, labels);
1665
+ }
468
1666
  return back;
469
1667
  }
470
1668
  /**
@@ -487,10 +1685,15 @@ export function pajekStyledExport(snapshot, input) {
487
1685
  const approximate = (property, value) => {
488
1686
  approximated.add(target, property, value);
489
1687
  };
490
- const get = getter((t, p) => input.picture(t, p), target);
1688
+ const get = elementValues((t, p) => input.picture(t, p), target, PAJEK_STYLE_CAPABILITIES[target]);
491
1689
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
492
1690
  const params = (i) => target === "node" ? vertexParams(get(i), approximate) : lineParams(get(i), directed[i], approximate);
493
- replaceParamColumns(table, target === "node" ? [SHAPE_COLUMN, ...NODE_KEYS] : EDGE_KEYS, paramColumns(n, params));
1691
+ const columns = paramColumns(n, params);
1692
+ if (target === "edge") {
1693
+ keepLineLabels(snapshot, columns);
1694
+ }
1695
+ restateCarried(target, columns, directed);
1696
+ replaceParamColumns(table, target === "node" ? [SHAPE_COLUMN, ...NODE_KEYS] : EDGE_KEYS, columns);
494
1697
  }
495
1698
  out.nodes.set(LABEL_COLUMN, drawnLabels(input, snapshot.nodeCount, approximated), { dtype: "string", role: "label" }, {
496
1699
  replaceRole: true,
@@ -500,6 +1703,29 @@ export function pajekStyledExport(snapshot, input) {
500
1703
  notes.push(...styleBlockLosses(block, input.rules.styles.rules.length, "size"));
501
1704
  return { snapshot: out, comment: PAJEK_STYLE_COMMENT + block, notes };
502
1705
  }
1706
+ /**
1707
+ * Keeps each line's own label `l` where graphty draws none: Pajek draws no line label by default (`drawsLineLabels`),
1708
+ * so a label the file holds is not drawn, and writing it back keeps it for a Draw window that marks lines with labels.
1709
+ * @param snapshot - the graph handed to the exporter
1710
+ * @param columns - the written edge columns, by key, changed in place
1711
+ */
1712
+ function keepLineLabels(snapshot, columns) {
1713
+ if (!snapshot.edges.has("l")) {
1714
+ return;
1715
+ }
1716
+ const own = (e) => plainCell(snapshot.edges.value("l", e));
1717
+ let col = columns.get("l");
1718
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1719
+ const v = own(e);
1720
+ if (typeof v === "string" && v !== "" && (col?.[e] ?? null) === null) {
1721
+ col ?? (col = new Array(snapshot.edgeCount).fill(null));
1722
+ col[e] = pajekLabelText(v);
1723
+ }
1724
+ }
1725
+ if (col !== undefined) {
1726
+ columns.set("l", col);
1727
+ }
1728
+ }
503
1729
  /**
504
1730
  * Each vertex's label as graphty draws it; a vertex graphty draws without one gets an empty label, which Pajek draws
505
1731
  * as nothing (with no label, Pajek writes and draws the vertex's id).
@@ -548,6 +1774,61 @@ function replaceParamColumns(table, keys, columns) {
548
1774
  table.set(key, col, { dtype: numeric ? "f64" : "string" });
549
1775
  }
550
1776
  }
1777
+ /**
1778
+ * Keeps Pajek from drawing an element with the parameters of the one before it (`CARRIED`): a line writes Pajek's own
1779
+ * default for each parameter another line writes and it leaves out, and a vertex that writes no parameter while
1780
+ * another does writes its shape, `ellipse`, which starts it again from Pajek's defaults.
1781
+ * @param target - nodes or edges
1782
+ * @param columns - the written columns, by key, changed in place
1783
+ * @param directed - for each line, whether it is an arc
1784
+ */
1785
+ function restateCarried(target, columns, directed) {
1786
+ if (target === "node") {
1787
+ const all = [...columns.values()];
1788
+ if (all.length === 0) {
1789
+ return;
1790
+ }
1791
+ const n = all[0].length;
1792
+ const shape = columns.get(SHAPE_COLUMN) ?? new Array(n).fill(null);
1793
+ for (let i = 0; i < n; i++) {
1794
+ if (all.every((col) => col[i] === null)) {
1795
+ shape[i] = "ellipse";
1796
+ }
1797
+ }
1798
+ columns.set(SHAPE_COLUMN, shape);
1799
+ return;
1800
+ }
1801
+ for (const key of CARRIED.edge) {
1802
+ const col = columns.get(key);
1803
+ if (col === undefined) {
1804
+ continue;
1805
+ }
1806
+ for (let i = 0; i < col.length; i++) {
1807
+ col[i] ?? (col[i] = lineRestated(key, directed[i]));
1808
+ }
1809
+ }
1810
+ }
1811
+ /**
1812
+ * What a line writes for a carried parameter it leaves at Pajek's default.
1813
+ * @param key - the parameter
1814
+ * @param directed - whether the line is an arc
1815
+ * @returns the value written
1816
+ */
1817
+ function lineRestated(key, directed) {
1818
+ const defaults = lineDefaults(directed);
1819
+ switch (key) {
1820
+ case "c":
1821
+ return pajekColorText(String(defaults.color));
1822
+ case "w":
1823
+ return directed ? Number(defaults.width) : Number(defaults.width) / EDGE_WIDTH_PER_W;
1824
+ case "p":
1825
+ return "Solid";
1826
+ case "s":
1827
+ return 1;
1828
+ default:
1829
+ return 0;
1830
+ }
1831
+ }
551
1832
  /**
552
1833
  * Every element's parameters, as columns.
553
1834
  * @param n - the element count