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