@graphty/graph-io 0.3.48 → 0.3.49

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