@graphty/graph-io 0.3.48 → 0.3.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-BRXsll0T.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-BRXsll0T.js.map} +1 -1
  3. package/dist/chunks/bends-BGXjv_U4.js +129 -0
  4. package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
  5. package/dist/chunks/{block-Cu22ewuS.js → block-yl0f_qLO.js} +931 -164
  6. package/dist/chunks/block-yl0f_qLO.js.map +1 -0
  7. package/dist/chunks/choose-CczZJaX_.js +717 -0
  8. package/dist/chunks/choose-CczZJaX_.js.map +1 -0
  9. package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/{context-cgmmOxZ2.js → context-DizmppHy.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-DizmppHy.js.map} +1 -1
  15. package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
  16. package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
  17. package/dist/chunks/{credentials-BiQsnMTb.js → credentials-CmvJXYeH.js} +67 -16
  18. package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
  19. package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
  20. package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-Dhez_Cew.js} +11 -2
  24. package/dist/chunks/descriptor-Dhez_Cew.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-7Jkpg0yd.js} +1788 -133
  26. package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
  27. package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
  28. package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
  29. package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
  30. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  31. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-1Ajud15J.js} +9 -8
  32. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-1Ajud15J.js.map} +1 -1
  33. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-B6yvL3wy.js} +7 -7
  34. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-B6yvL3wy.js.map} +1 -1
  35. package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  36. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-CA5q8ROP.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-CA5q8ROP.js.map} +1 -1
  39. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-CQQZFJ2v.js} +1284 -156
  40. package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
  41. package/dist/chunks/format-BXiAb-1h.js +408 -0
  42. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  43. package/dist/chunks/{generic-CP6YD34u.js → generic-Dc21MAdg.js} +387 -31
  44. package/dist/chunks/generic-Dc21MAdg.js.map +1 -0
  45. package/dist/chunks/{importer-B87_fmVx.js → importer-C6NVpCXp.js} +327 -84
  46. package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
  47. package/dist/chunks/{importer-DPZDN40-.js → importer-CKFe4q54.js} +25 -17
  48. package/dist/chunks/importer-CKFe4q54.js.map +1 -0
  49. package/dist/chunks/{importer-Bji2MeWr.js → importer-CKbUbsJA.js} +13 -10
  50. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-CKbUbsJA.js.map} +1 -1
  51. package/dist/chunks/{importer-CB_zV5_L.js → importer-ClCsdgTJ.js} +208 -1104
  52. package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
  53. package/dist/chunks/{importer-e3huMZbm.js → importer-CyVZTrf0.js} +14 -10
  54. package/dist/chunks/importer-CyVZTrf0.js.map +1 -0
  55. package/dist/chunks/{importer-CVT-JyDE.js → importer-DgG4v1Ms.js} +1537 -355
  56. package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
  57. package/dist/chunks/{importer-Cyy6UseH.js → importer-asFUrIsj.js} +66 -13
  58. package/dist/chunks/importer-asFUrIsj.js.map +1 -0
  59. package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
  60. package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
  61. package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
  62. package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  63. package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-IVRq2Uoo.js} +282 -74
  64. package/dist/chunks/lexicon-IVRq2Uoo.js.map +1 -0
  65. package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
  66. package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  67. package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
  68. package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
  69. package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
  70. package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
  71. package/dist/chunks/round-trip-Cy3_SHRZ.js +79 -0
  72. package/dist/chunks/round-trip-Cy3_SHRZ.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-BXwVbza3.js} +5 -5
  74. package/dist/chunks/sigv4-BXwVbza3.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CAVmBLgp.js} +1237 -138
  76. package/dist/chunks/styles-CAVmBLgp.js.map +1 -0
  77. package/dist/chunks/styles-CPczw1Jz.js +1928 -0
  78. package/dist/chunks/styles-CPczw1Jz.js.map +1 -0
  79. package/dist/chunks/styles-boK6Ln54.js +2847 -0
  80. package/dist/chunks/styles-boK6Ln54.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-XeFBQ6VS.js} +506 -175
  82. package/dist/chunks/styles-yed-XeFBQ6VS.js.map +1 -0
  83. package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
  84. package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
  85. package/dist/csv.js +7 -7
  86. package/dist/cx.js +2 -2
  87. package/dist/cx2.js +2 -2
  88. package/dist/cys.js +1 -1
  89. package/dist/dot.js +8 -4
  90. package/dist/dot.js.map +1 -1
  91. package/dist/gexf.js +42 -12
  92. package/dist/gexf.js.map +1 -1
  93. package/dist/gml.js +848 -194
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +230 -193
  96. package/dist/graph-io.js.map +1 -1
  97. package/dist/graphml.js +3 -3
  98. package/dist/json.js +3 -2
  99. package/dist/neo4j.js +6 -6
  100. package/dist/obo.js +1 -1
  101. package/dist/pajek.js +1 -1
  102. package/dist/positions.js +45 -3
  103. package/dist/positions.js.map +1 -1
  104. package/dist/sources-cypher.js +141 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +32 -6
  108. package/dist/sources-rest.js.map +1 -1
  109. package/dist/sources-sparql.d.ts +1 -0
  110. package/dist/sources-sparql.js +439 -0
  111. package/dist/sources-sparql.js.map +1 -0
  112. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  113. package/dist/src/formats/cx/importer.js +5 -1
  114. package/dist/src/formats/cx/importer.js.map +1 -1
  115. package/dist/src/formats/cx/styles.d.ts +24 -1
  116. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  117. package/dist/src/formats/cx/styles.js +117 -11
  118. package/dist/src/formats/cx/styles.js.map +1 -1
  119. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  120. package/dist/src/formats/cx2/importer.js +1 -0
  121. package/dist/src/formats/cx2/importer.js.map +1 -1
  122. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  123. package/dist/src/formats/cx2/styles.js +3 -1
  124. package/dist/src/formats/cx2/styles.js.map +1 -1
  125. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cys/importer.js +1 -0
  127. package/dist/src/formats/cys/importer.js.map +1 -1
  128. package/dist/src/formats/cys/styles.d.ts +2 -0
  129. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cys/styles.js +15 -7
  131. package/dist/src/formats/cys/styles.js.map +1 -1
  132. package/dist/src/formats/dot/drawing.d.ts +98 -0
  133. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  134. package/dist/src/formats/dot/drawing.js +734 -0
  135. package/dist/src/formats/dot/drawing.js.map +1 -0
  136. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  137. package/dist/src/formats/dot/exporter.js +34 -2
  138. package/dist/src/formats/dot/exporter.js.map +1 -1
  139. package/dist/src/formats/dot/importer.d.ts +6 -1
  140. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  141. package/dist/src/formats/dot/importer.js +170 -33
  142. package/dist/src/formats/dot/importer.js.map +1 -1
  143. package/dist/src/formats/dot/index.d.ts +3 -1
  144. package/dist/src/formats/dot/index.d.ts.map +1 -1
  145. package/dist/src/formats/dot/index.js +2 -1
  146. package/dist/src/formats/dot/index.js.map +1 -1
  147. package/dist/src/formats/dot/label-places.d.ts +41 -0
  148. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  149. package/dist/src/formats/dot/label-places.js +185 -0
  150. package/dist/src/formats/dot/label-places.js.map +1 -0
  151. package/dist/src/formats/dot/names.d.ts +13 -0
  152. package/dist/src/formats/dot/names.d.ts.map +1 -1
  153. package/dist/src/formats/dot/names.js +13 -0
  154. package/dist/src/formats/dot/names.js.map +1 -1
  155. package/dist/src/formats/dot/record.d.ts +50 -0
  156. package/dist/src/formats/dot/record.d.ts.map +1 -0
  157. package/dist/src/formats/dot/record.js +275 -0
  158. package/dist/src/formats/dot/record.js.map +1 -0
  159. package/dist/src/formats/dot/styles.d.ts +72 -2
  160. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  161. package/dist/src/formats/dot/styles.js +1500 -119
  162. package/dist/src/formats/dot/styles.js.map +1 -1
  163. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  165. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  167. package/dist/src/formats/gexf/importer.d.ts +7 -0
  168. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  169. package/dist/src/formats/gexf/importer.js +41 -5
  170. package/dist/src/formats/gexf/importer.js.map +1 -1
  171. package/dist/src/formats/gexf/preview.d.ts +77 -0
  172. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  173. package/dist/src/formats/gexf/preview.js +191 -0
  174. package/dist/src/formats/gexf/preview.js.map +1 -0
  175. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  176. package/dist/src/formats/gexf/schema.js +2 -1
  177. package/dist/src/formats/gexf/schema.js.map +1 -1
  178. package/dist/src/formats/gexf/styles.d.ts +58 -31
  179. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  180. package/dist/src/formats/gexf/styles.js +848 -225
  181. package/dist/src/formats/gexf/styles.js.map +1 -1
  182. package/dist/src/formats/gml/importer.d.ts +5 -2
  183. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  184. package/dist/src/formats/gml/importer.js +258 -71
  185. package/dist/src/formats/gml/importer.js.map +1 -1
  186. package/dist/src/formats/gml/index.d.ts +1 -1
  187. package/dist/src/formats/gml/index.d.ts.map +1 -1
  188. package/dist/src/formats/gml/index.js +1 -1
  189. package/dist/src/formats/gml/index.js.map +1 -1
  190. package/dist/src/formats/gml/positions.d.ts +7 -0
  191. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  192. package/dist/src/formats/gml/positions.js +29 -7
  193. package/dist/src/formats/gml/positions.js.map +1 -1
  194. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  195. package/dist/src/formats/gml/styles.js +754 -102
  196. package/dist/src/formats/gml/styles.js.map +1 -1
  197. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.js +6 -10
  199. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  200. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  201. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  202. package/dist/src/formats/graphml/styles-yed.js +695 -164
  203. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  204. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.js +431 -64
  207. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  208. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  209. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  210. package/dist/src/formats/graphml/yfiles.js +16 -6
  211. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  212. package/dist/src/formats/json/dialect.d.ts +3 -2
  213. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  214. package/dist/src/formats/json/dialect.js +39 -5
  215. package/dist/src/formats/json/dialect.js.map +1 -1
  216. package/dist/src/formats/json/importer.d.ts +13 -2
  217. package/dist/src/formats/json/importer.d.ts.map +1 -1
  218. package/dist/src/formats/json/importer.js +42 -6
  219. package/dist/src/formats/json/importer.js.map +1 -1
  220. package/dist/src/formats/json/index.d.ts +1 -1
  221. package/dist/src/formats/json/index.d.ts.map +1 -1
  222. package/dist/src/formats/json/index.js +1 -1
  223. package/dist/src/formats/json/index.js.map +1 -1
  224. package/dist/src/formats/json/positions.d.ts +5 -2
  225. package/dist/src/formats/json/positions.d.ts.map +1 -1
  226. package/dist/src/formats/json/positions.js +20 -4
  227. package/dist/src/formats/json/positions.js.map +1 -1
  228. package/dist/src/formats/json/styles.d.ts +12 -1
  229. package/dist/src/formats/json/styles.d.ts.map +1 -1
  230. package/dist/src/formats/json/styles.js +1843 -136
  231. package/dist/src/formats/json/styles.js.map +1 -1
  232. package/dist/src/formats/pajek/draw.d.ts +104 -0
  233. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  234. package/dist/src/formats/pajek/draw.js +382 -0
  235. package/dist/src/formats/pajek/draw.js.map +1 -0
  236. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  237. package/dist/src/formats/pajek/exporter.js +70 -51
  238. package/dist/src/formats/pajek/exporter.js.map +1 -1
  239. package/dist/src/formats/pajek/importer.d.ts +9 -0
  240. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  241. package/dist/src/formats/pajek/importer.js +233 -102
  242. package/dist/src/formats/pajek/importer.js.map +1 -1
  243. package/dist/src/formats/pajek/index.d.ts +1 -0
  244. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  245. package/dist/src/formats/pajek/index.js.map +1 -1
  246. package/dist/src/formats/pajek/styles.d.ts +15 -7
  247. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  248. package/dist/src/formats/pajek/styles.js +1395 -114
  249. package/dist/src/formats/pajek/styles.js.map +1 -1
  250. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  251. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  252. package/dist/src/formats/pajek/syntax.js +26 -2
  253. package/dist/src/formats/pajek/syntax.js.map +1 -1
  254. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  255. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  256. package/dist/src/formats/xgmml/emit.js +65 -5
  257. package/dist/src/formats/xgmml/emit.js.map +1 -1
  258. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  259. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  260. package/dist/src/formats/xgmml/styles.js +277 -60
  261. package/dist/src/formats/xgmml/styles.js.map +1 -1
  262. package/dist/src/index.d.ts +6 -5
  263. package/dist/src/index.d.ts.map +1 -1
  264. package/dist/src/index.js +6 -5
  265. package/dist/src/index.js.map +1 -1
  266. package/dist/src/positions/convert.d.ts.map +1 -1
  267. package/dist/src/positions/convert.js +11 -1
  268. package/dist/src/positions/convert.js.map +1 -1
  269. package/dist/src/positions/extract.d.ts.map +1 -1
  270. package/dist/src/positions/extract.js +53 -1
  271. package/dist/src/positions/extract.js.map +1 -1
  272. package/dist/src/positions/index.d.ts +1 -1
  273. package/dist/src/positions/index.d.ts.map +1 -1
  274. package/dist/src/positions/types.d.ts +59 -0
  275. package/dist/src/positions/types.d.ts.map +1 -1
  276. package/dist/src/sources/builtins.d.ts +1 -1
  277. package/dist/src/sources/builtins.d.ts.map +1 -1
  278. package/dist/src/sources/builtins.js +8 -2
  279. package/dist/src/sources/builtins.js.map +1 -1
  280. package/dist/src/sources/context.d.ts.map +1 -1
  281. package/dist/src/sources/context.js +1 -0
  282. package/dist/src/sources/context.js.map +1 -1
  283. package/dist/src/sources/credentials.d.ts +50 -1
  284. package/dist/src/sources/credentials.d.ts.map +1 -1
  285. package/dist/src/sources/credentials.js +69 -0
  286. package/dist/src/sources/credentials.js.map +1 -1
  287. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  288. package/dist/src/sources/cypher/descriptor.js +5 -0
  289. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  290. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  291. package/dist/src/sources/cypher/index.js +125 -18
  292. package/dist/src/sources/cypher/index.js.map +1 -1
  293. package/dist/src/sources/cypher/params.d.ts +1 -1
  294. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  295. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  296. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  297. package/dist/src/sources/cypher/query-builder.js +31 -0
  298. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  299. package/dist/src/sources/ids.d.ts +5 -0
  300. package/dist/src/sources/ids.d.ts.map +1 -1
  301. package/dist/src/sources/ids.js +5 -0
  302. package/dist/src/sources/ids.js.map +1 -1
  303. package/dist/src/sources/params.d.ts +3 -0
  304. package/dist/src/sources/params.d.ts.map +1 -1
  305. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  306. package/dist/src/sources/rest/descriptor.js +9 -0
  307. package/dist/src/sources/rest/descriptor.js.map +1 -1
  308. package/dist/src/sources/rest/openalex.d.ts +4 -2
  309. package/dist/src/sources/rest/openalex.d.ts.map +1 -1
  310. package/dist/src/sources/rest/openalex.js +4 -2
  311. package/dist/src/sources/rest/openalex.js.map +1 -1
  312. package/dist/src/sources/rest/source.d.ts.map +1 -1
  313. package/dist/src/sources/rest/source.js +38 -0
  314. package/dist/src/sources/rest/source.js.map +1 -1
  315. package/dist/src/sources/run.d.ts +2 -0
  316. package/dist/src/sources/run.d.ts.map +1 -1
  317. package/dist/src/sources/run.js +30 -3
  318. package/dist/src/sources/run.js.map +1 -1
  319. package/dist/src/sources/sigv4.d.ts +5 -3
  320. package/dist/src/sources/sigv4.d.ts.map +1 -1
  321. package/dist/src/sources/sigv4.js +6 -4
  322. package/dist/src/sources/sigv4.js.map +1 -1
  323. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  324. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  325. package/dist/src/sources/sparql/descriptor.js +64 -0
  326. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  327. package/dist/src/sources/sparql/index.d.ts +28 -0
  328. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  329. package/dist/src/sources/sparql/index.js +327 -0
  330. package/dist/src/sources/sparql/index.js.map +1 -0
  331. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  332. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  333. package/dist/src/sources/sparql/ntriples.js +180 -0
  334. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  335. package/dist/src/sources/sparql/params.d.ts +27 -0
  336. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  337. package/dist/src/sources/sparql/params.js +5 -0
  338. package/dist/src/sources/sparql/params.js.map +1 -0
  339. package/dist/src/sources/sparql/query.d.ts +73 -0
  340. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  341. package/dist/src/sources/sparql/query.js +164 -0
  342. package/dist/src/sources/sparql/query.js.map +1 -0
  343. package/dist/src/sources/types.d.ts +60 -0
  344. package/dist/src/sources/types.d.ts.map +1 -1
  345. package/dist/src/styles/annotations.d.ts +61 -5
  346. package/dist/src/styles/annotations.d.ts.map +1 -1
  347. package/dist/src/styles/annotations.js +130 -22
  348. package/dist/src/styles/annotations.js.map +1 -1
  349. package/dist/src/styles/canon/bends.d.ts +46 -0
  350. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  351. package/dist/src/styles/canon/bends.js +123 -0
  352. package/dist/src/styles/canon/bends.js.map +1 -1
  353. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  354. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  355. package/dist/src/styles/canon/color-tables.js +10 -4
  356. package/dist/src/styles/canon/color-tables.js.map +1 -1
  357. package/dist/src/styles/canon/color.d.ts +3 -1
  358. package/dist/src/styles/canon/color.d.ts.map +1 -1
  359. package/dist/src/styles/canon/color.js +103 -3
  360. package/dist/src/styles/canon/color.js.map +1 -1
  361. package/dist/src/styles/canon/enums.d.ts +2 -2
  362. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  363. package/dist/src/styles/canon/enums.js +89 -7
  364. package/dist/src/styles/canon/enums.js.map +1 -1
  365. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  366. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  367. package/dist/src/styles/canon/graphviz.js +57 -9
  368. package/dist/src/styles/canon/graphviz.js.map +1 -1
  369. package/dist/src/styles/canon/number.d.ts +8 -0
  370. package/dist/src/styles/canon/number.d.ts.map +1 -1
  371. package/dist/src/styles/canon/number.js +18 -0
  372. package/dist/src/styles/canon/number.js.map +1 -1
  373. package/dist/src/styles/canon/text.d.ts +29 -2
  374. package/dist/src/styles/canon/text.d.ts.map +1 -1
  375. package/dist/src/styles/canon/text.js +156 -16
  376. package/dist/src/styles/canon/text.js.map +1 -1
  377. package/dist/src/styles/codes.d.ts +2 -0
  378. package/dist/src/styles/codes.d.ts.map +1 -1
  379. package/dist/src/styles/codes.js +2 -0
  380. package/dist/src/styles/codes.js.map +1 -1
  381. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  382. package/dist/src/styles/cyjs/cascade.js +1139 -79
  383. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  384. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  385. package/dist/src/styles/cyjs/defaults-table.js +4 -0
  386. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  387. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  388. package/dist/src/styles/cyjs/emit.js +232 -10
  389. package/dist/src/styles/cyjs/emit.js.map +1 -1
  390. package/dist/src/styles/cyjs/face.d.ts +20 -0
  391. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  392. package/dist/src/styles/cyjs/face.js +332 -0
  393. package/dist/src/styles/cyjs/face.js.map +1 -0
  394. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  395. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  396. package/dist/src/styles/cyjs/mappers.js +475 -26
  397. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  398. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  399. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  400. package/dist/src/styles/cyjs/routes.js +360 -0
  401. package/dist/src/styles/cyjs/routes.js.map +1 -0
  402. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  403. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  404. package/dist/src/styles/cytoscape/charts.js +560 -0
  405. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  406. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  407. package/dist/src/styles/cytoscape/emit.js +156 -48
  408. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  409. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  410. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  411. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  412. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  413. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  414. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  415. package/dist/src/styles/cytoscape/mappings.js +92 -0
  416. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  417. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  418. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  419. package/dist/src/styles/cytoscape/resolve.js +623 -17
  420. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  421. package/dist/src/styles/defaults.d.ts +46 -26
  422. package/dist/src/styles/defaults.d.ts.map +1 -1
  423. package/dist/src/styles/defaults.js +308 -71
  424. package/dist/src/styles/defaults.js.map +1 -1
  425. package/dist/src/styles/index.d.ts +4 -3
  426. package/dist/src/styles/index.d.ts.map +1 -1
  427. package/dist/src/styles/index.js +3 -2
  428. package/dist/src/styles/index.js.map +1 -1
  429. package/dist/src/styles/internal.d.ts +1 -0
  430. package/dist/src/styles/internal.d.ts.map +1 -1
  431. package/dist/src/styles/internal.js +1 -0
  432. package/dist/src/styles/internal.js.map +1 -1
  433. package/dist/src/styles/io/generic.d.ts.map +1 -1
  434. package/dist/src/styles/io/generic.js +56 -16
  435. package/dist/src/styles/io/generic.js.map +1 -1
  436. package/dist/src/styles/io/round-trip.d.ts +56 -0
  437. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  438. package/dist/src/styles/io/round-trip.js +138 -0
  439. package/dist/src/styles/io/round-trip.js.map +1 -0
  440. package/dist/src/styles/io/writer.d.ts.map +1 -1
  441. package/dist/src/styles/io/writer.js +6 -1
  442. package/dist/src/styles/io/writer.js.map +1 -1
  443. package/dist/src/styles/notes.d.ts.map +1 -1
  444. package/dist/src/styles/notes.js +1 -0
  445. package/dist/src/styles/notes.js.map +1 -1
  446. package/dist/src/styles/plan/features.d.ts +1 -1
  447. package/dist/src/styles/plan/features.d.ts.map +1 -1
  448. package/dist/src/styles/plan/features.js +22 -2
  449. package/dist/src/styles/plan/features.js.map +1 -1
  450. package/dist/src/styles/plan/index.d.ts +1 -1
  451. package/dist/src/styles/plan/index.d.ts.map +1 -1
  452. package/dist/src/styles/plan/index.js +1 -1
  453. package/dist/src/styles/plan/index.js.map +1 -1
  454. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  455. package/dist/src/styles/plan/plan.js +61 -4
  456. package/dist/src/styles/plan/plan.js.map +1 -1
  457. package/dist/src/styles/plan/resolve.d.ts +11 -0
  458. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  459. package/dist/src/styles/plan/resolve.js +40 -4
  460. package/dist/src/styles/plan/resolve.js.map +1 -1
  461. package/dist/src/styles/slots.d.ts +14 -0
  462. package/dist/src/styles/slots.d.ts.map +1 -0
  463. package/dist/src/styles/slots.js +20 -0
  464. package/dist/src/styles/slots.js.map +1 -0
  465. package/dist/src/styles/types.d.ts +687 -6
  466. package/dist/src/styles/types.d.ts.map +1 -1
  467. package/dist/src/styles/vocabulary.d.ts +3 -2
  468. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  469. package/dist/src/styles/vocabulary.js +224 -0
  470. package/dist/src/styles/vocabulary.js.map +1 -1
  471. package/dist/styles.js +30 -239
  472. package/dist/styles.js.map +1 -1
  473. package/dist/xgmml.js +3 -3
  474. package/package.json +27 -7
  475. package/src/formats/cx/importer.ts +6 -1
  476. package/src/formats/cx/styles.ts +143 -9
  477. package/src/formats/cx2/importer.ts +1 -0
  478. package/src/formats/cx2/styles.ts +3 -1
  479. package/src/formats/cys/importer.ts +1 -0
  480. package/src/formats/cys/styles.ts +27 -5
  481. package/src/formats/dot/drawing.ts +799 -0
  482. package/src/formats/dot/exporter.ts +37 -1
  483. package/src/formats/dot/importer.ts +192 -36
  484. package/src/formats/dot/index.ts +3 -1
  485. package/src/formats/dot/label-places.ts +241 -0
  486. package/src/formats/dot/names.ts +15 -0
  487. package/src/formats/dot/record.ts +336 -0
  488. package/src/formats/dot/styles.ts +1702 -149
  489. package/src/formats/gexf/dialog-metrics.ts +61 -0
  490. package/src/formats/gexf/importer.ts +50 -5
  491. package/src/formats/gexf/preview.ts +217 -0
  492. package/src/formats/gexf/schema.ts +2 -1
  493. package/src/formats/gexf/styles.ts +1099 -245
  494. package/src/formats/gml/importer.ts +291 -76
  495. package/src/formats/gml/index.ts +1 -1
  496. package/src/formats/gml/positions.ts +29 -7
  497. package/src/formats/gml/styles.ts +852 -110
  498. package/src/formats/graphml/styles-plain.ts +6 -15
  499. package/src/formats/graphml/styles-yed.ts +765 -176
  500. package/src/formats/graphml/styles-yfiles3.ts +487 -63
  501. package/src/formats/graphml/yfiles.ts +19 -6
  502. package/src/formats/json/dialect.ts +41 -5
  503. package/src/formats/json/importer.ts +59 -7
  504. package/src/formats/json/index.ts +1 -0
  505. package/src/formats/json/positions.ts +22 -5
  506. package/src/formats/json/styles.ts +2123 -171
  507. package/src/formats/pajek/draw.ts +469 -0
  508. package/src/formats/pajek/exporter.ts +81 -49
  509. package/src/formats/pajek/importer.ts +278 -103
  510. package/src/formats/pajek/index.ts +1 -0
  511. package/src/formats/pajek/styles.ts +1506 -133
  512. package/src/formats/pajek/syntax.ts +27 -2
  513. package/src/formats/xgmml/emit.ts +73 -5
  514. package/src/formats/xgmml/styles.ts +330 -71
  515. package/src/index.ts +12 -1
  516. package/src/positions/convert.ts +12 -1
  517. package/src/positions/extract.ts +55 -1
  518. package/src/positions/index.ts +1 -0
  519. package/src/positions/types.ts +60 -0
  520. package/src/sources/builtins.ts +8 -1
  521. package/src/sources/context.ts +1 -0
  522. package/src/sources/credentials.ts +90 -1
  523. package/src/sources/cypher/descriptor.ts +7 -1
  524. package/src/sources/cypher/index.ts +149 -18
  525. package/src/sources/cypher/params.ts +1 -1
  526. package/src/sources/cypher/query-builder.ts +33 -0
  527. package/src/sources/ids.ts +6 -0
  528. package/src/sources/params.ts +3 -0
  529. package/src/sources/rest/descriptor.ts +9 -0
  530. package/src/sources/rest/openalex.ts +4 -2
  531. package/src/sources/rest/source.ts +41 -0
  532. package/src/sources/run.ts +35 -3
  533. package/src/sources/sigv4.ts +6 -4
  534. package/src/sources/sparql/descriptor.ts +67 -0
  535. package/src/sources/sparql/index.ts +371 -0
  536. package/src/sources/sparql/ntriples.ts +219 -0
  537. package/src/sources/sparql/params.ts +28 -0
  538. package/src/sources/sparql/query.ts +216 -0
  539. package/src/sources/types.ts +68 -0
  540. package/src/styles/annotations.ts +201 -27
  541. package/src/styles/canon/bends.ts +137 -0
  542. package/src/styles/canon/color-tables.ts +10 -4
  543. package/src/styles/canon/color.ts +105 -3
  544. package/src/styles/canon/enums.ts +107 -8
  545. package/src/styles/canon/graphviz.ts +71 -10
  546. package/src/styles/canon/number.ts +19 -0
  547. package/src/styles/canon/text.ts +187 -16
  548. package/src/styles/codes.ts +2 -0
  549. package/src/styles/cyjs/cascade.ts +1303 -84
  550. package/src/styles/cyjs/defaults-table.ts +4 -0
  551. package/src/styles/cyjs/emit.ts +268 -14
  552. package/src/styles/cyjs/face.ts +372 -0
  553. package/src/styles/cyjs/mappers.ts +506 -27
  554. package/src/styles/cyjs/routes.ts +432 -0
  555. package/src/styles/cytoscape/charts.ts +684 -0
  556. package/src/styles/cytoscape/emit.ts +170 -48
  557. package/src/styles/cytoscape/lexicon.ts +431 -78
  558. package/src/styles/cytoscape/mappings.ts +108 -0
  559. package/src/styles/cytoscape/resolve.ts +741 -17
  560. package/src/styles/defaults.ts +417 -108
  561. package/src/styles/index.ts +5 -0
  562. package/src/styles/internal.ts +1 -0
  563. package/src/styles/io/generic.ts +79 -23
  564. package/src/styles/io/round-trip.ts +202 -0
  565. package/src/styles/io/writer.ts +6 -1
  566. package/src/styles/notes.ts +1 -0
  567. package/src/styles/plan/features.ts +23 -2
  568. package/src/styles/plan/index.ts +1 -1
  569. package/src/styles/plan/plan.ts +69 -5
  570. package/src/styles/plan/resolve.ts +47 -4
  571. package/src/styles/slots.ts +25 -0
  572. package/src/styles/types.ts +698 -5
  573. package/src/styles/vocabulary.ts +232 -2
  574. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  575. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  576. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  577. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  578. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  579. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  580. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  581. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  582. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  583. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  584. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  585. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  586. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  587. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  588. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  589. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  590. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  591. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  592. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  593. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  594. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  595. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  596. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  597. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  598. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  599. package/dist/chunks/writer-DvHTl36J.js +0 -449
  600. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -5,7 +5,7 @@
5
5
  * - Cytoscape.js: the stylesheet (an array or a CSS-like text) over the raw element JSON, through graph-io's
6
6
  * Cytoscape.js engine, with Cytoscape.js's defaults beneath it; an element's own `style` is a bypass. Cytoscape
7
7
  * desktop's export without a stylesheet gets Cytoscape desktop's default style instead.
8
- * - graphology: the keys sigma.js draws (`color`, `size`, `label`, `hidden`), with sigma's defaults. Gephi's JSON
8
+ * - graphology: the keys sigma.js draws (`color`, `size` (a radius), `label`), with sigma's defaults. Gephi's JSON
9
9
  * export for sigma.js is read the same way.
10
10
  * - vis.js: the keys vis-network draws, `options.nodes` and `options.edges` as declared defaults and
11
11
  * `options.groups` as group rules, with vis-network's defaults, its palette for groups the file does not define,
@@ -33,11 +33,14 @@ import { cyjsHoldsExactly, CYTOSCAPE_IMPORT_KEY, toCytoscapeStylesheet, } from "
33
33
  import { CYJS_EDGE_PROPERTIES, CYJS_NODE_PROPERTIES } from "../../styles/cyjs/mappers.js";
34
34
  import { toolDefaults } from "../../styles/defaults.js";
35
35
  import { StyleApproximations } from "../../styles/io/approximations.js";
36
- import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
36
+ import { parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
37
+ import { takeFromBlock } from "../../styles/io/round-trip.js";
37
38
  import { StyleNotes } from "../../styles/notes.js";
38
39
  import { styleFingerprint } from "../../styles/plan/features.js";
39
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
40
+ import { plainCell } from "../../styles/plan/resolve.js";
40
41
  import { isJsonObject } from "./dialect.js";
42
+ /** sigma.js's own settings. */
43
+ export const SIGMA_DEFAULT_SETTINGS = { renderEdgeLabels: false, edgeLabelAnchors: "nodeLabels" };
41
44
  const TARGETS = ["node", "edge"];
42
45
  // ============================================================ reading
43
46
  /**
@@ -236,14 +239,17 @@ function graphologyStyles(snapshot, src, context) {
236
239
  const notes = new StyleNotes();
237
240
  const out = { node: {}, edge: {} };
238
241
  const readFrom = { node: [], edge: [] };
242
+ const edgeLabel = sigmaEdgeLabels(snapshot, src.sigma ?? SIGMA_DEFAULT_SETTINGS);
239
243
  for (const target of TARGETS) {
240
244
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
241
245
  const { values, put } = valuesOf(n);
242
246
  const keys = {
243
247
  color: cells(snapshot, target, "color"),
244
248
  size: cells(snapshot, target, "size"),
249
+ // sigma 4's default styles bind nothing else: it draws an element marked `hidden: true` (sigma 3's key)
250
+ // or `visibility: "hidden"` (its own display-data name) like any other (specimen sheets
251
+ // graphology-specimen-node-sizes-opacity, -edges-lines)
245
252
  label: cells(snapshot, target, "label"),
246
- hidden: cells(snapshot, target, "hidden"),
247
253
  };
248
254
  readFrom[target].push(...Object.entries(keys).flatMap(([name, c]) => (c === null ? [] : [name])));
249
255
  for (let i = 0; i < n; i++) {
@@ -254,7 +260,7 @@ function graphologyStyles(snapshot, src, context) {
254
260
  element: i,
255
261
  budget: context.budget,
256
262
  });
257
- readSigmaRow(keys, target, i, put, site);
263
+ readSigmaRow(keys, target, target === "node" || edgeLabel(i), i, put, site);
258
264
  }
259
265
  out[target] = values;
260
266
  }
@@ -271,36 +277,82 @@ function graphologyStyles(snapshot, src, context) {
271
277
  readFrom,
272
278
  };
273
279
  }
280
+ /**
281
+ * Which edges sigma.js draws the label of: none unless `renderEdgeLabels` is on; then, under `edgeLabelAnchors`
282
+ * "nodeLabels", only an edge whose two end nodes show a label.
283
+ * @param snapshot - the graph
284
+ * @param settings - sigma's settings
285
+ * @returns whether edge `i`'s label is drawn
286
+ */
287
+ function sigmaEdgeLabels(snapshot, settings) {
288
+ if (!settings.renderEdgeLabels) {
289
+ return () => false;
290
+ }
291
+ if (settings.edgeLabelAnchors === "allNodes") {
292
+ return () => true;
293
+ }
294
+ // ponytail: a node "shows" its label when it has one; sigma also hides labels by its label grid and
295
+ // labelRenderedSizeThreshold, which depend on its framing. Model those once graphty frames a sigma file as sigma does.
296
+ const label = cells(snapshot, "node", "label");
297
+ const shows = (node) => {
298
+ const text = label?.(node);
299
+ return typeof text === "string" ? text !== "" : text !== null && text !== undefined;
300
+ };
301
+ return (i) => shows(snapshot.edgeSource(i)) && shows(snapshot.edgeTarget(i));
302
+ }
274
303
  /**
275
304
  * Reads one element's sigma.js keys.
276
305
  * @param keys - the cell readers
277
306
  * @param target - nodes or edges
307
+ * @param labeled - whether sigma draws the element's label
278
308
  * @param i - the element
279
309
  * @param put - the setter
280
310
  * @param site - where a value of a property was read
281
311
  */
282
- function readSigmaRow(keys, target, i, put, site) {
283
- const { color, size, label, hidden } = keys;
312
+ function readSigmaRow(keys, target, labeled, i, put, site) {
313
+ const { color, size, label } = keys;
284
314
  if (color !== null) {
285
315
  const [fill, opacity] = target === "node" ? ["fill", "fillOpacity"] : ["color", "opacity"];
286
316
  putColor(put, fill, opacity, i, color(i), site(fill));
287
317
  }
288
318
  if (size !== null) {
289
- const v = canonNumber(size(i), site("width"));
290
- put("width", i, v);
291
- if (target === "node") {
292
- put("height", i, v);
293
- }
319
+ putSigmaSize(put, target, i, size(i), site);
294
320
  }
295
- if (label !== null) {
296
- put("label", i, canonText(label(i), site("label")));
321
+ // an edge's label is drawn only under sigma's renderEdgeLabels setting, which a document never carries (the
322
+ // `sigmaSettings` import option); otherwise it is data (graphology-specimen-edge-labels, -p2, -p3)
323
+ if (label !== null && labeled) {
324
+ const text = canonText(label(i), site("label"));
325
+ // sigma draws a label with the canvas's fillText, which draws each ASCII whitespace as a space: one line
326
+ // (graphology-specimen-labels, label-multiline)
327
+ put("label", i, text === null ? null : text.replaceAll(/[\t\n\f\r]/g, " "));
297
328
  }
298
- if (hidden !== null && canonBoolean(hidden(i), site("visible")) === true) {
299
- put("visible", i, false);
329
+ }
330
+ /**
331
+ * One element's sigma.js size, as its width (and a node's height).
332
+ * @param put - the setter
333
+ * @param target - nodes or edges
334
+ * @param i - the element
335
+ * @param raw - the size value
336
+ * @param site - where a value of a property was read
337
+ */
338
+ function putSigmaSize(put, target, i, raw, site) {
339
+ if (target === "node") {
340
+ // sigma draws a node's size as its radius, and a node of negative size not at all
341
+ // (graphology-specimen-node-sizes-opacity, size-negative)
342
+ const v = Number(raw) < 0 ? 0 : canonNumber(raw, site("width"));
343
+ const d = v === null ? null : v * 2;
344
+ put("width", i, d);
345
+ put("height", i, d);
346
+ }
347
+ else {
348
+ // sigma's edge shader draws an edge half as wide as a node of the same size is in radius: size 6 draws
349
+ // 3 px where a node of size 6 is 5.8 px in radius (graphology-specimen-arrows edge-size-6)
350
+ const w = canonNumber(raw, site("width"));
351
+ put("width", i, w === null ? null : w / 2);
300
352
  }
301
353
  }
302
354
  /**
303
- * A CSS color into a property and its opacity.
355
+ * A color as sigma.js reads it, into a property and its opacity.
304
356
  * @param put - the setter
305
357
  * @param property - the color property
306
358
  * @param opacity - its opacity property, or null when it has none
@@ -309,10 +361,11 @@ function readSigmaRow(keys, target, i, put, site) {
309
361
  * @param site - where it was read
310
362
  */
311
363
  function putColor(put, property, opacity, i, raw, site) {
312
- if (raw === null || raw === undefined || raw === "") {
364
+ // sigma draws a missing color with its default, and any string it cannot read ("" too) black
365
+ if (raw === null || raw === undefined) {
313
366
  return;
314
367
  }
315
- const c = canonColor(raw, "css", site);
368
+ const c = canonColor(raw, "graphology", site);
316
369
  if (c === null) {
317
370
  return;
318
371
  }
@@ -354,59 +407,27 @@ function blockText(snapshot, src) {
354
407
  * nothing about the block's (the writer never wrote it), so the block's value is the truth
355
408
  */
356
409
  function exactFromBlock(snapshot, text, values, written, holdsExactly) {
357
- if (text === null) {
358
- return;
359
- }
360
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
361
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "json") : null;
362
- if (ex === null) {
363
- return;
364
- }
365
- for (const target of TARGETS) {
410
+ takeFromBlock(snapshot, text, "json", (target, block, ex) => {
366
411
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
367
- const cache = new Map();
368
- const resolved = (p) => {
369
- let r = cache.get(p);
370
- if (r === undefined) {
371
- r = resolveStyles(ex, target, p, snapshot);
372
- cache.set(p, r);
373
- }
374
- return r;
375
- };
376
412
  if (holdsExactly !== undefined) {
377
413
  const properties = new Set(ex.styles.rules
378
414
  .filter((r) => r.target === target)
379
415
  .flatMap((r) => [...Object.keys(r.set ?? {}), ...Object.keys(r.map ?? {})]));
380
416
  for (const property of properties) {
381
417
  const held = (w) => holdsExactly(target, property, w);
382
- takeUnheld(values[target], property, resolved(property), n, held);
418
+ takeUnheld(values[target], property, block(target, property), n, held);
383
419
  }
384
420
  }
385
- for (const [property, write] of Object.entries(written[target] ?? {})) {
386
- takeWritten(values[target], property, n, resolved, write, snapshot);
387
- }
388
- }
389
- }
390
- /**
391
- * Takes the block's value of a property wherever the native value is what the writer writes for the block's values.
392
- * @param column - the native values of the target, changed in place
393
- * @param property - the property
394
- * @param n - the element count
395
- * @param resolved - the block's values of a property
396
- * @param write - what the writer writes for an element, from the block's values
397
- * @param snapshot - the graph
398
- */
399
- function takeWritten(column, property, n, resolved, write, snapshot) {
400
- const want = resolved(property);
401
- for (let i = 0; i < n; i++) {
402
- const w = want[i];
403
- const native = column[property]?.[i] ?? null;
404
- // a native value the writer made from the block's other values (vis.js's one size for a width) is the
405
- // block's own value, none included
406
- if (native !== w && native !== null && native === write((p) => resolved(p)[i], i, snapshot)) {
407
- setValue(column, property, n, i, w);
408
- }
409
- }
421
+ const writes = written[target] ?? {};
422
+ return {
423
+ values: values[target],
424
+ properties: Object.keys(writes),
425
+ // a native value the writer made from the block's other values (vis.js's one size for a width) is the
426
+ // block's own value, none included
427
+ readBack: (i, want) => (property) => writes[property](want, i, snapshot),
428
+ take: (_property, _wanted, native) => native !== null,
429
+ };
430
+ });
410
431
  }
411
432
  /**
412
433
  * Takes the block's value of a property wherever the dialect does not hold it exactly.
@@ -449,6 +470,17 @@ const VIS_SIZED = new Set([
449
470
  "square",
450
471
  "icon",
451
472
  ]);
473
+ /** vis-network's node shapes sized by their label, as the `labelFit` facts of each (vis-network 9.1.9 `resize`). */
474
+ const VIS_LABEL_FIT = new Map([
475
+ ["box", { square: false, ellipse: false }],
476
+ ["text", { square: false, ellipse: false }],
477
+ ["circle", { square: true, ellipse: false }],
478
+ ["database", { square: true, ellipse: false }],
479
+ ["ellipse", { square: false, ellipse: true }],
480
+ ]);
481
+ /** vis-network's default `margin` and font size (vis-network.esm.js NodesHandler defaults). */
482
+ const VIS_MARGIN = 5;
483
+ const VIS_FONT_SIZE = 14;
452
484
  /**
453
485
  * vis-network's palette for a group the file does not define, in the order groups first appear (vis-network
454
486
  * 10.1.2, `Groups._defaultGroups`): border, background.
@@ -475,6 +507,9 @@ const VIS_GROUP_PALETTE = [
475
507
  ["#C2FABC", "#74D66A"],
476
508
  ["#EE0000", "#990000"],
477
509
  ];
510
+ /** vis-network's node colors where nothing sets them (vis-network 9.1.9 `NodesHandler` defaults `color`). */
511
+ const VIS_NODE_FILL = "#97C2FC";
512
+ const VIS_NODE_BORDER = "#2B7CE9";
478
513
  /** vis-network's edge color when an edge inherits none (vis-network 10.1.2, `color.color`). */
479
514
  const VIS_EDGE_COLOR = "#848484";
480
515
  /**
@@ -563,7 +598,14 @@ function visLayer(o, target) {
563
598
  set("hidden", o.hidden);
564
599
  set("label", o.label);
565
600
  set("title", o.title);
601
+ set("wrapWidth", isJsonObject(o.widthConstraint) ? o.widthConstraint.maximum : o.widthConstraint);
602
+ if (isJsonObject(o.scaling)) {
603
+ set("scalingLabel", o.scaling.label);
604
+ }
566
605
  visFontLayer(o.font, set);
606
+ if (target === "edge" && o.font !== undefined && o.font !== null) {
607
+ set("fontText", typeof o.font === "string");
608
+ }
567
609
  return layer;
568
610
  }
569
611
  /**
@@ -584,8 +626,143 @@ function visNodeLayer(o, set) {
584
626
  }
585
627
  set("shape", o.shape);
586
628
  set("size", o.size);
629
+ set("value", o.value);
630
+ if (isJsonObject(o.scaling)) {
631
+ set("scalingMin", o.scaling.min);
632
+ set("scalingMax", o.scaling.max);
633
+ }
587
634
  set("borderWidth", o.borderWidth);
588
635
  set("opacity", o.opacity);
636
+ set("margin", o.margin);
637
+ set("widthMin", isJsonObject(o.widthConstraint) ? o.widthConstraint.minimum : o.widthConstraint);
638
+ set("heightMin", isJsonObject(o.heightConstraint) ? o.heightConstraint.minimum : o.heightConstraint);
639
+ set("valign", isJsonObject(o.heightConstraint) ? o.heightConstraint.valign : undefined);
640
+ set("image", isJsonObject(o.image) ? o.image.unselected : o.image);
641
+ set("brokenImage", o.brokenImage);
642
+ set("imagePadding", o.imagePadding);
643
+ if (isJsonObject(o.icon)) {
644
+ set("iconCode", o.icon.code);
645
+ set("iconFace", o.icon.face);
646
+ set("iconSize", o.icon.size);
647
+ set("iconColor", o.icon.color);
648
+ set("iconWeight", o.icon.weight);
649
+ }
650
+ if (isJsonObject(o.shapeProperties)) {
651
+ set("useImageSize", o.shapeProperties.useImageSize);
652
+ set("useBorderWithImage", o.shapeProperties.useBorderWithImage);
653
+ set("interpolation", o.shapeProperties.interpolation);
654
+ set("coordinateOrigin", o.shapeProperties.coordinateOrigin);
655
+ set("borderDashes", o.shapeProperties.borderDashes);
656
+ set("borderRadius", o.shapeProperties.borderRadius);
657
+ }
658
+ }
659
+ /**
660
+ * What a vis-network `image` or `circularImage` node draws (vis-network 9.1.9 `Image`, `CircularImage`,
661
+ * `CircleImageBase._resizeImage`): its picture, the broken image in its place when it has none, with its shorter
662
+ * side across the node's `2 * size` square (or at its own pixels with `shapeProperties.useImageSize`); an `image`
663
+ * node is the picture alone, reaching past that square, or over a box of its background with
664
+ * `shapeProperties.useBorderWithImage`; a `circularImage` is cut to a circle and ringed by its border. Its
665
+ * `imagePadding` keeps the picture that far inside the node's box on each side, stretched to what is left
666
+ * (`_drawImageAtPosition`); the node's `opacity` fades the picture too; with `shapeProperties.coordinateOrigin`
667
+ * "top-left" an `image` node with no box of its own draws its picture with its top left corner at the node's position.
668
+ * @param layer - the node's merged layer
669
+ * @param shape - its vis-network shape
670
+ * @param box - the node's width and height, in the file's units
671
+ * @param box.width - its width
672
+ * @param box.height - its height
673
+ * @returns the values, none for another shape or a node with no picture
674
+ */
675
+ function visImage(layer, shape, box) {
676
+ const text = (v) => (typeof v === "string" && v !== "" ? v : null);
677
+ const [own, broken] = [text(layer.image), text(layer.brokenImage)];
678
+ const src = own ?? broken;
679
+ if ((shape !== "image" && shape !== "circularImage") || src === null) {
680
+ return {};
681
+ }
682
+ const circular = shape === "circularImage";
683
+ const row = {
684
+ image: src,
685
+ ...(own !== null && broken !== null ? { imageFallback: broken } : {}),
686
+ imageFit: layer.useImageSize === true ? "none" : "cover",
687
+ imageClip: circular ? "node" : "none",
688
+ shape: "ellipse",
689
+ };
690
+ if (!circular) {
691
+ row.shape = layer.useBorderWithImage === true ? "rectangle" : "none";
692
+ }
693
+ if (layer.interpolation === false) {
694
+ row.imageSmoothing = false;
695
+ }
696
+ const opacity = typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity < 1 ? layer.opacity : null;
697
+ if (opacity !== null) {
698
+ row.imageOpacity = opacity;
699
+ }
700
+ visImagePlace(row, layer, box);
701
+ return row;
702
+ }
703
+ /**
704
+ * Where a vis-network picture sits in its node's box: inside its `imagePadding`, and from the top left corner under
705
+ * `shapeProperties.coordinateOrigin` "top-left" (visImage).
706
+ * @param row - the picture's values so far, written in place
707
+ * @param layer - the node's merged layer
708
+ * @param box - the node's width and height, in the file's units
709
+ * @param box.width - its width
710
+ * @param box.height - its height
711
+ */
712
+ function visImagePlace(row, layer, box) {
713
+ const pad = visImagePadding(layer.imagePadding);
714
+ if (pad !== null && layer.useImageSize !== true) {
715
+ row.imageFit = "none";
716
+ row.imageWidth = Math.max(0, box.width - pad.left - pad.right);
717
+ row.imageHeight = Math.max(0, box.height - pad.top - pad.bottom);
718
+ row.imageOffsetX = (pad.left - pad.right) / 2;
719
+ row.imageOffsetY = (pad.top - pad.bottom) / 2;
720
+ }
721
+ if (layer.coordinateOrigin === "top-left" && row.shape === "none") {
722
+ row.imageOffsetX = box.width / 2 + (typeof row.imageOffsetX === "number" ? row.imageOffsetX : 0);
723
+ row.imageOffsetY = box.height / 2 + (typeof row.imageOffsetY === "number" ? row.imageOffsetY : 0);
724
+ }
725
+ }
726
+ /**
727
+ * What a vis-network `icon` node draws (vis-network 9.1.9 `Icon`): its `icon.code` in `icon.face` (FontAwesome
728
+ * unless given) at `icon.size` (50) pixels and `icon.weight`, in `icon.color` (#2B7CE9), centered on the node; no
729
+ * code draws nothing.
730
+ * @param layer - the node's merged layer
731
+ * @param site - where a bad color is noted
732
+ * @returns the values, none for an icon with no code
733
+ */
734
+ function visIcon(layer, site) {
735
+ const code = typeof layer.iconCode === "string" && layer.iconCode !== "" ? layer.iconCode : null;
736
+ if (code === null) {
737
+ return {};
738
+ }
739
+ const size = typeof layer.iconSize === "number" && layer.iconSize >= 0 ? layer.iconSize : 50;
740
+ const color = typeof layer.iconColor === "string" ? canonColor(layer.iconColor, "vis", site) : null;
741
+ const weight = layer.iconWeight;
742
+ const bold = weight === "bold" || weight === "bolder" || (typeof weight === "number" && weight >= 600);
743
+ return {
744
+ glyph: code,
745
+ glyphFont: typeof layer.iconFace === "string" && layer.iconFace !== "" ? layer.iconFace : "FontAwesome",
746
+ glyphSize: size,
747
+ glyphColor: color?.color ?? "#2b7ce9",
748
+ glyphWeight: bold ? "bold" : "normal",
749
+ };
750
+ }
751
+ /**
752
+ * vis-network's `imagePadding`: one number for every side, or `{ top, right, bottom, left }` (each 0 unless given).
753
+ * @param raw - the option
754
+ * @returns the four sides, or null for no padding
755
+ */
756
+ function visImagePadding(raw) {
757
+ const side = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
758
+ let pad = null;
759
+ if (typeof raw === "number") {
760
+ pad = { top: side(raw), right: side(raw), bottom: side(raw), left: side(raw) };
761
+ }
762
+ else if (isJsonObject(raw)) {
763
+ pad = { top: side(raw.top), right: side(raw.right), bottom: side(raw.bottom), left: side(raw.left) };
764
+ }
765
+ return pad === null || pad.top + pad.right + pad.bottom + pad.left === 0 ? null : pad;
589
766
  }
590
767
  /**
591
768
  * The edge keys of a vis.js options object: a color of its own stops the edge inheriting its node's.
@@ -605,8 +782,19 @@ function visEdgeLayer(o, set) {
605
782
  set("inherit", own ? false : color.inherit);
606
783
  }
607
784
  set("width", o.width);
785
+ set("value", o.value);
786
+ if (isJsonObject(o.scaling)) {
787
+ set("scalingMin", o.scaling.min);
788
+ set("scalingMax", o.scaling.max);
789
+ }
608
790
  set("dashes", o.dashes);
609
791
  set("arrows", o.arrows);
792
+ set("background", o.background);
793
+ set("smooth", o.smooth);
794
+ set("selfReference", o.selfReference);
795
+ set("selfReferenceSize", o.selfReferenceSize);
796
+ set("physics", o.physics);
797
+ set("length", o.length);
610
798
  }
611
799
  /**
612
800
  * The font keys of a vis.js options object: an object, or text like `"14px arial red"`.
@@ -615,9 +803,19 @@ function visEdgeLayer(o, set) {
615
803
  */
616
804
  function visFontLayer(font, set) {
617
805
  if (isJsonObject(font)) {
806
+ set("fontMulti", font.multi);
807
+ set("fontStrokeWidth", font.strokeWidth);
808
+ set("fontStrokeColor", font.strokeColor);
809
+ set("fontBackground", font.background);
810
+ set("fontAlign", font.align);
618
811
  set("fontColor", font.color);
619
812
  set("fontSize", font.size);
620
813
  set("fontFace", font.face);
814
+ set("fontVadjust", font.vadjust);
815
+ const mods = ["bold", "ital", "boldital", "mono"].filter((m) => font[m] !== undefined && font[m] !== null);
816
+ if (mods.length > 0) {
817
+ set("fontMods", Object.fromEntries(mods.map((m) => [m, font[m]])));
818
+ }
621
819
  }
622
820
  else if (typeof font === "string") {
623
821
  const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(font.slice(0, 256));
@@ -657,30 +855,460 @@ function visValues(layer, target, site, sized) {
657
855
  };
658
856
  if (target === "node") {
659
857
  color("fill", "fillOpacity", layer.fill);
660
- color("borderColor", null, layer.border);
858
+ color("borderColor", "borderOpacity", layer.border);
661
859
  visNodeValues(layer, sized, put, site);
662
860
  }
663
861
  else {
664
862
  color("color", "opacity", layer.edgeColor);
665
863
  visEdgeValues(layer, put, site);
864
+ visEdgeBackground(layer, put, color);
666
865
  }
667
866
  if (layer.hidden !== undefined && canonBoolean(layer.hidden, site("visible")) === true) {
668
867
  put("visible", false);
669
868
  }
670
- if (layer.label !== undefined) {
671
- put("label", canonText(layer.label, site("label")));
869
+ visLabelValues(layer, target, put, color, site);
870
+ return out;
871
+ }
872
+ /**
873
+ * An edge's background: a band `size` wide under the line, its outline beyond the line. vis-network draws it only
874
+ * under a smooth (bezier) edge (BezierEdgeBase._bezierCurve calls drawBackground; StraightEdge never does).
875
+ * @param layer - the layer
876
+ * @param put - the row's setter
877
+ * @param color - the row's color setter
878
+ */
879
+ function visEdgeBackground(layer, put, color) {
880
+ // ponytail: `smooth` is read from this layer alone, else vis-network's default (smooth)
881
+ if (!isJsonObject(layer.background)) {
882
+ return;
672
883
  }
673
- if (layer.title !== undefined) {
674
- put("tooltip", canonText(layer.title, site("tooltip")));
884
+ const bg = layer.background;
885
+ const straight = layer.smooth === false || (isJsonObject(layer.smooth) && layer.smooth.enabled === false);
886
+ color("outlineColor", "outlineOpacity", bg.color);
887
+ if (bg.enabled !== undefined) {
888
+ const size = typeof bg.size === "number" ? bg.size : 10;
889
+ const width = typeof layer.width === "number" ? layer.width : 1;
890
+ put("outlineWidth", bg.enabled === true && !straight ? Math.max(0, size - width) : 0);
675
891
  }
676
- color("labelColor", null, layer.fontColor);
892
+ const band = visBandStyle(bg, false);
893
+ if (band !== null) {
894
+ put("outlineStyle", band);
895
+ }
896
+ }
897
+ /**
898
+ * The label values of a vis.js layer: its text (markup read), wrap width, font, outline, background and, for an edge,
899
+ * where on the line it sits.
900
+ * @param layer - the layer
901
+ * @param target - nodes or edges
902
+ * @param put - the row's setter
903
+ * @param color - the row's color setter
904
+ * @param site - where its values are read
905
+ */
906
+ function visLabelValues(layer, target, put, color, site) {
677
907
  if (layer.fontSize !== undefined) {
678
908
  put("labelSize", canonNumber(layer.fontSize, site("labelSize")));
679
909
  }
680
910
  if (layer.fontFace !== undefined) {
681
911
  put("labelFont", canonText(layer.fontFace, site("labelFont")));
682
912
  }
683
- return out;
913
+ if (layer.label !== undefined) {
914
+ visLabelText(layer, target, put, site);
915
+ }
916
+ // vis-network wraps a label's lines at `widthConstraint.maximum` (or a number), margins not taken off:
917
+ // Label.adjustSizes subtracts them only under constrainWidth, which nothing sets. LabelSplitter keeps each blank
918
+ // as a word of its own and joins words with a blank (getLongestFit), so a blank between two words counts three.
919
+ if (typeof layer.wrapWidth === "number" && Number.isFinite(layer.wrapWidth) && layer.wrapWidth > 0) {
920
+ put("labelWrapWidth", layer.wrapWidth);
921
+ put("labelWrapSpaceCount", 3);
922
+ }
923
+ if (layer.title !== undefined) {
924
+ put("tooltip", canonText(layer.title, site("tooltip")));
925
+ }
926
+ // a label drawn under `scaling.label.drawThreshold - 1` px on screen is not drawn (Label.draw)
927
+ const threshold = isJsonObject(layer.scalingLabel) ? layer.scalingLabel.drawThreshold : undefined;
928
+ if (typeof threshold === "number" && Number.isFinite(threshold)) {
929
+ put("labelMinScreenSize", Math.max(0, threshold - 1));
930
+ }
931
+ visLabelLook(layer, target, put, color, site);
932
+ }
933
+ /**
934
+ * The text of a vis.js layer's label, its markup read into runs.
935
+ * @param layer - the layer
936
+ * @param target - nodes or edges
937
+ * @param put - the row's setter
938
+ * @param site - where its values are read
939
+ */
940
+ function visLabelText(layer, target, put, site) {
941
+ // vis-network 9.1.9 draws no edge label whose `font` is text: Edge.drawLabel reads `options.font.align` (undefined
942
+ // on a string) as not horizontal and moves the context to the label point, while the label module, which
943
+ // parsed the text, still draws at the absolute point, so the label lands off the canvas
944
+ const text = target === "edge" && layer.fontText === true ? "" : canonText(layer.label, site("label"));
945
+ const marked = typeof text === "string" ? visMarkup(text, layer.fontMulti) : null;
946
+ put("label", marked?.text ?? text);
947
+ if (marked !== null) {
948
+ const runs = visRuns(marked.runs, layer, site("labelRuns"));
949
+ put("labelRuns", runs);
950
+ if (runs !== null) {
951
+ put("labelRunAlign", visRunAlign(layer, target));
952
+ }
953
+ }
954
+ // after the face: a label all in mono is drawn in the mono face
955
+ for (const [p, v] of Object.entries(VIS_MOD_VALUES[marked?.mod ?? "normal"] ?? {})) {
956
+ put(p, v);
957
+ }
958
+ }
959
+ /**
960
+ * The look of a vis.js layer's label: its color, background box, letter outline and, for an edge, where on the line
961
+ * it sits.
962
+ * @param layer - the layer
963
+ * @param target - nodes or edges
964
+ * @param put - the row's setter
965
+ * @param color - the row's color setter
966
+ * @param site - where its values are read
967
+ */
968
+ function visLabelLook(layer, target, put, color, site) {
969
+ color("labelColor", null, layer.fontColor);
970
+ // font.background "none" (vis-network's default) draws no box
971
+ color("labelBackground", null, layer.fontBackground === "none" ? undefined : layer.fontBackground);
972
+ // the box is the words' own: as wide as the text and one font size tall a line (Label._drawBackground: getSize)
973
+ if (typeof layer.fontBackground === "string" && layer.fontBackground !== "none") {
974
+ put("labelPadding", 0);
975
+ put("labelLineHeight", 1);
976
+ }
977
+ // a stroke with no color of its own is vis-network's default, white (font.strokeColor "#ffffff")
978
+ const stroked = typeof layer.fontStrokeWidth === "number" && layer.fontStrokeWidth > 0;
979
+ color("labelOutlineColor", null, layer.fontStrokeColor ?? (stroked ? "#ffffff" : undefined));
980
+ if (layer.fontStrokeWidth !== undefined) {
981
+ put("labelOutlineWidth", canonNumber(layer.fontStrokeWidth, site("labelOutlineWidth")));
982
+ }
983
+ if (target === "edge" && typeof layer.fontAlign === "string") {
984
+ // vis-network draws an edge label on the line ("horizontal", "middle"), above it ("top") or below it ("bottom")
985
+ put("labelPosition", layer.fontAlign === "top" || layer.fontAlign === "bottom" ? layer.fontAlign : "center");
986
+ }
987
+ else if (target === "node" && typeof layer.fontAlign === "string") {
988
+ // a node label's lines are centered on one another, or flush left or right in the label's box (Label._drawText:
989
+ // any align but center and right draws flush left); an edge label's lines are always centered
990
+ put("labelTextAlign", { center: "center", right: "right" }[layer.fontAlign] ?? "left");
991
+ }
992
+ // the text is drawn `font.vadjust` lower than its place (Label._drawText: y + block.vadjust)
993
+ if (typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) && layer.fontVadjust !== 0) {
994
+ put("labelOffsetY", layer.fontVadjust);
995
+ }
996
+ }
997
+ /** The label values of a label whose every word has one vis.js font modifier; vis-network's mono face is monospace. */
998
+ const VIS_MOD_VALUES = {
999
+ bold: { labelWeight: "bold" },
1000
+ ital: { labelItalic: true },
1001
+ boldital: { labelWeight: "bold", labelItalic: true },
1002
+ mono: { labelFont: "monospace" },
1003
+ };
1004
+ /**
1005
+ * Whether vis-network's node `opacity` replaces a color's alpha: a hex color of 3 or 6 digits or an `rgb()` color, and
1006
+ * vis-network's own default (overrideOpacity; hexToRGB reads no other text).
1007
+ * @param color - the color as the file gives it, or undefined for vis-network's default
1008
+ * @returns true when the opacity applies
1009
+ */
1010
+ function visTakesOpacity(color) {
1011
+ if (color === undefined) {
1012
+ return true;
1013
+ }
1014
+ return (typeof color === "string" &&
1015
+ !color.includes("rgba") &&
1016
+ (color.includes("rgb") || /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color.trim())));
1017
+ }
1018
+ /**
1019
+ * The line style of an edge's background band: stroked `size` wide (10 by default) in its own dashes, [5, 5] for true
1020
+ * (EdgeBase.drawBackground, setStrokeDashed), with the canvas's line caps.
1021
+ * @param bg - the edge's `background`
1022
+ * @param round - whether the canvas draws round caps by then (see visStyles' edge loop)
1023
+ * @returns the line style, or null for a band with no dashes
1024
+ */
1025
+ function visBandStyle(bg, round) {
1026
+ if (bg.dashes === undefined || bg.dashes === false) {
1027
+ return null;
1028
+ }
1029
+ return visDashStyle(bg.dashes === true ? [5, 5] : bg.dashes, typeof bg.size === "number" ? bg.size : 10, round);
1030
+ }
1031
+ /**
1032
+ * vis.js dashes (`dashes`, `shapeProperties.borderDashes`) as the line style they draw at a width, read as a dash
1033
+ * array is read anywhere: with round caps each dash a width longer and each gap a width shorter; then gaps under a
1034
+ * tenth of the pattern are solid, dashes no longer than 1.5 widths are dotted, dashes of which the longest is twice
1035
+ * the shortest dash-dot, others dashed; false is solid.
1036
+ * @param dashes - false, or the dash array (dash, gap, dash, gap ...; an odd one repeats)
1037
+ * @param width - the line's width
1038
+ * @param round - whether the dashes have round caps
1039
+ * @returns the line style
1040
+ */
1041
+ function visDashStyle(dashes, width, round) {
1042
+ if (!Array.isArray(dashes)) {
1043
+ return dashes === false ? "solid" : "dashed";
1044
+ }
1045
+ const lengths = dashes.filter((d) => typeof d === "number" && Number.isFinite(d));
1046
+ const pairs = lengths.length % 2 === 1 ? [...lengths, ...lengths] : lengths;
1047
+ const grow = round ? width : 0;
1048
+ const on = pairs.filter((_d, i) => i % 2 === 0).map((d) => Math.max(0, d) + grow);
1049
+ const off = pairs.filter((_d, i) => i % 2 === 1).map((d) => Math.max(0, d - grow));
1050
+ const sum = (xs) => xs.reduce((a, b) => a + b, 0);
1051
+ // a gap of exactly a tenth still shows (mappings-bypass edge-default: [5, 5] at width 4)
1052
+ if (on.length === 0 || sum(off) < 0.1 * (sum(on) + sum(off))) {
1053
+ return "solid";
1054
+ }
1055
+ if (on.every((d) => d <= 1.5 * Math.max(1, width))) {
1056
+ return "dotted";
1057
+ }
1058
+ return Math.max(...on) >= 2 * Math.min(...on) ? "dash-dot" : "dashed";
1059
+ }
1060
+ /**
1061
+ * vis.js label markup (`font.multi`): true or "html" reads `<b>`, `<i>` and `<code>` and the entities `&lt;` and
1062
+ * `&amp;`; "markdown" or "md" reads `*bold*`, `_ital_` and `` `mono` `` at word edges, with `\` escaping a mark
1063
+ * (vis-network 9.1.9 `LabelSplitter.splitHtmlBlocks`, `splitMarkdownBlocks`). The text without its markup, and the
1064
+ * one font modifier every word has when they share one.
1065
+ * @param text - the label
1066
+ * @param multi - `font.multi`
1067
+ * @returns the plain text and the shared modifier, or null when the label is not read as markup
1068
+ */
1069
+ function visMarkup(text, multi) {
1070
+ const system = VIS_MARKUP_SYSTEM.get(multi);
1071
+ if (system === undefined) {
1072
+ return null;
1073
+ }
1074
+ const state = { bold: false, ital: false, mono: false };
1075
+ const runs = [];
1076
+ const source = text.slice(0, 4096);
1077
+ let i = 0;
1078
+ while (i < source.length) {
1079
+ const tagged = visMarkTag(source, i, system, state);
1080
+ if (tagged > 0) {
1081
+ i += tagged;
1082
+ continue;
1083
+ }
1084
+ const [ch, next] = visMarkChar(source, i, system);
1085
+ runs.push({ text: ch, mod: visMarkMod(state) });
1086
+ i = next;
1087
+ }
1088
+ const mods = new Set(runs.filter((r) => /\S/.test(r.text)).map((r) => r.mod));
1089
+ const only = mods.size === 1 ? [...mods][0] : null;
1090
+ // the characters in runs of one modifier each, as vis-network draws its blocks
1091
+ const merged = [];
1092
+ for (const r of runs) {
1093
+ const last = merged.at(-1);
1094
+ if (last?.mod === r.mod) {
1095
+ last.text += r.text;
1096
+ }
1097
+ else {
1098
+ merged.push({ ...r });
1099
+ }
1100
+ }
1101
+ return { text: runs.map((r) => r.text).join(""), mod: only === "normal" ? null : only, runs: merged };
1102
+ }
1103
+ /**
1104
+ * vis-network's own options of each font modifier (vis-network 9.1.9 `NodesHandler` defaults, `font.mono`); an option
1105
+ * a modifier does not set is the label's own (Label.propagateFonts).
1106
+ */
1107
+ const VIS_MOD_DEFAULTS = {
1108
+ bold: { mod: "bold" },
1109
+ ital: { mod: "italic" },
1110
+ boldital: { mod: "bold italic" },
1111
+ mono: { mod: "", face: "monospace", size: 15, vadjust: 2 },
1112
+ };
1113
+ /**
1114
+ * vis.js multi-font runs as the neutral `labelRuns`: each run's font as vis-network draws it (its modifier's own
1115
+ * `font.bold`, `font.ital`, `font.boldital` or `font.mono` options, an object or text like "16px arial black", over
1116
+ * the label's font; mono is 15 px monospace unless given), each field only where it differs from the label's own, and
1117
+ * `shift`: how much lower than the label's own words its modifier's `vadjust` draws the run (mono's is 2 unless given;
1118
+ * the others' are the label's).
1119
+ * @param runs - the runs, each with its modifier
1120
+ * @param layer - the element's layer
1121
+ * @param site - where a bad color is noted
1122
+ * @returns the runs as JSON, or null when every run is drawn in the label's own font
1123
+ */
1124
+ function visRuns(runs, layer, site) {
1125
+ const own = {
1126
+ face: typeof layer.fontFace === "string" ? layer.fontFace : "arial",
1127
+ size: typeof layer.fontSize === "number" ? layer.fontSize : VIS_FONT_SIZE,
1128
+ color: typeof layer.fontColor === "string" ? layer.fontColor : null,
1129
+ vadjust: typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) ? layer.fontVadjust : 0,
1130
+ };
1131
+ const mods = isJsonObject(layer.fontMods) ? layer.fontMods : {};
1132
+ let differs = false;
1133
+ const out = runs.map((r) => {
1134
+ const run = { text: r.text };
1135
+ const base = VIS_MOD_DEFAULTS[r.mod];
1136
+ if (base === undefined) {
1137
+ return run;
1138
+ }
1139
+ const given = visModOptions(mods[r.mod]);
1140
+ const modText = typeof given.mod === "string" ? given.mod : base.mod;
1141
+ const face = typeof given.face === "string" ? given.face : (base.face ?? own.face);
1142
+ const size = typeof given.size === "number" ? given.size : (base.size ?? own.size);
1143
+ const vadjust = typeof given.vadjust === "number" ? given.vadjust : (base.vadjust ?? own.vadjust);
1144
+ const color = typeof given.color === "string" ? canonColor(given.color, "vis", site)?.color : null;
1145
+ if (modText.includes("bold")) {
1146
+ run.weight = "bold";
1147
+ }
1148
+ if (modText.includes("italic")) {
1149
+ run.italic = true;
1150
+ }
1151
+ if (face !== null && face !== own.face) {
1152
+ run.font = face;
1153
+ }
1154
+ if (size !== own.size) {
1155
+ run.size = size;
1156
+ }
1157
+ if (Number.isFinite(vadjust) && vadjust !== own.vadjust) {
1158
+ run.shift = vadjust - own.vadjust;
1159
+ }
1160
+ if (typeof color === "string") {
1161
+ run.color = color;
1162
+ }
1163
+ differs || (differs = Object.keys(run).length > 1);
1164
+ return run;
1165
+ });
1166
+ return differs ? JSON.stringify(out) : null;
1167
+ }
1168
+ /**
1169
+ * Where vis-network sets the runs of a label on a line: each block is drawn at the line's place with the label's
1170
+ * `textBaseline` (Label._drawText): "hanging" below a node sized by `size` (ShapeBase._drawShape), "middle" inside
1171
+ * one sized by its label and on an edge's line, "alphabetic" above an edge (`font.align` top) and "hanging" below it.
1172
+ * @param layer - the element's merged layer
1173
+ * @param target - nodes or edges
1174
+ * @returns the neutral run alignment
1175
+ */
1176
+ function visRunAlign(layer, target) {
1177
+ if (target === "edge") {
1178
+ return ({ top: "baseline", bottom: "hanging" }[String(layer.fontAlign)] ?? "middle");
1179
+ }
1180
+ const shape = typeof layer.shape === "string" ? layer.shape : "ellipse";
1181
+ return VIS_SIZED.has(shape) ? "hanging" : "middle";
1182
+ }
1183
+ /**
1184
+ * How much lower than a neutral `bottom` label vis-network draws the label of a node sized by `size`: it hangs the
1185
+ * text's top 4 + size / 2 below the outline, whatever the line count (ShapeBase._drawShape puts the label half its
1186
+ * height below the node and Label.calculateLabelSize moves its first line up by that again, then down 4 and half a
1187
+ * font size more), where a `bottom` label's words start about 2 + size / 3 below it (a small gap, then the space
1188
+ * above the capitals in a line's box). Measured on the vis-network labels sheet, font sizes 6 to 40.
1189
+ * @param size - the label's font size
1190
+ * @returns the extra distance down
1191
+ */
1192
+ function visBelowGap(size) {
1193
+ // ponytail: a fixed fit of the renderer's gap and line box; a neutral "hang the text's top here" anchor would remove it
1194
+ return 2 + size / 6;
1195
+ }
1196
+ /**
1197
+ * How much farther below a node sized by `size` vis-network draws a label in runs than one in its own font alone:
1198
+ * half of what its lines are taller (each line as tall as its largest run, Label: line.height) than one font size each
1199
+ * (ShapeBase._drawShape centers the label half its height below the node, then draws its first line at the top).
1200
+ * @param runs - the label's runs, as `visRuns` writes them
1201
+ * @param size - the label's font size
1202
+ * @returns the distance, 0 for runs no larger than the label's font
1203
+ */
1204
+ function visRunsDrop(runs, size) {
1205
+ let parsed;
1206
+ try {
1207
+ parsed = JSON.parse(runs);
1208
+ }
1209
+ catch {
1210
+ return 0;
1211
+ }
1212
+ if (!Array.isArray(parsed)) {
1213
+ return 0;
1214
+ }
1215
+ const lines = [size];
1216
+ for (const run of parsed) {
1217
+ const runSize = typeof run.size === "number" ? run.size : size;
1218
+ (typeof run.text === "string" ? run.text : "").split("\n").forEach((piece, i) => {
1219
+ if (i > 0) {
1220
+ lines.push(size);
1221
+ }
1222
+ if (piece !== "") {
1223
+ lines[lines.length - 1] = Math.max(lines.at(-1) ?? size, runSize);
1224
+ }
1225
+ });
1226
+ }
1227
+ return lines.reduce((sum, h) => sum + h - size, 0) / 2;
1228
+ }
1229
+ /**
1230
+ * A vis.js font modifier's options: an object, or text like "16px arial black" (size, face, color).
1231
+ * @param raw - `font.bold`, `font.ital`, `font.boldital` or `font.mono`
1232
+ * @returns the options
1233
+ */
1234
+ function visModOptions(raw) {
1235
+ if (isJsonObject(raw)) {
1236
+ return raw;
1237
+ }
1238
+ if (typeof raw === "string") {
1239
+ const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(raw.slice(0, 256));
1240
+ if (m !== null) {
1241
+ return { size: Number(m[1]), face: m[2], ...(m[3] === undefined ? {} : { color: m[3] }) };
1242
+ }
1243
+ }
1244
+ return {};
1245
+ }
1246
+ /** vis-network's markup systems by `font.multi`. */
1247
+ const VIS_MARKUP_SYSTEM = new Map([
1248
+ [true, "html"],
1249
+ ["html", "html"],
1250
+ ["markdown", "md"],
1251
+ ["md", "md"],
1252
+ ]);
1253
+ /** Each system's marks and the modifier each turns on or off. */
1254
+ const VIS_MARKS = {
1255
+ html: { "<b>": "bold", "</b>": "bold", "<i>": "ital", "</i>": "ital", "<code>": "mono", "</code>": "mono" },
1256
+ md: { "*": "bold", _: "ital", "`": "mono" },
1257
+ };
1258
+ /**
1259
+ * Reads a mark at a place in a label, turning its modifier on or off: an html tag opens a modifier that is off and
1260
+ * closes one that is on; a markdown mark opens at the start of a word and closes a modifier that is on.
1261
+ * @param source - the label
1262
+ * @param i - the place
1263
+ * @param system - the markup system
1264
+ * @param state - the modifiers on, changed in place
1265
+ * @returns how many characters the mark takes, 0 for no mark
1266
+ */
1267
+ function visMarkTag(source, i, system, state) {
1268
+ const marks = VIS_MARKS[system];
1269
+ const tag = Object.keys(marks).find((m) => source.startsWith(m, i));
1270
+ if (tag === undefined) {
1271
+ return 0;
1272
+ }
1273
+ const key = marks[tag];
1274
+ const toggles = system === "html" ? state[key] === tag.startsWith("</") : state[key] || i === 0 || /\s/.test(source[i - 1]);
1275
+ if (!toggles) {
1276
+ return 0;
1277
+ }
1278
+ state[key] = !state[key];
1279
+ return tag.length;
1280
+ }
1281
+ /**
1282
+ * The character at a place in a label, an html entity (`&lt;`, `&amp;`) or a markdown escape read as the character it
1283
+ * stands for.
1284
+ * @param source - the label
1285
+ * @param i - the place
1286
+ * @param system - the markup system
1287
+ * @returns the character and the place after it
1288
+ */
1289
+ function visMarkChar(source, i, system) {
1290
+ if (system === "md") {
1291
+ return source[i] === "\\" && i + 1 < source.length ? [source[i + 1], i + 2] : [source[i], i + 1];
1292
+ }
1293
+ const entity = [
1294
+ ["&lt;", "<"],
1295
+ ["&amp;", "&"],
1296
+ ].find(([e]) => source.startsWith(e, i));
1297
+ return entity === undefined ? [source[i], i + 1] : [entity[1], i + entity[0].length];
1298
+ }
1299
+ /**
1300
+ * The font modifier the modifiers on make: mono over the others, bold and italic together boldital.
1301
+ * @param state - the modifiers on
1302
+ * @returns the modifier
1303
+ */
1304
+ function visMarkMod(state) {
1305
+ if (state.mono) {
1306
+ return "mono";
1307
+ }
1308
+ if (state.bold) {
1309
+ return state.ital ? "boldital" : "bold";
1310
+ }
1311
+ return state.ital ? "ital" : "normal";
684
1312
  }
685
1313
  /**
686
1314
  * The node values of a vis.js layer besides its colors.
@@ -692,17 +1320,40 @@ function visValues(layer, target, site, sized) {
692
1320
  function visNodeValues(layer, sized, put, site) {
693
1321
  if (layer.shape !== undefined) {
694
1322
  put("shape", canonEnum("shape", layer.shape, "vis", site("shape")));
1323
+ // vis-network draws a label inside the shapes sized by their label and below the shapes sized by `size`
1324
+ if (typeof layer.shape === "string") {
1325
+ put("labelPosition", VIS_SIZED.has(layer.shape) ? "bottom" : "center");
1326
+ }
695
1327
  }
696
1328
  if (sized && layer.size !== undefined) {
1329
+ // vis-network draws `size` as a radius: a dot of size 25 is 50 across
697
1330
  const v = canonNumber(layer.size, site("width"));
698
- put("width", v);
699
- put("height", v);
1331
+ put("width", v === null ? null : 2 * v);
1332
+ put("height", v === null ? null : 2 * v);
700
1333
  }
701
1334
  if (layer.borderWidth !== undefined) {
702
1335
  put("borderWidth", canonNumber(layer.borderWidth, site("borderWidth")));
703
1336
  }
1337
+ visNodeOpacityAndDashes(layer, put, site);
1338
+ }
1339
+ /**
1340
+ * A vis.js node's opacity and border dashes (visNodeValues).
1341
+ * @param layer - the layer
1342
+ * @param put - the row's setter
1343
+ * @param site - where its values are read
1344
+ */
1345
+ function visNodeOpacityAndDashes(layer, put, site) {
704
1346
  if (layer.opacity !== undefined) {
705
- put("opacity", canonOpacity(layer.opacity, "vis", site("opacity")));
1347
+ // vis-network's node opacity replaces the alpha of its border color and its background color alike, but only of
1348
+ // a hex or rgb() color: overrideOpacity leaves rgba() and any other text (a name) as it is (Node.getFormattingValues)
1349
+ const opacity = canonOpacity(layer.opacity, "vis", site("opacity"));
1350
+ put("opacity", visTakesOpacity(layer.fill) ? opacity : null);
1351
+ put("borderOpacity", visTakesOpacity(layer.border) ? opacity : null);
1352
+ }
1353
+ if (layer.borderDashes !== undefined) {
1354
+ // vis-network's default border dashes are [5, 15]
1355
+ const width = typeof layer.borderWidth === "number" ? layer.borderWidth : 1;
1356
+ put("borderStyle", visDashStyle(layer.borderDashes === true ? [5, 15] : layer.borderDashes, width, false));
706
1357
  }
707
1358
  }
708
1359
  /**
@@ -719,24 +1370,71 @@ function visEdgeValues(layer, put, site) {
719
1370
  put("width", canonNumber(layer.width, site("width")));
720
1371
  }
721
1372
  if (layer.dashes !== undefined) {
722
- put("lineStyle", layer.dashes === false || layer.dashes === "false" ? "solid" : "dashed");
1373
+ // vis-network's default dashes are [5, 5]
1374
+ const width = typeof layer.width === "number" ? layer.width : 1;
1375
+ const dashes = layer.dashes === true ? [5, 5] : layer.dashes;
1376
+ // vis-network draws an edge with round caps (EdgeBase: lineCap "round"), each dash a width longer
1377
+ put("lineStyle", layer.dashes === "false" ? "solid" : visDashStyle(dashes, width, true));
723
1378
  }
724
1379
  if (layer.arrows !== undefined) {
725
- const [to, from] = visArrows(layer.arrows, site);
1380
+ const [to, from, middle] = visArrows(layer.arrows, site);
726
1381
  put("targetArrow", to);
727
1382
  put("sourceArrow", from);
1383
+ put("midTargetArrow", middle);
1384
+ // One neutral size for every end: the target's scaleFactor, else the source's, else the middle's.
1385
+ const scaled = (name) => {
1386
+ const a = isJsonObject(layer.arrows) ? layer.arrows[name] : undefined;
1387
+ return isJsonObject(a) ? a.scaleFactor : undefined;
1388
+ };
1389
+ const scale = scaled("to") ?? scaled("from") ?? scaled("middle");
1390
+ if (scale !== undefined) {
1391
+ put("arrowSize", canonNumber(scale, site("arrowSize")));
1392
+ }
1393
+ visArrowImages(layer.arrows, put, site);
1394
+ }
1395
+ }
1396
+ /**
1397
+ * The pictures of vis.js `image` arrows (`{ to: { type: "image", src, imageWidth, imageHeight } }`), each with the
1398
+ * size it is drawn at where the file gives one (vis-network 9.1.9 `Image.draw`: `imageWidth` across the line,
1399
+ * `imageHeight` along it, else the picture's own).
1400
+ * @param arrows - the `arrows` value
1401
+ * @param put - the row's setter
1402
+ * @param site - where its values are read
1403
+ */
1404
+ function visArrowImages(arrows, put, site) {
1405
+ if (!isJsonObject(arrows)) {
1406
+ return;
1407
+ }
1408
+ for (const [name, property] of [
1409
+ ["to", "targetArrow"],
1410
+ ["from", "sourceArrow"],
1411
+ ["middle", "midTargetArrow"],
1412
+ ]) {
1413
+ const a = arrows[name];
1414
+ if (!isJsonObject(a) || a.type !== "image" || typeof a.src !== "string" || a.src === "") {
1415
+ continue;
1416
+ }
1417
+ put(`${property}Image`, a.src);
1418
+ if (a.imageWidth !== undefined) {
1419
+ put(`${property}ImageWidth`, canonNumber(a.imageWidth, site(`${property}ImageWidth`)));
1420
+ }
1421
+ if (a.imageHeight !== undefined) {
1422
+ put(`${property}ImageHeight`, canonNumber(a.imageHeight, site(`${property}ImageHeight`)));
1423
+ }
728
1424
  }
729
1425
  }
730
1426
  /**
731
- * vis.js `arrows` as the target and source arrows: `"to"`, `"to, from"`, or `{ to: true | { enabled, type } }`.
1427
+ * vis.js `arrows` as the target, source and middle arrows: `"to"`, `"to, from"`, `"middle"`, or
1428
+ * `{ to: true | { enabled, type } }`. The middle one points toward the target.
732
1429
  * @param arrows - the value
733
1430
  * @param site - where it is read
734
- * @returns the target arrow and the source arrow
1431
+ * @returns the target arrow, the source arrow and the middle arrow
735
1432
  */
736
1433
  function visArrows(arrows, site) {
737
- const end = (name, property) => {
1434
+ // a middle arrow the file does not mention is no rule at all, so a file that names only its ends carries none
1435
+ const end = (name, property, absent = "none") => {
738
1436
  if (typeof arrows === "string") {
739
- return new RegExp(String.raw `\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : "none";
1437
+ return new RegExp(String.raw `\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : absent;
740
1438
  }
741
1439
  if (!isJsonObject(arrows)) {
742
1440
  return null;
@@ -745,12 +1443,15 @@ function visArrows(arrows, site) {
745
1443
  if (a === true) {
746
1444
  return "triangle";
747
1445
  }
1446
+ if (a === undefined) {
1447
+ return absent;
1448
+ }
748
1449
  if (!isJsonObject(a) || a.enabled === false) {
749
1450
  return "none";
750
1451
  }
751
1452
  return canonEnum("arrow", a.type ?? "arrow", "vis", site(property));
752
1453
  };
753
- return [end("to", "targetArrow"), end("from", "sourceArrow")];
1454
+ return [end("to", "targetArrow"), end("from", "sourceArrow"), end("middle", "midTargetArrow", null)];
754
1455
  }
755
1456
  /**
756
1457
  * vis.js: the per-element keys over the file's groups and `options.nodes` / `options.edges`, beneath them
@@ -780,13 +1481,17 @@ function visStyles(snapshot, src, context) {
780
1481
  context.budget.spend("node", n);
781
1482
  // what vis-network draws per node: its own keys over its group over options.nodes
782
1483
  const node = valuesOf(n);
783
- const records = nodeRecords(snapshot);
784
- const rawShape = (layer) => (typeof layer.shape === "string" ? layer.shape : null);
1484
+ const shapes = [];
1485
+ const rawRecords = elementRaw(snapshot, "node");
1486
+ const range = visNodeValueRange(n, groupOf, rawRecords, groupLayers, nodeDefaults);
1487
+ const ownGroup = visOwnGroup(groupLayers, visGroupDefs(src, options));
785
1488
  for (let i = 0; i < n; i++) {
786
1489
  const g = groupOf(i);
787
- const own = records(i);
788
- const group = g === null ? {} : (groupLayers.get(g) ?? {});
789
- const shape = rawShape(own) ?? rawShape(group) ?? rawShape(nodeDefaults) ?? "ellipse";
1490
+ const raw = rawRecords(i);
1491
+ const own = visLayer(raw, "node");
1492
+ const group = g === null ? {} : ownGroup(g, raw);
1493
+ const shape = visShapeOf(own) ?? visShapeOf(group) ?? visShapeOf(nodeDefaults) ?? "ellipse";
1494
+ shapes.push(shape);
790
1495
  const site = (property) => ({
791
1496
  notes,
792
1497
  target: "node",
@@ -794,8 +1499,8 @@ function visStyles(snapshot, src, context) {
794
1499
  element: i,
795
1500
  budget: context.budget,
796
1501
  });
797
- const merged = { ...nodeDefaults, ...group, ...own };
798
- for (const [p, v] of Object.entries(visValues(merged, "node", site, VIS_SIZED.has(shape)))) {
1502
+ const merged = visMergedNode(nodeDefaults, group, own, g !== null && raw.color !== undefined);
1503
+ for (const [p, v] of Object.entries(visNodeRow(merged, shape, range, site))) {
799
1504
  node.put(p, i, v);
800
1505
  }
801
1506
  }
@@ -803,23 +1508,15 @@ function visStyles(snapshot, src, context) {
803
1508
  if (Object.keys(declaredNode).length > 0) {
804
1509
  nodeRules.push(rule("node.vis.defaults", "node", "defaults", "declared", { all: true }, declaredNode));
805
1510
  }
806
- let k = 0;
807
- for (const [g, layer] of groupLayers) {
808
- const set = paletteRows.get(g) ?? visValues(layer, "node", ruleSite("node"), true);
809
- if (Object.keys(set).length === 0) {
810
- continue;
811
- }
812
- const key = `group.${String(k)}`;
813
- nodeColumns.set(key, Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)));
814
- const origin = paletteRows.has(g) ? "tool" : "declared";
815
- const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
816
- nodeRules.push({ ...r, facts: { names: [[key, g]] } });
817
- }
1511
+ visGroupRules(n, groupOf, groupLayers, paletteRows, ruleSite("node"), nodeRules, nodeColumns);
818
1512
  const toolNode = toolDefaults("vis-network")?.node;
819
1513
  const borderOf = (i) => node.values.borderColor?.[i] ?? toolNode?.borderColor?.value ?? "#2b7ce9";
820
- const edge = visEdges(snapshot, visLayer(options.edges, "edge"), borderOf, notes, context);
1514
+ const geometry = visGeometry(snapshot, node.values, shapes);
1515
+ const support = visSupport(options.physics, options.layout);
1516
+ const edges = visLayer(options.edges, "edge");
1517
+ const edge = visEdges(snapshot, edges, borderOf, { ...geometry, support }, notes, context);
821
1518
  const values = { node: node.values, edge: edge.values };
822
- exactFromBlock(snapshot, src.block, values, VIS_WRITTEN);
1519
+ exactFromBlock(snapshot, src.block, values, visWritten(snapshot, values.edge));
823
1520
  return {
824
1521
  canonical: true,
825
1522
  dialect: "vis",
@@ -836,6 +1533,343 @@ function visStyles(snapshot, src, context) {
836
1533
  },
837
1534
  };
838
1535
  }
1536
+ /**
1537
+ * vis.js: a layer's own shape.
1538
+ * @param layer - the layer
1539
+ * @returns its `shape`, or null when it gives none
1540
+ */
1541
+ function visShapeOf(layer) {
1542
+ return typeof layer.shape === "string" ? layer.shape : null;
1543
+ }
1544
+ /**
1545
+ * vis.js: the smallest and largest `value` of the nodes, each its own over its group's over `options.nodes`.
1546
+ * @param n - the node count
1547
+ * @param groupOf - each node's group
1548
+ * @param rawRecords - each node's own keys
1549
+ * @param groupLayers - each group's layer (visGroupLayers)
1550
+ * @param nodeDefaults - `options.nodes`
1551
+ * @returns the range, or null when no node has a value
1552
+ */
1553
+ function visNodeValueRange(n, groupOf, rawRecords, groupLayers, nodeDefaults) {
1554
+ return visValueRange(n, (i) => {
1555
+ const g = groupOf(i);
1556
+ const own = visLayer(rawRecords(i), "node").value;
1557
+ const value = own ?? (g === null ? undefined : groupLayers.get(g)?.value) ?? nodeDefaults.value;
1558
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1559
+ });
1560
+ }
1561
+ /**
1562
+ * vis.js: one node's layer, its own keys over its group's over `options.nodes`.
1563
+ * @param nodeDefaults - `options.nodes`
1564
+ * @param group - its group's layer
1565
+ * @param own - its own layer
1566
+ * @param ownColorInGroup - whether it is in a group and gives a color of its own
1567
+ * @returns the merged layer
1568
+ */
1569
+ function visMergedNode(nodeDefaults, group, own, ownColorInGroup) {
1570
+ const merged = { ...nodeDefaults, ...group, ...own };
1571
+ if (ownColorInGroup) {
1572
+ // a color of the node's own fills its gaps from options.nodes and vis-network's defaults, never its group's
1573
+ merged.fill ?? (merged.fill = VIS_NODE_FILL);
1574
+ merged.border ?? (merged.border = VIS_NODE_BORDER);
1575
+ }
1576
+ return merged;
1577
+ }
1578
+ /**
1579
+ * vis.js: the values one node draws, from its merged layer: its colors and label, then its shape's own.
1580
+ * @param merged - the node's merged layer
1581
+ * @param shape - its vis-network shape
1582
+ * @param range - the smallest and largest `value` of the graph's nodes (visValueRange)
1583
+ * @param site - where its values are read
1584
+ * @returns the values
1585
+ */
1586
+ function visNodeRow(merged, shape, range, site) {
1587
+ const sized = VIS_SIZED.has(shape);
1588
+ const row = visValues(merged, "node", site, sized);
1589
+ const shaped = sized
1590
+ ? visSizedShape(merged, shape, visRadius(merged, range, site), site)
1591
+ : visFitShape(merged, shape);
1592
+ if (sized && merged.label !== undefined) {
1593
+ const size = visLabelScaled(merged, range) ?? (typeof merged.fontSize === "number" ? merged.fontSize : VIS_FONT_SIZE);
1594
+ // a label in runs below the node hangs lower by half of what its larger runs make its lines taller
1595
+ const drop = typeof row.labelRuns === "string" ? visRunsDrop(row.labelRuns, size) : 0;
1596
+ const below = typeof row.labelOffsetY === "number" ? row.labelOffsetY : 0;
1597
+ row.labelOffsetY = below + drop + visBelowGap(size);
1598
+ }
1599
+ // the shape's own label place, then font.vadjust below it
1600
+ for (const p of ["labelOffsetX", "labelOffsetY"]) {
1601
+ const [a, b] = [row[p], shaped[p]];
1602
+ if (typeof a === "number" && typeof b === "number") {
1603
+ shaped[p] = a + b;
1604
+ }
1605
+ }
1606
+ Object.assign(row, shaped);
1607
+ const labelSize = visLabelScaled(merged, range);
1608
+ if (labelSize !== null) {
1609
+ row.labelSize = labelSize;
1610
+ }
1611
+ return row;
1612
+ }
1613
+ /**
1614
+ * vis.js: one rule per node group with values, over a column marking the group's nodes; a group the file does not
1615
+ * define is the tool's palette.
1616
+ * @param n - the node count
1617
+ * @param groupOf - each node's group
1618
+ * @param groupLayers - each group's layer (visGroupLayers)
1619
+ * @param paletteRows - the palette values of the groups the file does not define
1620
+ * @param site - where a group's values are read
1621
+ * @param rules - the node rules, appended to
1622
+ * @param columns - the node columns, written to
1623
+ */
1624
+ function visGroupRules(n, groupOf, groupLayers, paletteRows, site, rules, columns) {
1625
+ let k = 0;
1626
+ for (const [g, layer] of groupLayers) {
1627
+ const set = paletteRows.get(g) ?? visValues(layer, "node", site, true);
1628
+ if (Object.keys(set).length === 0) {
1629
+ continue;
1630
+ }
1631
+ const key = `group.${String(k)}`;
1632
+ columns.set(key, Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)));
1633
+ const origin = paletteRows.has(g) ? "tool" : "declared";
1634
+ const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1635
+ rules.push({ ...r, facts: { names: [[key, g]] } });
1636
+ }
1637
+ }
1638
+ /**
1639
+ * vis.js: what a node of a shape sized by `size` draws besides its colors: its box from its radius, its picture or
1640
+ * glyph, and triangleDown turned over.
1641
+ * @param merged - the node's merged layer
1642
+ * @param shape - its vis-network shape
1643
+ * @param radius - its radius (visRadius)
1644
+ * @param site - where its values are read
1645
+ * @returns the values
1646
+ */
1647
+ function visSizedShape(merged, shape, radius, site) {
1648
+ // vis-network's hexagon is regular, its corners `size` from its center on the left and right (drawHexagon): it is
1649
+ // 2 * size wide and sqrt(3) * size tall, where the neutral hexagon fills its box
1650
+ const row = { width: 2 * radius, height: 2 * radius };
1651
+ if (shape === "hexagon") {
1652
+ // its label still hangs below the 2 * size square (ShapeBase._drawShape: y + 0.5 * height + half the label)
1653
+ row.height = Math.sqrt(3) * radius;
1654
+ row.labelOffsetY = ((2 - Math.sqrt(3)) * radius) / 2;
1655
+ }
1656
+ Object.assign(row, visImage(merged, shape, { width: 2 * radius, height: 2 * radius }));
1657
+ if (shape === "icon") {
1658
+ // an icon node is its glyph alone: vis-network draws no body, no fill and no border for it (Icon.draw)
1659
+ Object.assign(row, { shape: "none" }, visIcon(merged, site("glyphColor")));
1660
+ }
1661
+ // vis-network's triangleDown: its triangle turned over, a corner down
1662
+ if (shape === "triangleDown") {
1663
+ row.shape = "polygon";
1664
+ row.shapePoints = "-1,1 0,-1 1,1";
1665
+ }
1666
+ return row;
1667
+ }
1668
+ /**
1669
+ * vis.js: what a node of a shape sized by its label draws besides its colors: its growth round its label, nothing at
1670
+ * all with a margin of 0, and its label at the top or bottom under `heightConstraint.valign`.
1671
+ * @param merged - the node's merged layer
1672
+ * @param shape - its vis-network shape
1673
+ * @returns the values
1674
+ */
1675
+ function visFitShape(merged, shape) {
1676
+ const row = visLabelFit(merged, shape);
1677
+ // a margin of 0 leaves vis-network's margins undefined (`_setMargins` tests `if (options.margin)`), so the node's
1678
+ // size is NaN and neither it nor its label is drawn
1679
+ if (merged.margin === 0 && (shape === "box" || shape === "circle" || shape === "database")) {
1680
+ row.visible = false;
1681
+ }
1682
+ // heightConstraint.valign moves the label to the top or the bottom of a node taller than it (Label._drawText)
1683
+ if (merged.valign === "top" || merged.valign === "bottom") {
1684
+ row.labelPosition = `inside-${merged.valign}`;
1685
+ }
1686
+ else if (shape === "database") {
1687
+ // a database draws its label at its top, its top margin down (Database.draw: this.top + margin.top), not in
1688
+ // the middle of the square it grows to
1689
+ const m = merged.margin;
1690
+ const top = isJsonObject(m) ? m.top : m;
1691
+ row.labelPosition = "inside-top";
1692
+ row.labelOffsetY = typeof top === "number" && Number.isFinite(top) && top >= 0 ? top : VIS_MARGIN;
1693
+ }
1694
+ // a box's corners are rounded at `shapeProperties.borderRadius`, at most half its shorter side (drawRoundRect)
1695
+ if (shape === "box" && typeof merged.borderRadius === "number" && Number.isFinite(merged.borderRadius)) {
1696
+ row.cornerRadius = Math.max(0, merged.borderRadius);
1697
+ }
1698
+ return row;
1699
+ }
1700
+ /**
1701
+ * vis.js: how a node of a shape sized by its label grows to hold it (each shape's `resize`): its label's box, at
1702
+ * least `widthConstraint.minimum` by `heightConstraint.minimum`, plus the `margin` on each side is the node's width
1703
+ * and height; a circle and a database stay as wide as they are tall; an ellipse is the box's height taller and
1704
+ * wider instead of the margin (`width + height` by twice the height).
1705
+ * @param layer - the node's merged layer
1706
+ * @param shape - its vis-network shape
1707
+ * @returns the values, none for a shape not sized by its label
1708
+ */
1709
+ function visLabelFit(layer, shape) {
1710
+ const fit = VIS_LABEL_FIT.get(shape);
1711
+ if (fit === undefined) {
1712
+ return {};
1713
+ }
1714
+ const length = (v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
1715
+ const fontSize = length(layer.fontSize) ?? VIS_FONT_SIZE;
1716
+ const lines = typeof layer.label === "string" ? Math.max(1, layer.label.split("\n").length) : 1;
1717
+ const minW = length(layer.widthMin) ?? 0;
1718
+ const minH = length(layer.heightMin) ?? 0;
1719
+ let padX;
1720
+ let padY;
1721
+ let offset = {};
1722
+ if (fit.ellipse) {
1723
+ padX = Math.max(lines * fontSize, minH) / 2;
1724
+ padY = padX;
1725
+ }
1726
+ else {
1727
+ ({ padX, padY, offset } = visMarginPads(layer.margin, fit.square));
1728
+ }
1729
+ // a node with no label text is sized as if its label were 14 x 14 (NodeBase.getDimensionsFromLabel)
1730
+ const empty = typeof layer.label !== "string" || layer.label === "";
1731
+ const [boxW, boxH] = empty ? [Math.max(minW, 14), Math.max(minH, 14)] : [minW, minH];
1732
+ if (empty && fit.ellipse) {
1733
+ padX = boxH / 2;
1734
+ padY = padX;
1735
+ }
1736
+ const row = {
1737
+ ...offset,
1738
+ labelFit: "bounds",
1739
+ labelPaddingX: padX,
1740
+ labelPaddingY: padY,
1741
+ width: boxW > 0 ? boxW + 2 * padX : 0,
1742
+ height: boxH > 0 ? boxH + 2 * padY : 0,
1743
+ };
1744
+ if (fit.square) {
1745
+ row.labelFitSquare = true;
1746
+ }
1747
+ return row;
1748
+ }
1749
+ /**
1750
+ * vis.js: the padding a node's `margin` puts round its label (visLabelFit). A box or text node draws its label its
1751
+ * left and top margins in from its corner (Box.draw, Text.draw), off center by half the difference of opposite
1752
+ * margins.
1753
+ * @param m - the `margin`: one number for every side, or `{ top, right, bottom, left }`
1754
+ * @param square - whether the shape stays as wide as it is tall
1755
+ * @returns the horizontal and vertical padding, and the label's offset
1756
+ */
1757
+ function visMarginPads(m, square) {
1758
+ const length = (v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
1759
+ const side = (key) => (isJsonObject(m) ? length(m[key]) : length(m)) ?? VIS_MARGIN;
1760
+ const offset = {};
1761
+ if (!square && side("left") !== side("right")) {
1762
+ offset.labelOffsetX = (side("left") - side("right")) / 2;
1763
+ }
1764
+ if (!square && side("top") !== side("bottom")) {
1765
+ offset.labelOffsetY = (side("top") - side("bottom")) / 2;
1766
+ }
1767
+ return { padX: (side("left") + side("right")) / 2, padY: (side("top") + side("bottom")) / 2, offset };
1768
+ }
1769
+ /**
1770
+ * vis.js `scaling.label`: with `enabled`, the font size of an element with a `value`, mapped as its size is from the
1771
+ * value range onto `scaling.label.min` .. `max` (14 .. 30) (vis-network 9.1.9 `Node.setValueRange`, `Edge.setValueRange`).
1772
+ * @param layer - the element's merged layer
1773
+ * @param range - the value range, or null
1774
+ * @returns the font size, or null when the label is not scaled
1775
+ */
1776
+ function visLabelScaled(layer, range) {
1777
+ const value = finiteValue(layer.value);
1778
+ const label = layer.scalingLabel;
1779
+ if (value === null || range === null || !isJsonObject(label) || label.enabled !== true) {
1780
+ return null;
1781
+ }
1782
+ const min = typeof label.min === "number" ? label.min : 14;
1783
+ const max = typeof label.max === "number" ? label.max : 30;
1784
+ return visScaled(value, range, min, max);
1785
+ }
1786
+ /** vis-network's default node size, a radius (`size: 25`, vis-network.esm.js NodesHandler defaults). */
1787
+ const VIS_SIZE = 25;
1788
+ /** vis-network's default `scaling.min` and `scaling.max`, the radii `value` is mapped onto. */
1789
+ const VIS_SCALING = [10, 30];
1790
+ /**
1791
+ * vis.js: the smallest and largest `value` of the nodes that have one, as vis-network's
1792
+ * `_updateValueRange` takes them; null when no node has one.
1793
+ * @param n - the node count
1794
+ * @param valueOf - each node's value
1795
+ * @returns the range
1796
+ */
1797
+ function visValueRange(n, valueOf) {
1798
+ let min = Infinity;
1799
+ let max = -Infinity;
1800
+ for (let i = 0; i < n; i++) {
1801
+ const v = valueOf(i);
1802
+ if (v !== null) {
1803
+ min = Math.min(min, v);
1804
+ max = Math.max(max, v);
1805
+ }
1806
+ }
1807
+ return min <= max ? [min, max] : null;
1808
+ }
1809
+ /**
1810
+ * A `value` that is a finite number, else null.
1811
+ * @param value - the value
1812
+ * @returns the number, or null
1813
+ */
1814
+ const finiteValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1815
+ /**
1816
+ * vis.js: `value` mapped linearly from the value range onto `min` .. `max`, the middle when every value is the same
1817
+ * (vis-network's default `customScalingFunction`).
1818
+ * @param value - the element's value
1819
+ * @param range - the value range
1820
+ * @param min - `scaling.min`
1821
+ * @param max - `scaling.max`
1822
+ * @returns the scaled value
1823
+ */
1824
+ function visScaled(value, range, min, max) {
1825
+ const [lo, hi] = range;
1826
+ const scale = hi === lo ? 0.5 : Math.max(0, (value - lo) / (hi - lo));
1827
+ return min + scale * (max - min);
1828
+ }
1829
+ /** vis-network's default edge `scaling.min` and `scaling.max`, the widths `value` is mapped onto. */
1830
+ const VIS_EDGE_SCALING = [1, 15];
1831
+ /**
1832
+ * vis.js: the width vis-network draws an edge at once any edge has a `value`. Its `_updateValueRange` then sets every
1833
+ * edge's width: one with a value to the value mapped onto `scaling`, one without to the width it was created with,
1834
+ * which is `options.edges.width` -- an edge's own `width` is dropped (`Edge.baseWidth` is read before the edge's own
1835
+ * options are applied, and nothing updates it).
1836
+ * @param layer - the edge's merged layer
1837
+ * @param defaults - `options.edges`
1838
+ * @param range - the value range over the edges, or null when no edge has a value
1839
+ * @param site - where its values are read
1840
+ * @returns the width, or nothing when no edge has a value (the edge's own width holds)
1841
+ */
1842
+ function visEdgeWidth(layer, defaults, range, site) {
1843
+ if (range === null) {
1844
+ return {};
1845
+ }
1846
+ const value = finiteValue(layer.value);
1847
+ if (value !== null) {
1848
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_EDGE_SCALING[0];
1849
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_EDGE_SCALING[1];
1850
+ return { width: visScaled(value, range, min, max) };
1851
+ }
1852
+ return { width: (defaults.width === undefined ? null : canonNumber(defaults.width, site("width"))) ?? 1 };
1853
+ }
1854
+ /**
1855
+ * vis.js: the radius vis-network draws a node of a sized shape at: its `value` mapped linearly from the value range
1856
+ * onto `scaling.min` .. `scaling.max` (the middle when every value is the same), else its `size`, else 25
1857
+ * (vis-network's `setValueRange` and `customScalingFunction`).
1858
+ * @param layer - the node's merged layer
1859
+ * @param range - the value range over the nodes
1860
+ * @param site - where its values are read
1861
+ * @returns the radius
1862
+ */
1863
+ function visRadius(layer, range, site) {
1864
+ const value = finiteValue(layer.value);
1865
+ if (value !== null && range !== null) {
1866
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_SCALING[0];
1867
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_SCALING[1];
1868
+ return visScaled(value, range, min, max);
1869
+ }
1870
+ const size = layer.size === undefined ? null : canonNumber(layer.size, site("width"));
1871
+ return size ?? VIS_SIZE;
1872
+ }
839
1873
  /**
840
1874
  * vis.js: the group definitions, from `options.groups` or a top-level `groups`.
841
1875
  * @param src - what the import kept
@@ -876,25 +1910,64 @@ function visGroupLayers(n, groupOf, groupDefs) {
876
1910
  }
877
1911
  return { groupLayers, paletteRows };
878
1912
  }
1913
+ /**
1914
+ * vis.js: the group layer one node takes. vis-network merges a group's options into a node only for the keys the node
1915
+ * does not set itself, each key whole (vis-network 9.1.9 `Node.updateGroupOptions`: every option of the node's own is
1916
+ * skipped, `font` always), so a node with a `color` of its own takes none of its group's colors, not even a border it
1917
+ * leaves out.
1918
+ * @param groupLayers - each group's whole layer (visGroupLayers)
1919
+ * @param groupDefs - the group definitions the file gives
1920
+ * @returns the layer of a group for a node of the given own keys
1921
+ */
1922
+ function visOwnGroup(groupLayers, groupDefs) {
1923
+ const cache = new Map();
1924
+ return (g, own) => {
1925
+ const whole = groupLayers.get(g) ?? {};
1926
+ const def = Object.hasOwn(groupDefs, g) ? groupDefs[g] : null;
1927
+ // a group the file does not define is vis-network's palette color, which a color of the node's own replaces
1928
+ const skip = Object.keys(own).filter((k) => k !== "font" && (isJsonObject(def) ? Object.hasOwn(def, k) : k === "color"));
1929
+ if (skip.length === 0) {
1930
+ return whole;
1931
+ }
1932
+ const key = `${g}\u0000${skip.join("\u0000")}`;
1933
+ let layer = cache.get(key);
1934
+ if (layer === undefined) {
1935
+ layer = isJsonObject(def)
1936
+ ? visLayer(Object.fromEntries(Object.entries(def).filter(([k]) => !skip.includes(k))), "node")
1937
+ : {};
1938
+ cache.set(key, layer);
1939
+ }
1940
+ return layer;
1941
+ };
1942
+ }
879
1943
  /**
880
1944
  * vis.js edges: their own keys over `options.edges`; an edge without a color of its own takes its source node's
881
1945
  * border color (its target's under `inherit: "to"`).
882
1946
  * @param snapshot - the graph
883
1947
  * @param edgeDefaults - `options.edges`
884
1948
  * @param borderOf - each node's drawn border color
1949
+ * @param geometry - the nodes' positions and boxes, for the edges' routes
885
1950
  * @param notes - where problems are noted
886
1951
  * @param context - the work budget
887
1952
  * @returns the edge values, rules and style columns
888
1953
  */
889
- function visEdges(snapshot, edgeDefaults, borderOf, notes, context) {
1954
+ function visEdges(snapshot, edgeDefaults, borderOf, geometry, notes, context) {
890
1955
  const m = snapshot.edgeCount;
891
1956
  const edge = valuesOf(m);
892
1957
  const edgeRecords = elementRecords(snapshot, "edge");
893
1958
  const inherited = new Array(m).fill(null);
894
1959
  let inheriting = 0;
1960
+ const range = visValueRange(m, (e) => finiteValue({ ...edgeDefaults, ...edgeRecords(e) }.value));
1961
+ // vis-network sets round caps to draw a dashed line (EdgeBase._drawDashedLine) and never sets them back, so every
1962
+ // band drawn with or after a dashed edge, in the file's order, has round caps, which close gaps narrower than the
1963
+ // band
1964
+ let roundCaps = false;
895
1965
  for (let e = 0; e < m; e++) {
896
1966
  const own = edgeRecords(e);
897
- const merged = { ...edgeDefaults, ...own };
1967
+ // an array of `options.edges.dashes` reaches the edge through bridgeObject as a plain object, no longer an
1968
+ // array, so vis-network draws its default [5, 5] (EdgeBase.setStrokeDashed)
1969
+ const bridged = own.dashes === undefined && Array.isArray(edgeDefaults.dashes) ? { dashes: true } : {};
1970
+ const merged = { ...edgeDefaults, ...own, ...bridged };
898
1971
  const site = (property) => ({
899
1972
  notes,
900
1973
  target: "edge",
@@ -902,24 +1975,70 @@ function visEdges(snapshot, edgeDefaults, borderOf, notes, context) {
902
1975
  element: e,
903
1976
  budget: context.budget,
904
1977
  });
905
- const vals = visValues(merged, "edge", site, true);
1978
+ const [s, t] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
1979
+ const vals = {
1980
+ ...visValues(merged, "edge", site, true),
1981
+ ...visEdgeWidth(merged, edgeDefaults, range, site),
1982
+ ...visRoute(edgeDefaults, own, s, t, geometry),
1983
+ ...visEdgeLabel(merged, range, geometry.at(s), geometry.at(t)),
1984
+ };
1985
+ // a dashed edge sets them before it draws its own band (BezierEdgeBase._bezierCurve draws the band first)
1986
+ roundCaps || (roundCaps = merged.hidden !== true && ![undefined, false, "false"].includes(merged.dashes));
1987
+ const band = roundCaps && isJsonObject(merged.background) ? visBandStyle(merged.background, true) : null;
1988
+ if (band !== null) {
1989
+ vals.outlineStyle = band;
1990
+ }
1991
+ const from = visEdgeInherit(vals, own.inherit ?? edgeDefaults.inherit ?? "from", borderOf, s, t);
906
1992
  for (const [p, v] of Object.entries(vals)) {
907
1993
  edge.put(p, e, v);
908
1994
  }
909
- const inherit = own.inherit ?? edgeDefaults.inherit ?? "from";
910
- if (inherit === false || inherit === "false") {
911
- if (vals.color === undefined) {
912
- edge.put("color", e, VIS_EDGE_COLOR);
913
- }
914
- continue;
915
- }
916
- if (inherit === "both") {
917
- notes.add(STYLE_ISSUE.GRADIENT, "edge", "color", { sample: "inherit both", element: e });
1995
+ if (from !== null) {
1996
+ inherited[e] = from;
1997
+ inheriting++;
918
1998
  }
919
- inherited[e] = borderOf(inherit === "to" ? snapshot.edgeTarget(e) : snapshot.edgeSource(e));
920
- inheriting++;
921
1999
  }
922
2000
  context.budget.spend("edge", m);
2001
+ const { rules, columns } = visEdgeRules(edgeDefaults, notes, inherited, inheriting);
2002
+ return { values: edge.values, rules, columns };
2003
+ }
2004
+ /**
2005
+ * vis.js: an edge's `inherit`, applied to its values: an edge that inherits draws its node's border color, not a
2006
+ * color `options.edges` gives (that only stops inheriting there), and under "both" a gradient between its nodes'; one
2007
+ * that does not inherit and has no color draws vis-network's default.
2008
+ * @param vals - the edge's values, written in place
2009
+ * @param inherit - its `inherit`
2010
+ * @param borderOf - each node's drawn border color
2011
+ * @param s - its source node
2012
+ * @param t - its target node
2013
+ * @returns the color it inherits, or null for none
2014
+ */
2015
+ function visEdgeInherit(vals, inherit, borderOf, s, t) {
2016
+ const inherits = inherit !== false && inherit !== "false";
2017
+ if (inherits) {
2018
+ delete vals.color;
2019
+ }
2020
+ if (inherit === "both") {
2021
+ // vis-network: a linear gradient from the source node's border color to the target's
2022
+ vals.gradientKind = "linear";
2023
+ vals.gradientColors = `${String(borderOf(s))} ${String(borderOf(t))}`;
2024
+ }
2025
+ if (!inherits) {
2026
+ if (vals.color === undefined) {
2027
+ vals.color = VIS_EDGE_COLOR;
2028
+ }
2029
+ return null;
2030
+ }
2031
+ return borderOf(inherit === "to" ? t : s);
2032
+ }
2033
+ /**
2034
+ * vis.js: the edge rules: `options.edges` as declared, and the colors edges inherit from their nodes.
2035
+ * @param edgeDefaults - `options.edges`
2036
+ * @param notes - where problems are noted
2037
+ * @param inherited - each edge's inherited color, or null
2038
+ * @param inheriting - how many edges inherit one
2039
+ * @returns the rules and their style columns
2040
+ */
2041
+ function visEdgeRules(edgeDefaults, notes, inherited, inheriting) {
923
2042
  const edgeRules = [];
924
2043
  const edgeColumns = new Map();
925
2044
  const declaredEdge = visValues(edgeDefaults, "edge", (property) => ({ notes, target: "edge", property }), true);
@@ -941,21 +2060,479 @@ function visEdges(snapshot, edgeDefaults, borderOf, notes, context) {
941
2060
  facts: { elements: inheriting, reason: "SOURCE_NODE_COLOR" },
942
2061
  });
943
2062
  }
944
- return { values: edge.values, rules: edgeRules, columns: edgeColumns };
2063
+ return { rules: edgeRules, columns: edgeColumns };
945
2064
  }
946
- /** The vis.js keys the reader reads, per target. */
947
- const VIS_KEYS = {
948
- node: ["color", "shape", "size", "borderWidth", "opacity", "hidden", "label", "title", "font"],
949
- edge: ["color", "width", "dashes", "arrows", "hidden", "label", "title", "font"],
950
- };
951
2065
  /**
952
- * Each node's own vis.js keys as a layer.
2066
+ * vis.js: the positions and boxes of the nodes, for the routes of their edges.
953
2067
  * @param snapshot - the graph
954
- * @returns the layer of a node
2068
+ * @param nodeValues - the node values read so far (sizes and border widths)
2069
+ * @param shapes - each node's vis-network shape
2070
+ * @returns the geometry
955
2071
  */
956
- function nodeRecords(snapshot) {
957
- return elementRecords(snapshot, "node");
2072
+ function visGeometry(snapshot, nodeValues, shapes) {
2073
+ const whole = (v) => {
2074
+ if (typeof v === "string") {
2075
+ return Number.parseInt(v, 10);
2076
+ }
2077
+ return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : Number.NaN;
2078
+ };
2079
+ const [xs, ys] = [cells(snapshot, "node", "x"), cells(snapshot, "node", "y")];
2080
+ const num = (v) => (typeof v === "number" && v > 0 ? v : null);
2081
+ return {
2082
+ at: (i) => {
2083
+ const p = [whole(xs?.(i)), whole(ys?.(i))];
2084
+ return Number.isNaN(p[0]) || Number.isNaN(p[1]) ? null : p;
2085
+ },
2086
+ box: (i) => {
2087
+ const [w, h] = [num(nodeValues.width?.[i]), num(nodeValues.height?.[i])];
2088
+ const bw = nodeValues.borderWidth?.[i];
2089
+ return w === null || h === null ? null : [w / 2, h / 2, typeof bw === "number" ? bw : 1, shapes[i]];
2090
+ },
2091
+ };
958
2092
  }
2093
+ /**
2094
+ * vis.js: an edge's `smooth` over `options.edges.smooth` over vis-network's defaults (dynamic, roundness 0.5).
2095
+ * @param layers - the layers, the lowest first
2096
+ * @returns the smooth options
2097
+ */
2098
+ function visSmooth(...layers) {
2099
+ const s = { enabled: true, type: "dynamic", roundness: 0.5, forceDirection: "none" };
2100
+ for (const l of layers) {
2101
+ if (typeof l === "boolean") {
2102
+ s.enabled = l;
2103
+ }
2104
+ else if (isJsonObject(l)) {
2105
+ s.enabled = typeof l.enabled === "boolean" ? l.enabled : s.enabled;
2106
+ s.type = typeof l.type === "string" ? l.type : s.type;
2107
+ s.roundness = typeof l.roundness === "number" && Number.isFinite(l.roundness) ? l.roundness : s.roundness;
2108
+ s.forceDirection = l.forceDirection ?? s.forceDirection;
2109
+ }
2110
+ }
2111
+ return s;
2112
+ }
2113
+ /**
2114
+ * vis.js: an edge label's size under `scaling.label` and its turn along the line.
2115
+ * @param merged - the edge's merged layer
2116
+ * @param range - the edges' value range
2117
+ * @param a - the source node's center, or null
2118
+ * @param b - the target node's center, or null
2119
+ * @returns the values
2120
+ */
2121
+ function visEdgeLabel(merged, range, a, b) {
2122
+ const row = {};
2123
+ const size = visLabelScaled(merged, range);
2124
+ if (size !== null) {
2125
+ row.labelSize = size;
2126
+ }
2127
+ const turn = visLabelTurn(merged.fontAlign, a, b);
2128
+ if (turn !== null) {
2129
+ row.labelRotation = turn;
2130
+ }
2131
+ return row;
2132
+ }
2133
+ /**
2134
+ * vis.js: how far an edge label whose `font.align` is not "horizontal" is turned to run along its edge, kept readable
2135
+ * (vis-network 9.1.9 `Edge._getRotation`), in degrees clockwise on screen.
2136
+ * @param align - `font.align`
2137
+ * @param a - the source node's center, or null
2138
+ * @param b - the target node's center, or null
2139
+ * @returns the turn, or null for a level label
2140
+ */
2141
+ function visLabelTurn(align, a, b) {
2142
+ if (typeof align !== "string" || align === "horizontal" || a === null || b === null) {
2143
+ return null;
2144
+ }
2145
+ const [dx, dy] = [a[0] - b[0], a[1] - b[1]];
2146
+ let angle = Math.atan2(dy, dx);
2147
+ if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
2148
+ angle += Math.PI;
2149
+ }
2150
+ const degrees = Math.round(((angle * 180) / Math.PI) * 1e4) / 1e4;
2151
+ return degrees < 0 ? degrees + 360 : degrees;
2152
+ }
2153
+ /**
2154
+ * vis-network's control point of a static smooth curve (9.1.9 `BezierEdgeStatic._getViaCoordinates`), from the two
2155
+ * node centers.
2156
+ * @param from - the source node's center
2157
+ * @param to - the target node's center
2158
+ * @param smooth - the smooth options
2159
+ * @returns the control point
2160
+ */
2161
+ function visStaticVia(from, to, smooth) {
2162
+ const f = smooth.roundness;
2163
+ const [fx, fy] = from;
2164
+ const [tx, ty] = to;
2165
+ const dx = Math.abs(fx - tx);
2166
+ const dy = Math.abs(fy - ty);
2167
+ // the step a discrete, diagonalCross or continuous curve takes from its source, toward its target in x and in y
2168
+ // (away from it in y when level: `from.y >= to.y` steps up)
2169
+ const step = f * Math.max(dx, dy);
2170
+ const sx = step * (Math.sign(tx - fx) || 1);
2171
+ const sy = step * (Math.sign(ty - fy) || -1);
2172
+ // which way a cross, horizontal or vertical curve's point lies from the target, back toward the source
2173
+ const backX = (1 - f) * dx * (Math.sign(fx - tx) || 1);
2174
+ const backY = (1 - f) * dy * (Math.sign(fy - ty) || 1);
2175
+ switch (smooth.type) {
2176
+ case "discrete":
2177
+ return visDiscreteVia(from, [fx + sx, fy + sy], dx, dy, f);
2178
+ case "diagonalCross":
2179
+ return [fx + sx, fy + sy];
2180
+ case "straightCross":
2181
+ return dx <= dy ? [tx, ty + backY] : [tx + backX, ty];
2182
+ case "horizontal":
2183
+ return [tx + backX, fy];
2184
+ case "vertical":
2185
+ return [fx, ty + backY];
2186
+ case "curvedCW":
2187
+ return visCurvedVia(from, to, f, f * 0.5 + 0.5);
2188
+ case "curvedCCW":
2189
+ return visCurvedVia(from, to, f, -f * 0.5 + 0.5);
2190
+ default:
2191
+ // continuous: the diagonal step, no farther than the target along the longer axis
2192
+ return dx <= dy
2193
+ ? [fx + Math.sign(sx) * Math.min(step, dx), fy + sy]
2194
+ : [fx + sx, fy + Math.sign(sy) * Math.min(step, dy)];
2195
+ }
2196
+ }
2197
+ /**
2198
+ * vis-network's discrete curve: the diagonal step, kept at the source's x (or y) when the nodes are nearly in line.
2199
+ * @param from - the source node's center
2200
+ * @param diagonal - the diagonal step's point
2201
+ * @param dx - the distance between the nodes in x
2202
+ * @param dy - the distance in y
2203
+ * @param f - the roundness
2204
+ * @returns the control point
2205
+ */
2206
+ function visDiscreteVia(from, diagonal, dx, dy, f) {
2207
+ if (dx <= dy) {
2208
+ return [dx < f * dy ? from[0] : diagonal[0], diagonal[1]];
2209
+ }
2210
+ return [diagonal[0], dy < f * dx ? from[1] : diagonal[1]];
2211
+ }
2212
+ /**
2213
+ * vis-network's curvedCW and curvedCCW: a point `(roundness / 2 + 1 / 2)` of the distance from the source, turned
2214
+ * `turn` half turns from the line to the target (vis-network's sin / cos convention, y down).
2215
+ * @param from - the source node's center
2216
+ * @param to - the target node's center
2217
+ * @param f - the roundness
2218
+ * @param turn - the turn, in half turns
2219
+ * @returns the control point
2220
+ */
2221
+ function visCurvedVia(from, to, f, turn) {
2222
+ const ex = to[0] - from[0];
2223
+ const ey = from[1] - to[1];
2224
+ const r = (f * 0.5 + 0.5) * Math.hypot(ex, ey);
2225
+ const angle = (Math.atan2(ey, ex) + turn * Math.PI) % (2 * Math.PI);
2226
+ return [from[0] + r * Math.sin(angle), from[1] + r * Math.cos(angle)];
2227
+ }
2228
+ /**
2229
+ * vis-network's two control points of a cubicBezier curve (9.1.9 `CubicBezierEdge._getViaCoordinates`): each
2230
+ * `roundness` of the way across from its own end, horizontally when the nodes are farther apart in x or
2231
+ * `forceDirection` is horizontal (or true), else vertically.
2232
+ * @param from - the source node's center
2233
+ * @param to - the target node's center
2234
+ * @param smooth - the smooth options
2235
+ * @returns the two control points
2236
+ */
2237
+ function visCubicVia(from, to, smooth) {
2238
+ const [dx, dy] = [from[0] - to[0], from[1] - to[1]];
2239
+ const r = smooth.roundness;
2240
+ const force = smooth.forceDirection;
2241
+ const across = (Math.abs(dx) > Math.abs(dy) || force === true || force === "horizontal") && force !== "vertical";
2242
+ return across
2243
+ ? [
2244
+ [from[0] - r * dx, from[1]],
2245
+ [to[0] + r * dx, to[1]],
2246
+ ]
2247
+ : [
2248
+ [from[0], from[1] - r * dy],
2249
+ [to[0], to[1] + r * dy],
2250
+ ];
2251
+ }
2252
+ /**
2253
+ * A point as the neutral `bends` spell it: `along,across` relative to the line between two centers, y down in, y up
2254
+ * out.
2255
+ * @param a - the source center
2256
+ * @param b - the target center
2257
+ * @param p - the point
2258
+ * @returns `along,across`
2259
+ */
2260
+ function alongAcross(a, b, p) {
2261
+ const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
2262
+ const [qx, qy] = [p[0] - a[0], p[1] - a[1]];
2263
+ const l2 = dx * dx + dy * dy;
2264
+ return `${fraction((qx * dx + qy * dy) / l2)},${fraction((dy * qx - dx * qy) / l2)}`;
2265
+ }
2266
+ /**
2267
+ * A number rounded to four places for a bend, with no negative zero.
2268
+ * @param v - the number
2269
+ * @returns its text
2270
+ */
2271
+ const fraction = (v) => String(Math.round(v * 1e4) / 1e4 || 0);
2272
+ /**
2273
+ * vis.js: the route vis-network draws an edge along. A smooth edge is one Bezier curve through its control points
2274
+ * (`bendCurve: "bezier"`): a static type's one point, cubicBezier's two. A dynamic one (vis-network's default) is
2275
+ * drawn through a support node the physics simulation moves; where it never moves (physics off for the network or
2276
+ * the edge) it stays at the middle and the edge is straight. A self-loop is vis-network's circle of
2277
+ * `selfReference.size` (20) centered on its node's outline at `selfReference.angle` (pi / 4).
2278
+ * @param defaults - `options.edges`
2279
+ * @param own - the edge's own keys
2280
+ * @param s - its source node
2281
+ * @param t - its target node
2282
+ * @param g - the nodes' geometry
2283
+ * @returns the route's values, none for a straight edge or a route the file does not decide
2284
+ */
2285
+ function visRoute(defaults, own, s, t, g) {
2286
+ if (s === t) {
2287
+ return visLoop(defaults, own, g.box(s));
2288
+ }
2289
+ const smooth = visSmooth(defaults.smooth, own.smooth);
2290
+ if (smooth.enabled && smooth.type === "dynamic") {
2291
+ return visDynamic(defaults, own, g.support ?? null);
2292
+ }
2293
+ const [a, b] = [g.at(s), g.at(t)];
2294
+ if (!smooth.enabled || a === null || b === null || (a[0] === b[0] && a[1] === b[1])) {
2295
+ return {};
2296
+ }
2297
+ const controls = smooth.type === "cubicBezier" ? visCubicVia(a, b, smooth) : [visStaticVia(a, b, smooth)];
2298
+ return { curved: true, bendCurve: "bezier", bends: controls.map((p) => alongAcross(a, b, p)).join(" ") };
2299
+ }
2300
+ /**
2301
+ * vis.js: a dynamic smooth edge, drawn through the support node vis-network's physics places, wherever its nodes
2302
+ * are (they need no position in the file): its forces, with the edge's own `length` as its springs' rest length.
2303
+ * Straight where physics does not run for it (`physics: false` on the network or the edge), as the support node
2304
+ * then stays at the middle.
2305
+ * @param defaults - `options.edges`
2306
+ * @param own - the edge's own keys
2307
+ * @param support - the network's forces, or null
2308
+ * @returns the route's values
2309
+ */
2310
+ function visDynamic(defaults, own, support) {
2311
+ if (support === null || own.physics === false || (own.physics === undefined && defaults.physics === false)) {
2312
+ return {};
2313
+ }
2314
+ const length = [own.length, defaults.length].find((v) => typeof v === "number" && Number.isFinite(v) && v >= 0);
2315
+ return { curved: true, ...support, ...(length === undefined ? {} : { supportSpringLength: length }) };
2316
+ }
2317
+ /** vis-network 9.1.9's physics defaults for the two solvers whose forces a support node is drawn under. */
2318
+ const VIS_SOLVERS = {
2319
+ barnesHut: { model: "barnesHut", g: -2000, cg: 0.3, length: 95, k: 0.04 },
2320
+ forceAtlas2Based: { model: "forceAtlas2", g: -50, cg: 0.01, length: 100, k: 0.08 },
2321
+ };
2322
+ /**
2323
+ * vis.js: the forces a dynamic edge's support node settles under (9.1.9 BarnesHutSolver,
2324
+ * ForceAtlas2BasedRepulsionSolver, their central gravity solvers and SpringSolver). None where physics is off, where
2325
+ * a hierarchical layout turns every dynamic curve static (`_forceDisableDynamicCurves`), or under the repulsion and
2326
+ * hierarchicalRepulsion solvers (ponytail: drawn straight; their force laws when a file uses them).
2327
+ * @param physics - `options.physics`
2328
+ * @param layout - `options.layout`
2329
+ * @returns the neutral support values, or null
2330
+ */
2331
+ function visSupport(physics, layout) {
2332
+ const hierarchical = isJsonObject(layout) ? layout.hierarchical : undefined;
2333
+ if (hierarchical === true || (isJsonObject(hierarchical) && hierarchical.enabled !== false)) {
2334
+ return null;
2335
+ }
2336
+ if (physics === false || (isJsonObject(physics) && physics.enabled === false)) {
2337
+ return null;
2338
+ }
2339
+ const o = isJsonObject(physics) ? physics : {};
2340
+ const solver = typeof o.solver === "string" ? o.solver : "barnesHut";
2341
+ const base = VIS_SOLVERS[solver];
2342
+ if (base === undefined) {
2343
+ return null;
2344
+ }
2345
+ const given = isJsonObject(o[solver]) ? o[solver] : {};
2346
+ const num = (v, fallback) => (typeof v === "number" && Number.isFinite(v) ? v : fallback);
2347
+ return {
2348
+ supportModel: base.model,
2349
+ supportSpringLength: Math.max(0, num(given.springLength, base.length)),
2350
+ supportSpringConstant: Math.max(0, num(given.springConstant, base.k)),
2351
+ supportRepulsion: Math.max(0, -num(given.gravitationalConstant, base.g)),
2352
+ supportCentralGravity: Math.max(0, num(given.centralGravity, base.cg)),
2353
+ };
2354
+ }
2355
+ /**
2356
+ * vis.js: a self-loop, vis-network's circle (9.1.9 `EdgeBase._getCircleData`, `getSelfRefCoordinates`) of radius
2357
+ * `selfReference.size` (or the older `selfReferenceSize`; 20) centered on the node's outline (visToBorder) along
2358
+ * the ray at `selfReference.angle` (pi / 4, counterclockwise from the right). Written as bends at the corners of the
2359
+ * square round the circle, drawn through their midpoints, which run along the circle.
2360
+ * @param defaults - `options.edges`
2361
+ * @param own - the edge's own keys
2362
+ * @param box - the node's half width, half height, border width and shape, or null when unknown
2363
+ * @returns the loop's values, none for a node whose size is not known here
2364
+ */
2365
+ function visLoop(defaults, own, box) {
2366
+ if (box === null) {
2367
+ return {};
2368
+ }
2369
+ const field = (layer, key) => isJsonObject(layer.selfReference) ? layer.selfReference[key] : undefined;
2370
+ const pick = (...vs) => vs.find((v) => typeof v === "number" && Number.isFinite(v)) ?? null;
2371
+ const radius = pick(field(own, "size"), own.selfReferenceSize, field(defaults, "size"), defaults.selfReferenceSize) ?? 20;
2372
+ const angle = pick(field(own, "angle"), field(defaults, "angle")) ?? Math.PI / 4;
2373
+ const [hw, hh] = box;
2374
+ const reach = visToBorder(box, angle);
2375
+ // the circle's center relative to the node's, y up
2376
+ const [cx, cy] = [Math.cos(angle) * reach, Math.sin(angle) * reach];
2377
+ const behind = [field(own, "renderBehindTheNode"), field(defaults, "renderBehindTheNode")].find((v) => typeof v === "boolean");
2378
+ if (behind === false) {
2379
+ return visFrontArc(cx, cy, radius, angle, box);
2380
+ }
2381
+ // the node's farthest reach from its center: its radius when round, its corner otherwise
2382
+ const round = box[3] === "dot" || box[3] === "circle" || box[3] === "circularImage";
2383
+ if (reach + (round ? hw : Math.hypot(hw, hh)) < radius) {
2384
+ return visRing(cx, cy, radius, angle, box);
2385
+ }
2386
+ // the corners of the square round the circle, the first the one facing the node and again last, so the curve
2387
+ // through their midpoints runs the whole circle and its ends, out of and back into the node, bulge as it does
2388
+ const corners = [];
2389
+ for (let k = 0; k <= 4; k++) {
2390
+ const a = angle + Math.PI + ((k % 4) * Math.PI) / 2;
2391
+ const [x, y] = [cx + Math.SQRT2 * radius * Math.cos(a), cy + Math.SQRT2 * radius * Math.sin(a)];
2392
+ corners.push(`${fraction(x / hw)},${fraction(y / hh)}`);
2393
+ }
2394
+ return { curved: true, bendCurve: "midpoints", bends: corners.join(" ") };
2395
+ }
2396
+ /** How many points a vis-network self-loop's circle is written through where it does not meet its node. */
2397
+ const VIS_RING_STEPS = 96;
2398
+ /**
2399
+ * vis.js: a self-loop whose circle holds its whole node, so meets it nowhere (a small node, or a large
2400
+ * `selfReference.size`): the circle alone, written as points round it from the side facing the node and back to it,
2401
+ * the first and the last the same point, which graphty draws as that closed ring with no line to the node.
2402
+ * @param cx - the circle's center across from the node's, y up
2403
+ * @param cy - and up from it
2404
+ * @param radius - its radius
2405
+ * @param angle - the ray its center is on, counterclockwise from the right
2406
+ * @param box - the node's half width and half height
2407
+ * @returns the loop's values
2408
+ */
2409
+ function visRing(cx, cy, radius, angle, box) {
2410
+ const [hw, hh] = box;
2411
+ const points = [];
2412
+ for (let k = 0; k <= VIS_RING_STEPS; k++) {
2413
+ const a = angle + Math.PI + (2 * Math.PI * (k % VIS_RING_STEPS)) / VIS_RING_STEPS;
2414
+ points.push(`${fraction((cx + radius * Math.cos(a)) / hw)},${fraction((cy + radius * Math.sin(a)) / hh)}`);
2415
+ }
2416
+ return { curved: false, bends: points.join(" ") };
2417
+ }
2418
+ /**
2419
+ * vis.js: a self-loop drawn in front of its node (`selfReference.renderBehindTheNode: false`): only the arc of its
2420
+ * circle between the two points vis-network 9.1.9 searches for (Edge._circle, `_findBorderPositionCircle`: ten
2421
+ * halvings of a "position" between the angle and the angle plus pi, read as a fraction of a turn), clockwise on screen
2422
+ * from the first to the second, written as points along it.
2423
+ * @param cx - the circle's center across from the node's, y up
2424
+ * @param cy - and up from it
2425
+ * @param radius - its radius
2426
+ * @param angle - `selfReference.angle`
2427
+ * @param box - the node's half width, half height, border width and shape
2428
+ * @returns the loop's values
2429
+ */
2430
+ function visFrontArc(cx, cy, radius, angle, box) {
2431
+ // on screen, y down, the node's center at the origin
2432
+ const [x, y] = [cx, -cy];
2433
+ const search = (direction) => {
2434
+ let [low, high] = [angle, angle + Math.PI];
2435
+ let pos = [x, y];
2436
+ for (let iteration = 0; low <= high && iteration < 10; iteration++) {
2437
+ const middle = (low + high) * 0.5;
2438
+ const turn = middle * 2 * Math.PI;
2439
+ pos = [x + radius * Math.cos(turn), y - radius * Math.sin(turn)];
2440
+ const difference = visToBorder(box, Math.atan2(-pos[1], -pos[0])) - Math.hypot(pos[0], pos[1]);
2441
+ if (Math.abs(difference) < 0.05) {
2442
+ break;
2443
+ }
2444
+ if (difference > 0 === direction > 0) {
2445
+ low = middle;
2446
+ }
2447
+ else {
2448
+ high = middle;
2449
+ }
2450
+ }
2451
+ return pos;
2452
+ };
2453
+ const [from, to] = [search(-1), search(1)];
2454
+ const start = Math.atan2(from[1] - y, from[0] - x);
2455
+ let end = Math.atan2(to[1] - y, to[0] - x);
2456
+ if (end < start) {
2457
+ end += 2 * Math.PI;
2458
+ }
2459
+ const [hw, hh] = box;
2460
+ const steps = Math.max(2, Math.ceil(((end - start) / (2 * Math.PI)) * VIS_RING_STEPS));
2461
+ const points = [];
2462
+ for (let k = 0; k <= steps; k++) {
2463
+ const a = start + ((end - start) * k) / steps;
2464
+ points.push(`${fraction((x + radius * Math.cos(a)) / hw)},${fraction(-(y + radius * Math.sin(a)) / hh)}`);
2465
+ }
2466
+ // the arc's two ends are where vis-network starts and stops it, off the node as often as on it: ports, so no line
2467
+ // is drawn from the node out to them
2468
+ return {
2469
+ curved: false,
2470
+ sourcePort: points[0],
2471
+ bends: points.slice(1, -1).join(" "),
2472
+ targetPort: points.at(-1) ?? points[0],
2473
+ };
2474
+ }
2475
+ /**
2476
+ * vis-network 9.1.9's `distanceToBorder` of a node, how far from its center its outline is along a ray: a dot its
2477
+ * size, a circle or circularImage half its width, an ellipse its ellipse, all with no border; every other shape its
2478
+ * box plus its border width (NodeBase._distanceToBorder, Box).
2479
+ * @param box - the node's half width, half height, border width and shape
2480
+ * @param angle - the ray, counterclockwise from the right
2481
+ * @returns the distance
2482
+ */
2483
+ function visToBorder(box, angle) {
2484
+ const [hw, hh, bw, shape] = box;
2485
+ if (shape === "dot" || shape === "circle" || shape === "circularImage") {
2486
+ return hw;
2487
+ }
2488
+ if (shape === "ellipse") {
2489
+ return (hw * hh) / Math.hypot(Math.sin(angle) * hw, Math.cos(angle) * hh);
2490
+ }
2491
+ return Math.min(Math.abs(hw / Math.cos(angle)), Math.abs(hh / Math.sin(angle))) + bw;
2492
+ }
2493
+ /** The vis.js keys the reader reads, per target. */
2494
+ const VIS_KEYS = {
2495
+ node: [
2496
+ "color",
2497
+ "shape",
2498
+ "size",
2499
+ "value",
2500
+ "scaling",
2501
+ "borderWidth",
2502
+ "opacity",
2503
+ "hidden",
2504
+ "label",
2505
+ "title",
2506
+ "font",
2507
+ "margin",
2508
+ "widthConstraint",
2509
+ "heightConstraint",
2510
+ "image",
2511
+ "brokenImage",
2512
+ "imagePadding",
2513
+ "icon",
2514
+ "shapeProperties",
2515
+ ],
2516
+ edge: [
2517
+ "color",
2518
+ "width",
2519
+ "value",
2520
+ "scaling",
2521
+ "dashes",
2522
+ "arrows",
2523
+ "hidden",
2524
+ "label",
2525
+ "title",
2526
+ "font",
2527
+ "background",
2528
+ "smooth",
2529
+ "selfReference",
2530
+ "selfReferenceSize",
2531
+ "physics",
2532
+ "length",
2533
+ "widthConstraint",
2534
+ ],
2535
+ };
959
2536
  /**
960
2537
  * Each element's own vis.js keys as a layer, read from the graph's columns.
961
2538
  * @param snapshot - the graph
@@ -963,6 +2540,16 @@ function nodeRecords(snapshot) {
963
2540
  * @returns the layer of an element
964
2541
  */
965
2542
  function elementRecords(snapshot, target) {
2543
+ const raw = elementRaw(snapshot, target);
2544
+ return (i) => visLayer(raw(i), target);
2545
+ }
2546
+ /**
2547
+ * Each element's own vis.js keys as the file gives them, read from the graph's columns.
2548
+ * @param snapshot - the graph
2549
+ * @param target - nodes or edges
2550
+ * @returns the keys of an element
2551
+ */
2552
+ function elementRaw(snapshot, target) {
966
2553
  const readers = VIS_KEYS[target]
967
2554
  .map((name) => [name, cells(snapshot, target, name)])
968
2555
  .filter((x) => x[1] !== null);
@@ -974,7 +2561,7 @@ function elementRecords(snapshot, target) {
974
2561
  record[name] = v;
975
2562
  }
976
2563
  }
977
- return visLayer(record, target);
2564
+ return record;
978
2565
  };
979
2566
  }
980
2567
  /**
@@ -1067,10 +2654,88 @@ const visSize = (want) => {
1067
2654
  const shape = typeof s === "string" ? visShape(s)[0] : "ellipse";
1068
2655
  return VIS_SIZED.has(shape) ? oneSize(want) : null;
1069
2656
  };
2657
+ /**
2658
+ * The vis.js shape a written node is read back as.
2659
+ * @param want - the block's values of the element
2660
+ * @returns the shape
2661
+ */
2662
+ const visShapeBack = (want) => {
2663
+ const s = want("shape");
2664
+ return typeof s === "string" ? visShape(s)[0] : "ellipse";
2665
+ };
2666
+ /**
2667
+ * What a written vis.js node of a shape that changes what its size gives back reads as (visSizedShape, visFitShape):
2668
+ * a hexagon's height and how far below its square its label hangs, a database's label at its top.
2669
+ * @param property - the fact
2670
+ * @returns the reading back of the element's values, null where the shape changes nothing
2671
+ */
2672
+ const visShapeFactBack = (property) => (want) => {
2673
+ const shape = visShapeBack(want);
2674
+ const size = visSize(want);
2675
+ if (shape === "hexagon" && typeof size === "number") {
2676
+ return property === "height" ? (Math.sqrt(3) * size) / 2 : ((2 - Math.sqrt(3)) * size) / 4;
2677
+ }
2678
+ return property === "labelOffsetY" && shape === "database" ? VIS_MARGIN : null;
2679
+ };
2680
+ /**
2681
+ * What the reader reads back for a fact of a node the writer wrote in a shape vis-network sizes by its label: the
2682
+ * shape's own growth, from the label and font size written.
2683
+ * @param property - the fact
2684
+ * @returns the reading back of the element's values
2685
+ */
2686
+ const visFitBack = (property) => (want) => {
2687
+ const s = want("shape");
2688
+ const shape = typeof s === "string" ? visShape(s)[0] : "ellipse";
2689
+ return visLabelFit({ fontSize: want("labelSize"), label: want("label") }, shape)[property] ?? null;
2690
+ };
2691
+ /**
2692
+ * What reading a file graphty wrote in vis.js gives back: VIS_WRITTEN, and for a self-loop the route the reader draws
2693
+ * it along (vis-network's circle, made from vis-network's own defaults and the node's size, never from the block).
2694
+ * @param snapshot - the graph
2695
+ * @param edgeValues - the edge values the reader read
2696
+ * @returns the readings back
2697
+ */
2698
+ function visWritten(snapshot, edgeValues) {
2699
+ const isLoop = (i) => snapshot.edgeSource(i) === snapshot.edgeTarget(i);
2700
+ const loop = (property) => (want, i) => isLoop(i) ? (edgeValues[property]?.[i] ?? null) : want(property);
2701
+ // the writer writes no `smooth` and no `options.physics`, so every other edge reads back as vis-network's default,
2702
+ // a dynamic curve under the default barnesHut physics
2703
+ const dynamic = visSupport(undefined, undefined) ?? {};
2704
+ const support = (property) => (_want, i) => isLoop(i) ? (edgeValues[property]?.[i] ?? null) : (dynamic[property] ?? null);
2705
+ const supports = Object.fromEntries(Object.keys(dynamic).map((property) => [property, support(property)]));
2706
+ // an edge's own `length`, kept as data, is its springs' rest length again
2707
+ const lengths = cells(snapshot, "edge", "length");
2708
+ supports.supportSpringLength = (want, i) => {
2709
+ const own = lengths?.(i);
2710
+ return typeof own === "number" && Number.isFinite(own) && own >= 0 && !isLoop(i)
2711
+ ? own
2712
+ : support("supportSpringLength")(want, i);
2713
+ };
2714
+ return {
2715
+ node: VIS_WRITTEN.node,
2716
+ edge: {
2717
+ ...VIS_WRITTEN.edge,
2718
+ ...supports,
2719
+ bends: loop("bends"),
2720
+ bendCurve: loop("bendCurve"),
2721
+ curved: (_want, i) => (isLoop(i) ? (edgeValues.curved?.[i] ?? null) : true),
2722
+ },
2723
+ };
2724
+ }
1070
2725
  const VIS_WRITTEN = {
1071
2726
  node: {
1072
- width: visSize,
1073
- height: visSize,
2727
+ width: (want) => visSize(want) ?? visFitBack("width")(want),
2728
+ height: (want) => visShapeFactBack("height")(want) ?? visSize(want) ?? visFitBack("height")(want),
2729
+ labelOffsetY: visShapeFactBack("labelOffsetY"),
2730
+ labelFit: visFitBack("labelFit"),
2731
+ labelPaddingX: visFitBack("labelPaddingX"),
2732
+ labelPaddingY: visFitBack("labelPaddingY"),
2733
+ labelFitSquare: visFitBack("labelFitSquare"),
2734
+ // the writer writes hex colors, whose alpha vis-network's node opacity replaces, the border's too
2735
+ borderOpacity: (want) => {
2736
+ const opacity = want("opacity");
2737
+ return typeof opacity === "number" ? opacity : want("borderOpacity");
2738
+ },
1074
2739
  shape: (want) => {
1075
2740
  const s = want("shape");
1076
2741
  if (typeof s !== "string") {
@@ -1079,6 +2744,17 @@ const VIS_WRITTEN = {
1079
2744
  const site = { notes: new StyleNotes(), target: "node", property: "shape" };
1080
2745
  return canonEnum("shape", visShape(s)[0], "vis", site);
1081
2746
  },
2747
+ labelPosition: (want) => {
2748
+ const s = want("shape");
2749
+ if (typeof s !== "string") {
2750
+ return null;
2751
+ }
2752
+ const shape = visShape(s)[0];
2753
+ if (shape === "database") {
2754
+ return "inside-top";
2755
+ }
2756
+ return VIS_SIZED.has(shape) ? "bottom" : "center";
2757
+ },
1082
2758
  },
1083
2759
  edge: {
1084
2760
  lineStyle: (want) => {
@@ -1086,7 +2762,12 @@ const VIS_WRITTEN = {
1086
2762
  if (s === null) {
1087
2763
  return null;
1088
2764
  }
1089
- return s === "solid" ? "solid" : "dashed";
2765
+ // written as `dashes: true`, vis-network's [5, 5], which a wide edge's round caps close up
2766
+ if (s === "solid") {
2767
+ return "solid";
2768
+ }
2769
+ const width = want("width");
2770
+ return visDashStyle([5, 5], typeof width === "number" ? width : 1, true);
1090
2771
  },
1091
2772
  targetArrow: (want) => arrowBack(want("targetArrow")),
1092
2773
  sourceArrow: (want) => arrowBack(want("sourceArrow")),
@@ -1134,8 +2815,8 @@ export const NATIVE = {
1134
2815
  edge: cyjsProperties(CYJS_EDGE_PROPERTIES, ["curved", "labelItalic", "visible"]),
1135
2816
  },
1136
2817
  graphology: {
1137
- node: ["fill", "fillOpacity", "width", "height", "label", "visible"],
1138
- edge: ["color", "opacity", "width", "label", "visible"],
2818
+ node: ["fill", "fillOpacity", "width", "height", "label"],
2819
+ edge: ["color", "opacity", "width", "label"],
1139
2820
  },
1140
2821
  vis: {
1141
2822
  node: [
@@ -1639,9 +3320,11 @@ function writeGraphology(out, input, approximated, replaced) {
1639
3320
  const colors = p(target, fill);
1640
3321
  const alphas = p(target, opacity);
1641
3322
  writeKey(table, "color", colors.map((c, i) => cssColor(c, alphas[i])), replaced);
1642
- writeKey(table, "size", target === "node" ? nodeSizes(p("node", "width"), p("node", "height"), approximated) : p("edge", "width"), replaced);
3323
+ writeKey(table, "size", target === "node"
3324
+ ? nodeSizes(p("node", "width"), p("node", "height"), approximated).map((d) => d === null ? null : d / 2)
3325
+ : // sigma draws an edge half as wide as its size (readSigmaRow)
3326
+ p("edge", "width").map((w) => (typeof w === "number" ? w * 2 : w)), replaced);
1643
3327
  writeKey(table, "label", p(target, "label"), replaced);
1644
- writeKey(table, "hidden", p(target, "visible").map((v) => (v === false ? true : null)), replaced);
1645
3328
  }
1646
3329
  }
1647
3330
  /**
@@ -1651,6 +3334,21 @@ function writeGraphology(out, input, approximated, replaced) {
1651
3334
  * @param approximated - where approximations are counted
1652
3335
  * @param replaced - where replaced data values are counted
1653
3336
  */
3337
+ /**
3338
+ * vis.js: a scaling range of each written size for the elements with a `value`, which vis-network would otherwise
3339
+ * map onto `scaling` instead of drawing the size written (a node's radius, an edge's width).
3340
+ * @param table - the node or edge table being written
3341
+ * @param sizes - each element's written size, or null
3342
+ * @param replaced - the data values replaced so far
3343
+ */
3344
+ function keepScaled(table, sizes, replaced) {
3345
+ const value = table.get("value");
3346
+ if (value === null) {
3347
+ return;
3348
+ }
3349
+ const scaling = sizes.map((v, i) => typeof v !== "number" || typeof plainCell(value.value(i)) !== "number" ? null : { min: v, max: v });
3350
+ writeKey(table, "scaling", scaling, replaced);
3351
+ }
1654
3352
  function writeVis(out, input, approximated, replaced) {
1655
3353
  const p = (target, property) => input.picture(target, property);
1656
3354
  const font = (target) => {
@@ -1693,7 +3391,9 @@ function writeVis(out, input, approximated, replaced) {
1693
3391
  }), replaced);
1694
3392
  const sizes = nodeSizes(p("node", "width"), p("node", "height"), approximated);
1695
3393
  const shapes = p("node", "shape").map((s, i) => {
1696
- const [shape, exact] = typeof s === "string" ? visShape(s) : [null, true];
3394
+ const [written, exact] = typeof s === "string" ? visShape(s) : [null, true];
3395
+ // an ellipse with no size of its own is vis-network's label-sized `ellipse`, one with a size its `dot`
3396
+ const shape = written === "dot" && s === "ellipse" && sizes[i] === null ? "ellipse" : written;
1697
3397
  if (!exact) {
1698
3398
  approximated.add("node", "shape", s);
1699
3399
  }
@@ -1703,7 +3403,11 @@ function writeVis(out, input, approximated, replaced) {
1703
3403
  return shape;
1704
3404
  });
1705
3405
  writeKey(out.nodes, "shape", shapes, replaced);
1706
- writeKey(out.nodes, "size", sizes, replaced);
3406
+ // vis-network draws `size` as a radius
3407
+ const radii = sizes.map((d) => (d === null ? null : d / 2));
3408
+ writeKey(out.nodes, "size", radii, replaced);
3409
+ // a node's `value` would scale its size again on reading: a scaling range of its own radius keeps it
3410
+ keepScaled(out.nodes, radii, replaced);
1707
3411
  writeKey(out.nodes, "borderWidth", p("node", "borderWidth"), replaced);
1708
3412
  writeKey(out.nodes, "opacity", p("node", "opacity"), replaced);
1709
3413
  label(out.nodes, "node");
@@ -1719,7 +3423,10 @@ function writeVis(out, input, approximated, replaced) {
1719
3423
  }
1720
3424
  return Object.keys(c).length === 0 ? null : c;
1721
3425
  }), replaced);
1722
- writeKey(out.edges, "width", p("edge", "width"), replaced);
3426
+ const widths = p("edge", "width");
3427
+ writeKey(out.edges, "width", widths, replaced);
3428
+ // an edge's `value` would scale its width again on reading: a scaling range of its own width keeps it
3429
+ keepScaled(out.edges, widths, replaced);
1723
3430
  writeKey(out.edges, "dashes", p("edge", "lineStyle").map((s) => {
1724
3431
  if (typeof s !== "string") {
1725
3432
  return null;