@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,
@@ -36,11 +36,12 @@ import {
36
36
  import { CYJS_EDGE_PROPERTIES, CYJS_NODE_PROPERTIES } from "../../styles/cyjs/mappers.js";
37
37
  import { toolDefaults } from "../../styles/defaults.js";
38
38
  import { StyleApproximations } from "../../styles/io/approximations.js";
39
- import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
39
+ import { parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
40
+ import { takeFromBlock } from "../../styles/io/round-trip.js";
40
41
  import type { ExporterStyleInput } from "../../styles/io/writer.js";
41
42
  import { StyleNotes } from "../../styles/notes.js";
42
43
  import { styleFingerprint } from "../../styles/plan/features.js";
43
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
44
+ import { plainCell } from "../../styles/plan/resolve.js";
44
45
  import type {
45
46
  ColumnRef,
46
47
  EdgeStyleProperty,
@@ -81,10 +82,23 @@ export interface JsonStyleSource {
81
82
  readonly groups?: unknown;
82
83
  /** graphology: whether the document is Gephi's JSON export (`id` instead of `key`). */
83
84
  readonly gephi?: boolean;
85
+ /** graphology: the sigma.js settings the drawing uses (the `sigmaSettings` import option). */
86
+ readonly sigma?: SigmaSettings;
84
87
  /** node-link: whether the document has NetworkX's `directed`, `multigraph` and `graph` keys. */
85
88
  readonly networkx?: boolean;
86
89
  }
87
90
 
91
+ /** The sigma.js settings that change what graph-io reads from a graphology document; the rest are the application's. */
92
+ export interface SigmaSettings {
93
+ /** Whether sigma draws edge labels (`renderEdgeLabels`, off by default). */
94
+ readonly renderEdgeLabels: boolean;
95
+ /** Whose edge labels it draws (`edgeLabelAnchors`): between two labeled nodes ("nodeLabels", the default), or every edge's. */
96
+ readonly edgeLabelAnchors: "nodeLabels" | "allNodes";
97
+ }
98
+
99
+ /** sigma.js's own settings. */
100
+ export const SIGMA_DEFAULT_SETTINGS: SigmaSettings = { renderEdgeLabels: false, edgeLabelAnchors: "nodeLabels" };
101
+
88
102
  /** The writable values of one row. */
89
103
  type Row = Record<string, StyleValue | null>;
90
104
  /** Per-property values of one target, by element index. */
@@ -316,14 +330,17 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
316
330
  const notes = new StyleNotes();
317
331
  const out: Record<StyleTarget, Values> = { node: {}, edge: {} };
318
332
  const readFrom: Record<StyleTarget, string[]> = { node: [], edge: [] };
333
+ const edgeLabel = sigmaEdgeLabels(snapshot, src.sigma ?? SIGMA_DEFAULT_SETTINGS);
319
334
  for (const target of TARGETS) {
320
335
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
321
336
  const { values, put } = valuesOf(n);
322
337
  const keys: SigmaKeys = {
323
338
  color: cells(snapshot, target, "color"),
324
339
  size: cells(snapshot, target, "size"),
340
+ // sigma 4's default styles bind nothing else: it draws an element marked `hidden: true` (sigma 3's key)
341
+ // or `visibility: "hidden"` (its own display-data name) like any other (specimen sheets
342
+ // graphology-specimen-node-sizes-opacity, -edges-lines)
325
343
  label: cells(snapshot, target, "label"),
326
- hidden: cells(snapshot, target, "hidden"),
327
344
  };
328
345
  readFrom[target].push(...Object.entries(keys).flatMap(([name, c]) => (c === null ? [] : [name])));
329
346
  for (let i = 0; i < n; i++) {
@@ -334,7 +351,7 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
334
351
  element: i,
335
352
  budget: context.budget,
336
353
  });
337
- readSigmaRow(keys, target, i, put, site);
354
+ readSigmaRow(keys, target, target === "node" || edgeLabel(i), i, put, site);
338
355
  }
339
356
  out[target] = values;
340
357
  }
@@ -352,18 +369,42 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
352
369
  };
353
370
  }
354
371
 
372
+ /**
373
+ * Which edges sigma.js draws the label of: none unless `renderEdgeLabels` is on; then, under `edgeLabelAnchors`
374
+ * "nodeLabels", only an edge whose two end nodes show a label.
375
+ * @param snapshot - the graph
376
+ * @param settings - sigma's settings
377
+ * @returns whether edge `i`'s label is drawn
378
+ */
379
+ function sigmaEdgeLabels(snapshot: GraphSnapshot, settings: SigmaSettings): (i: number) => boolean {
380
+ if (!settings.renderEdgeLabels) {
381
+ return () => false;
382
+ }
383
+ if (settings.edgeLabelAnchors === "allNodes") {
384
+ return () => true;
385
+ }
386
+ // ponytail: a node "shows" its label when it has one; sigma also hides labels by its label grid and
387
+ // labelRenderedSizeThreshold, which depend on its framing. Model those once graphty frames a sigma file as sigma does.
388
+ const label = cells(snapshot, "node", "label");
389
+ const shows = (node: number): boolean => {
390
+ const text = label?.(node);
391
+ return typeof text === "string" ? text !== "" : text !== null && text !== undefined;
392
+ };
393
+ return (i) => shows(snapshot.edgeSource(i)) && shows(snapshot.edgeTarget(i));
394
+ }
395
+
355
396
  /** The cell readers of the keys sigma.js draws, null for a key the graph does not have. */
356
397
  interface SigmaKeys {
357
398
  readonly color: ((i: number) => unknown) | null;
358
399
  readonly size: ((i: number) => unknown) | null;
359
400
  readonly label: ((i: number) => unknown) | null;
360
- readonly hidden: ((i: number) => unknown) | null;
361
401
  }
362
402
 
363
403
  /**
364
404
  * Reads one element's sigma.js keys.
365
405
  * @param keys - the cell readers
366
406
  * @param target - nodes or edges
407
+ * @param labeled - whether sigma draws the element's label
367
408
  * @param i - the element
368
409
  * @param put - the setter
369
410
  * @param site - where a value of a property was read
@@ -371,32 +412,61 @@ interface SigmaKeys {
371
412
  function readSigmaRow(
372
413
  keys: SigmaKeys,
373
414
  target: StyleTarget,
415
+ labeled: boolean,
374
416
  i: number,
375
417
  put: (p: string, i: number, v: StyleValue | null) => void,
376
418
  site: (property: string) => CanonSite,
377
419
  ): void {
378
- const { color, size, label, hidden } = keys;
420
+ const { color, size, label } = keys;
379
421
  if (color !== null) {
380
422
  const [fill, opacity] = target === "node" ? ["fill", "fillOpacity"] : ["color", "opacity"];
381
423
  putColor(put, fill, opacity, i, color(i), site(fill));
382
424
  }
383
425
  if (size !== null) {
384
- const v = canonNumber(size(i), site("width"));
385
- put("width", i, v);
386
- if (target === "node") {
387
- put("height", i, v);
388
- }
426
+ putSigmaSize(put, target, i, size(i), site);
389
427
  }
390
- if (label !== null) {
391
- put("label", i, canonText(label(i), site("label")));
428
+ // an edge's label is drawn only under sigma's renderEdgeLabels setting, which a document never carries (the
429
+ // `sigmaSettings` import option); otherwise it is data (graphology-specimen-edge-labels, -p2, -p3)
430
+ if (label !== null && labeled) {
431
+ const text = canonText(label(i), site("label"));
432
+ // sigma draws a label with the canvas's fillText, which draws each ASCII whitespace as a space: one line
433
+ // (graphology-specimen-labels, label-multiline)
434
+ put("label", i, text === null ? null : text.replaceAll(/[\t\n\f\r]/g, " "));
392
435
  }
393
- if (hidden !== null && canonBoolean(hidden(i), site("visible")) === true) {
394
- put("visible", i, false);
436
+ }
437
+
438
+ /**
439
+ * One element's sigma.js size, as its width (and a node's height).
440
+ * @param put - the setter
441
+ * @param target - nodes or edges
442
+ * @param i - the element
443
+ * @param raw - the size value
444
+ * @param site - where a value of a property was read
445
+ */
446
+ function putSigmaSize(
447
+ put: (p: string, i: number, v: StyleValue | null) => void,
448
+ target: StyleTarget,
449
+ i: number,
450
+ raw: unknown,
451
+ site: (property: string) => CanonSite,
452
+ ): void {
453
+ if (target === "node") {
454
+ // sigma draws a node's size as its radius, and a node of negative size not at all
455
+ // (graphology-specimen-node-sizes-opacity, size-negative)
456
+ const v = Number(raw) < 0 ? 0 : canonNumber(raw, site("width"));
457
+ const d = v === null ? null : v * 2;
458
+ put("width", i, d);
459
+ put("height", i, d);
460
+ } else {
461
+ // sigma's edge shader draws an edge half as wide as a node of the same size is in radius: size 6 draws
462
+ // 3 px where a node of size 6 is 5.8 px in radius (graphology-specimen-arrows edge-size-6)
463
+ const w = canonNumber(raw, site("width"));
464
+ put("width", i, w === null ? null : w / 2);
395
465
  }
396
466
  }
397
467
 
398
468
  /**
399
- * A CSS color into a property and its opacity.
469
+ * A color as sigma.js reads it, into a property and its opacity.
400
470
  * @param put - the setter
401
471
  * @param property - the color property
402
472
  * @param opacity - its opacity property, or null when it has none
@@ -412,10 +482,11 @@ function putColor(
412
482
  raw: unknown,
413
483
  site: CanonSite,
414
484
  ): void {
415
- if (raw === null || raw === undefined || raw === "") {
485
+ // sigma draws a missing color with its default, and any string it cannot read ("" too) black
486
+ if (raw === null || raw === undefined) {
416
487
  return;
417
488
  }
418
- const c = canonColor(raw, "css", site);
489
+ const c = canonColor(raw, "graphology", site);
419
490
  if (c === null) {
420
491
  return;
421
492
  }
@@ -475,25 +546,8 @@ function exactFromBlock(
475
546
  written: Written,
476
547
  holdsExactly?: (target: StyleTarget, property: string, value: StyleValue) => boolean,
477
548
  ): void {
478
- if (text === null) {
479
- return;
480
- }
481
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
482
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "json") : null;
483
- if (ex === null) {
484
- return;
485
- }
486
- for (const target of TARGETS) {
549
+ takeFromBlock(snapshot, text, "json", (target, block, ex) => {
487
550
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
488
- const cache = new Map<string, readonly (StyleValue | null)[]>();
489
- const resolved = (p: string): readonly (StyleValue | null)[] => {
490
- let r = cache.get(p);
491
- if (r === undefined) {
492
- r = resolveStyles(ex, target, p as never, snapshot);
493
- cache.set(p, r);
494
- }
495
- return r;
496
- };
497
551
  if (holdsExactly !== undefined) {
498
552
  const properties = new Set(
499
553
  ex.styles.rules
@@ -502,42 +556,19 @@ function exactFromBlock(
502
556
  );
503
557
  for (const property of properties) {
504
558
  const held = (w: StyleValue): boolean => holdsExactly(target, property, w);
505
- takeUnheld(values[target], property, resolved(property), n, held);
559
+ takeUnheld(values[target], property, block(target, property), n, held);
506
560
  }
507
561
  }
508
- for (const [property, write] of Object.entries(written[target] ?? {})) {
509
- takeWritten(values[target], property, n, resolved, write, snapshot);
510
- }
511
- }
512
- }
513
-
514
- /**
515
- * Takes the block's value of a property wherever the native value is what the writer writes for the block's values.
516
- * @param column - the native values of the target, changed in place
517
- * @param property - the property
518
- * @param n - the element count
519
- * @param resolved - the block's values of a property
520
- * @param write - what the writer writes for an element, from the block's values
521
- * @param snapshot - the graph
522
- */
523
- function takeWritten(
524
- column: Values,
525
- property: string,
526
- n: number,
527
- resolved: (p: string) => readonly (StyleValue | null)[],
528
- write: (want: (p: string) => StyleValue | null, i: number, snapshot: GraphSnapshot) => StyleValue | null,
529
- snapshot: GraphSnapshot,
530
- ): void {
531
- const want = resolved(property);
532
- for (let i = 0; i < n; i++) {
533
- const w = want[i];
534
- const native = column[property]?.[i] ?? null;
535
- // a native value the writer made from the block's other values (vis.js's one size for a width) is the
536
- // block's own value, none included
537
- if (native !== w && native !== null && native === write((p) => resolved(p)[i], i, snapshot)) {
538
- setValue(column, property, n, i, w);
539
- }
540
- }
562
+ const writes = written[target] ?? {};
563
+ return {
564
+ values: values[target],
565
+ properties: Object.keys(writes),
566
+ // a native value the writer made from the block's other values (vis.js's one size for a width) is the
567
+ // block's own value, none included
568
+ readBack: (i, want) => (property) => writes[property](want, i, snapshot),
569
+ take: (_property, _wanted, native) => native !== null,
570
+ };
571
+ });
541
572
  }
542
573
 
543
574
  /**
@@ -592,6 +623,19 @@ const VIS_SIZED: ReadonlySet<string> = new Set([
592
623
  "icon",
593
624
  ]);
594
625
 
626
+ /** vis-network's node shapes sized by their label, as the `labelFit` facts of each (vis-network 9.1.9 `resize`). */
627
+ const VIS_LABEL_FIT: ReadonlyMap<string, { readonly square: boolean; readonly ellipse: boolean }> = new Map([
628
+ ["box", { square: false, ellipse: false }],
629
+ ["text", { square: false, ellipse: false }],
630
+ ["circle", { square: true, ellipse: false }],
631
+ ["database", { square: true, ellipse: false }],
632
+ ["ellipse", { square: false, ellipse: true }],
633
+ ]);
634
+
635
+ /** vis-network's default `margin` and font size (vis-network.esm.js NodesHandler defaults). */
636
+ const VIS_MARGIN = 5;
637
+ const VIS_FONT_SIZE = 14;
638
+
595
639
  /**
596
640
  * vis-network's palette for a group the file does not define, in the order groups first appear (vis-network
597
641
  * 10.1.2, `Groups._defaultGroups`): border, background.
@@ -619,6 +663,10 @@ const VIS_GROUP_PALETTE: readonly (readonly [string, string])[] = [
619
663
  ["#EE0000", "#990000"],
620
664
  ];
621
665
 
666
+ /** vis-network's node colors where nothing sets them (vis-network 9.1.9 `NodesHandler` defaults `color`). */
667
+ const VIS_NODE_FILL = "#97C2FC";
668
+ const VIS_NODE_BORDER = "#2B7CE9";
669
+
622
670
  /** vis-network's edge color when an edge inherits none (vis-network 10.1.2, `color.color`). */
623
671
  const VIS_EDGE_COLOR = "#848484";
624
672
 
@@ -690,20 +738,67 @@ interface VisLayer {
690
738
  border?: unknown;
691
739
  shape?: unknown;
692
740
  size?: unknown;
741
+ value?: unknown;
742
+ scalingMin?: unknown;
743
+ scalingMax?: unknown;
693
744
  borderWidth?: unknown;
694
745
  opacity?: unknown;
695
746
  hidden?: unknown;
696
747
  label?: unknown;
697
748
  title?: unknown;
749
+ /** edges: whether `font` was given as text, which leaves the edge's label off the canvas (visValues) */
750
+ fontText?: unknown;
751
+ /** `font.multi`: whether the label's text holds html or markdown markup (visLabelText) */
752
+ fontMulti?: unknown;
753
+ fontStrokeWidth?: unknown;
754
+ fontStrokeColor?: unknown;
755
+ fontBackground?: unknown;
756
+ fontAlign?: unknown;
698
757
  fontColor?: unknown;
699
758
  fontSize?: unknown;
700
759
  fontFace?: unknown;
760
+ /** `font.vadjust`: how far down the label's text is drawn */
761
+ fontVadjust?: unknown;
762
+ /** `font.bold`, `font.ital`, `font.boldital` and `font.mono`: each font modifier's own options */
763
+ fontMods?: unknown;
764
+ margin?: unknown;
765
+ /** `widthConstraint` (a number) or its `maximum`: the width the label's lines wrap at */
766
+ wrapWidth?: unknown;
767
+ /** `scaling.label`: the label's font size from `value` when enabled */
768
+ scalingLabel?: unknown;
769
+ /** nodes: `shapeProperties.borderDashes` */
770
+ borderDashes?: unknown;
771
+ /** nodes: `shapeProperties.borderRadius`, a box's corner radius */
772
+ borderRadius?: unknown;
773
+ widthMin?: unknown;
774
+ heightMin?: unknown;
775
+ /** nodes: `heightConstraint.valign` */
776
+ valign?: unknown;
701
777
  width?: unknown;
702
778
  dashes?: unknown;
703
779
  arrows?: unknown;
704
780
  edgeColor?: unknown;
705
781
  edgeOpacity?: unknown;
706
782
  inherit?: unknown;
783
+ background?: unknown;
784
+ smooth?: unknown;
785
+ selfReference?: unknown;
786
+ selfReferenceSize?: unknown;
787
+ physics?: unknown;
788
+ /** edges: the rest length of its spring (and of its support node's two springs together) */
789
+ length?: unknown;
790
+ image?: unknown;
791
+ brokenImage?: unknown;
792
+ useImageSize?: unknown;
793
+ useBorderWithImage?: unknown;
794
+ interpolation?: unknown;
795
+ imagePadding?: unknown;
796
+ coordinateOrigin?: unknown;
797
+ iconCode?: unknown;
798
+ iconFace?: unknown;
799
+ iconSize?: unknown;
800
+ iconColor?: unknown;
801
+ iconWeight?: unknown;
707
802
  }
708
803
 
709
804
  /**
@@ -730,7 +825,14 @@ function visLayer(o: unknown, target: StyleTarget): VisLayer {
730
825
  set("hidden", o.hidden);
731
826
  set("label", o.label);
732
827
  set("title", o.title);
828
+ set("wrapWidth", isJsonObject(o.widthConstraint) ? o.widthConstraint.maximum : o.widthConstraint);
829
+ if (isJsonObject(o.scaling)) {
830
+ set("scalingLabel", o.scaling.label);
831
+ }
733
832
  visFontLayer(o.font, set);
833
+ if (target === "edge" && o.font !== undefined && o.font !== null) {
834
+ set("fontText", typeof o.font === "string");
835
+ }
734
836
  return layer;
735
837
  }
736
838
 
@@ -754,8 +856,146 @@ function visNodeLayer(o: Record<string, unknown>, set: SetLayer): void {
754
856
  }
755
857
  set("shape", o.shape);
756
858
  set("size", o.size);
859
+ set("value", o.value);
860
+ if (isJsonObject(o.scaling)) {
861
+ set("scalingMin", o.scaling.min);
862
+ set("scalingMax", o.scaling.max);
863
+ }
757
864
  set("borderWidth", o.borderWidth);
758
865
  set("opacity", o.opacity);
866
+ set("margin", o.margin);
867
+ set("widthMin", isJsonObject(o.widthConstraint) ? o.widthConstraint.minimum : o.widthConstraint);
868
+ set("heightMin", isJsonObject(o.heightConstraint) ? o.heightConstraint.minimum : o.heightConstraint);
869
+ set("valign", isJsonObject(o.heightConstraint) ? o.heightConstraint.valign : undefined);
870
+ set("image", isJsonObject(o.image) ? o.image.unselected : o.image);
871
+ set("brokenImage", o.brokenImage);
872
+ set("imagePadding", o.imagePadding);
873
+ if (isJsonObject(o.icon)) {
874
+ set("iconCode", o.icon.code);
875
+ set("iconFace", o.icon.face);
876
+ set("iconSize", o.icon.size);
877
+ set("iconColor", o.icon.color);
878
+ set("iconWeight", o.icon.weight);
879
+ }
880
+ if (isJsonObject(o.shapeProperties)) {
881
+ set("useImageSize", o.shapeProperties.useImageSize);
882
+ set("useBorderWithImage", o.shapeProperties.useBorderWithImage);
883
+ set("interpolation", o.shapeProperties.interpolation);
884
+ set("coordinateOrigin", o.shapeProperties.coordinateOrigin);
885
+ set("borderDashes", o.shapeProperties.borderDashes);
886
+ set("borderRadius", o.shapeProperties.borderRadius);
887
+ }
888
+ }
889
+
890
+ /**
891
+ * What a vis-network `image` or `circularImage` node draws (vis-network 9.1.9 `Image`, `CircularImage`,
892
+ * `CircleImageBase._resizeImage`): its picture, the broken image in its place when it has none, with its shorter
893
+ * side across the node's `2 * size` square (or at its own pixels with `shapeProperties.useImageSize`); an `image`
894
+ * node is the picture alone, reaching past that square, or over a box of its background with
895
+ * `shapeProperties.useBorderWithImage`; a `circularImage` is cut to a circle and ringed by its border. Its
896
+ * `imagePadding` keeps the picture that far inside the node's box on each side, stretched to what is left
897
+ * (`_drawImageAtPosition`); the node's `opacity` fades the picture too; with `shapeProperties.coordinateOrigin`
898
+ * "top-left" an `image` node with no box of its own draws its picture with its top left corner at the node's position.
899
+ * @param layer - the node's merged layer
900
+ * @param shape - its vis-network shape
901
+ * @param box - the node's width and height, in the file's units
902
+ * @param box.width - its width
903
+ * @param box.height - its height
904
+ * @returns the values, none for another shape or a node with no picture
905
+ */
906
+ function visImage(layer: VisLayer, shape: string, box: { width: number; height: number }): Row {
907
+ const text = (v: unknown): string | null => (typeof v === "string" && v !== "" ? v : null);
908
+ const [own, broken] = [text(layer.image), text(layer.brokenImage)];
909
+ const src = own ?? broken;
910
+ if ((shape !== "image" && shape !== "circularImage") || src === null) {
911
+ return {};
912
+ }
913
+ const circular = shape === "circularImage";
914
+ const row: Row = {
915
+ image: src,
916
+ ...(own !== null && broken !== null ? { imageFallback: broken } : {}),
917
+ imageFit: layer.useImageSize === true ? "none" : "cover",
918
+ imageClip: circular ? "node" : "none",
919
+ shape: "ellipse",
920
+ };
921
+ if (!circular) {
922
+ row.shape = layer.useBorderWithImage === true ? "rectangle" : "none";
923
+ }
924
+ if (layer.interpolation === false) {
925
+ row.imageSmoothing = false;
926
+ }
927
+ const opacity = typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity < 1 ? layer.opacity : null;
928
+ if (opacity !== null) {
929
+ row.imageOpacity = opacity;
930
+ }
931
+ visImagePlace(row, layer, box);
932
+ return row;
933
+ }
934
+
935
+ /**
936
+ * Where a vis-network picture sits in its node's box: inside its `imagePadding`, and from the top left corner under
937
+ * `shapeProperties.coordinateOrigin` "top-left" (visImage).
938
+ * @param row - the picture's values so far, written in place
939
+ * @param layer - the node's merged layer
940
+ * @param box - the node's width and height, in the file's units
941
+ * @param box.width - its width
942
+ * @param box.height - its height
943
+ */
944
+ function visImagePlace(row: Row, layer: VisLayer, box: { width: number; height: number }): void {
945
+ const pad = visImagePadding(layer.imagePadding);
946
+ if (pad !== null && layer.useImageSize !== true) {
947
+ row.imageFit = "none";
948
+ row.imageWidth = Math.max(0, box.width - pad.left - pad.right);
949
+ row.imageHeight = Math.max(0, box.height - pad.top - pad.bottom);
950
+ row.imageOffsetX = (pad.left - pad.right) / 2;
951
+ row.imageOffsetY = (pad.top - pad.bottom) / 2;
952
+ }
953
+ if (layer.coordinateOrigin === "top-left" && row.shape === "none") {
954
+ row.imageOffsetX = box.width / 2 + (typeof row.imageOffsetX === "number" ? row.imageOffsetX : 0);
955
+ row.imageOffsetY = box.height / 2 + (typeof row.imageOffsetY === "number" ? row.imageOffsetY : 0);
956
+ }
957
+ }
958
+
959
+ /**
960
+ * What a vis-network `icon` node draws (vis-network 9.1.9 `Icon`): its `icon.code` in `icon.face` (FontAwesome
961
+ * unless given) at `icon.size` (50) pixels and `icon.weight`, in `icon.color` (#2B7CE9), centered on the node; no
962
+ * code draws nothing.
963
+ * @param layer - the node's merged layer
964
+ * @param site - where a bad color is noted
965
+ * @returns the values, none for an icon with no code
966
+ */
967
+ function visIcon(layer: VisLayer, site: CanonSite): Row {
968
+ const code = typeof layer.iconCode === "string" && layer.iconCode !== "" ? layer.iconCode : null;
969
+ if (code === null) {
970
+ return {};
971
+ }
972
+ const size = typeof layer.iconSize === "number" && layer.iconSize >= 0 ? layer.iconSize : 50;
973
+ const color = typeof layer.iconColor === "string" ? canonColor(layer.iconColor, "vis", site) : null;
974
+ const weight = layer.iconWeight;
975
+ const bold = weight === "bold" || weight === "bolder" || (typeof weight === "number" && weight >= 600);
976
+ return {
977
+ glyph: code,
978
+ glyphFont: typeof layer.iconFace === "string" && layer.iconFace !== "" ? layer.iconFace : "FontAwesome",
979
+ glyphSize: size,
980
+ glyphColor: color?.color ?? "#2b7ce9",
981
+ glyphWeight: bold ? "bold" : "normal",
982
+ };
983
+ }
984
+
985
+ /**
986
+ * vis-network's `imagePadding`: one number for every side, or `{ top, right, bottom, left }` (each 0 unless given).
987
+ * @param raw - the option
988
+ * @returns the four sides, or null for no padding
989
+ */
990
+ function visImagePadding(raw: unknown): { top: number; right: number; bottom: number; left: number } | null {
991
+ const side = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
992
+ let pad: { top: number; right: number; bottom: number; left: number } | null = null;
993
+ if (typeof raw === "number") {
994
+ pad = { top: side(raw), right: side(raw), bottom: side(raw), left: side(raw) };
995
+ } else if (isJsonObject(raw)) {
996
+ pad = { top: side(raw.top), right: side(raw.right), bottom: side(raw.bottom), left: side(raw.left) };
997
+ }
998
+ return pad === null || pad.top + pad.right + pad.bottom + pad.left === 0 ? null : pad;
759
999
  }
760
1000
 
761
1001
  /**
@@ -775,8 +1015,19 @@ function visEdgeLayer(o: Record<string, unknown>, set: SetLayer): void {
775
1015
  set("inherit", own ? false : color.inherit);
776
1016
  }
777
1017
  set("width", o.width);
1018
+ set("value", o.value);
1019
+ if (isJsonObject(o.scaling)) {
1020
+ set("scalingMin", o.scaling.min);
1021
+ set("scalingMax", o.scaling.max);
1022
+ }
778
1023
  set("dashes", o.dashes);
779
1024
  set("arrows", o.arrows);
1025
+ set("background", o.background);
1026
+ set("smooth", o.smooth);
1027
+ set("selfReference", o.selfReference);
1028
+ set("selfReferenceSize", o.selfReferenceSize);
1029
+ set("physics", o.physics);
1030
+ set("length", o.length);
780
1031
  }
781
1032
 
782
1033
  /**
@@ -786,9 +1037,19 @@ function visEdgeLayer(o: Record<string, unknown>, set: SetLayer): void {
786
1037
  */
787
1038
  function visFontLayer(font: unknown, set: SetLayer): void {
788
1039
  if (isJsonObject(font)) {
1040
+ set("fontMulti", font.multi);
1041
+ set("fontStrokeWidth", font.strokeWidth);
1042
+ set("fontStrokeColor", font.strokeColor);
1043
+ set("fontBackground", font.background);
1044
+ set("fontAlign", font.align);
789
1045
  set("fontColor", font.color);
790
1046
  set("fontSize", font.size);
791
1047
  set("fontFace", font.face);
1048
+ set("fontVadjust", font.vadjust);
1049
+ const mods = ["bold", "ital", "boldital", "mono"].filter((m) => font[m] !== undefined && font[m] !== null);
1050
+ if (mods.length > 0) {
1051
+ set("fontMods", Object.fromEntries(mods.map((m) => [m, font[m]])));
1052
+ }
792
1053
  } else if (typeof font === "string") {
793
1054
  const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(font.slice(0, 256));
794
1055
  if (m !== null) {
@@ -814,7 +1075,7 @@ function visValues(layer: VisLayer, target: StyleTarget, site: (property: string
814
1075
  out[p] = v;
815
1076
  }
816
1077
  };
817
- const color = (p: string, opacity: string | null, raw: unknown): void => {
1078
+ const color: PutColor = (p, opacity, raw) => {
818
1079
  if (raw === undefined || raw === null || raw === "") {
819
1080
  return;
820
1081
  }
@@ -828,29 +1089,508 @@ function visValues(layer: VisLayer, target: StyleTarget, site: (property: string
828
1089
  };
829
1090
  if (target === "node") {
830
1091
  color("fill", "fillOpacity", layer.fill);
831
- color("borderColor", null, layer.border);
1092
+ color("borderColor", "borderOpacity", layer.border);
832
1093
  visNodeValues(layer, sized, put, site);
833
1094
  } else {
834
1095
  color("color", "opacity", layer.edgeColor);
835
1096
  visEdgeValues(layer, put, site);
1097
+ visEdgeBackground(layer, put, color);
836
1098
  }
837
1099
  if (layer.hidden !== undefined && canonBoolean(layer.hidden, site("visible")) === true) {
838
1100
  put("visible", false);
839
1101
  }
840
- if (layer.label !== undefined) {
841
- put("label", canonText(layer.label, site("label")));
1102
+ visLabelValues(layer, target, put, color, site);
1103
+ return out;
1104
+ }
1105
+
1106
+ /**
1107
+ * An edge's background: a band `size` wide under the line, its outline beyond the line. vis-network draws it only
1108
+ * under a smooth (bezier) edge (BezierEdgeBase._bezierCurve calls drawBackground; StraightEdge never does).
1109
+ * @param layer - the layer
1110
+ * @param put - the row's setter
1111
+ * @param color - the row's color setter
1112
+ */
1113
+ function visEdgeBackground(layer: VisLayer, put: PutRow, color: PutColor): void {
1114
+ // ponytail: `smooth` is read from this layer alone, else vis-network's default (smooth)
1115
+ if (!isJsonObject(layer.background)) {
1116
+ return;
842
1117
  }
843
- if (layer.title !== undefined) {
844
- put("tooltip", canonText(layer.title, site("tooltip")));
1118
+ const bg = layer.background;
1119
+ const straight = layer.smooth === false || (isJsonObject(layer.smooth) && layer.smooth.enabled === false);
1120
+ color("outlineColor", "outlineOpacity", bg.color);
1121
+ if (bg.enabled !== undefined) {
1122
+ const size = typeof bg.size === "number" ? bg.size : 10;
1123
+ const width = typeof layer.width === "number" ? layer.width : 1;
1124
+ put("outlineWidth", bg.enabled === true && !straight ? Math.max(0, size - width) : 0);
845
1125
  }
846
- color("labelColor", null, layer.fontColor);
1126
+ const band = visBandStyle(bg, false);
1127
+ if (band !== null) {
1128
+ put("outlineStyle", band);
1129
+ }
1130
+ }
1131
+
1132
+ /** Puts a color of a row, and its alpha as an opacity when it has one (visValues). */
1133
+ type PutColor = (p: string, opacity: string | null, raw: unknown) => void;
1134
+
1135
+ /**
1136
+ * The label values of a vis.js layer: its text (markup read), wrap width, font, outline, background and, for an edge,
1137
+ * where on the line it sits.
1138
+ * @param layer - the layer
1139
+ * @param target - nodes or edges
1140
+ * @param put - the row's setter
1141
+ * @param color - the row's color setter
1142
+ * @param site - where its values are read
1143
+ */
1144
+ function visLabelValues(
1145
+ layer: VisLayer,
1146
+ target: StyleTarget,
1147
+ put: PutRow,
1148
+ color: PutColor,
1149
+ site: (property: string) => CanonSite,
1150
+ ): void {
847
1151
  if (layer.fontSize !== undefined) {
848
1152
  put("labelSize", canonNumber(layer.fontSize, site("labelSize")));
849
1153
  }
850
1154
  if (layer.fontFace !== undefined) {
851
1155
  put("labelFont", canonText(layer.fontFace, site("labelFont")));
852
1156
  }
853
- return out;
1157
+ if (layer.label !== undefined) {
1158
+ visLabelText(layer, target, put, site);
1159
+ }
1160
+ // vis-network wraps a label's lines at `widthConstraint.maximum` (or a number), margins not taken off:
1161
+ // Label.adjustSizes subtracts them only under constrainWidth, which nothing sets. LabelSplitter keeps each blank
1162
+ // as a word of its own and joins words with a blank (getLongestFit), so a blank between two words counts three.
1163
+ if (typeof layer.wrapWidth === "number" && Number.isFinite(layer.wrapWidth) && layer.wrapWidth > 0) {
1164
+ put("labelWrapWidth", layer.wrapWidth);
1165
+ put("labelWrapSpaceCount", 3);
1166
+ }
1167
+ if (layer.title !== undefined) {
1168
+ put("tooltip", canonText(layer.title, site("tooltip")));
1169
+ }
1170
+ // a label drawn under `scaling.label.drawThreshold - 1` px on screen is not drawn (Label.draw)
1171
+ const threshold = isJsonObject(layer.scalingLabel) ? layer.scalingLabel.drawThreshold : undefined;
1172
+ if (typeof threshold === "number" && Number.isFinite(threshold)) {
1173
+ put("labelMinScreenSize", Math.max(0, threshold - 1));
1174
+ }
1175
+ visLabelLook(layer, target, put, color, site);
1176
+ }
1177
+
1178
+ /**
1179
+ * The text of a vis.js layer's label, its markup read into runs.
1180
+ * @param layer - the layer
1181
+ * @param target - nodes or edges
1182
+ * @param put - the row's setter
1183
+ * @param site - where its values are read
1184
+ */
1185
+ function visLabelText(layer: VisLayer, target: StyleTarget, put: PutRow, site: (property: string) => CanonSite): void {
1186
+ // vis-network 9.1.9 draws no edge label whose `font` is text: Edge.drawLabel reads `options.font.align` (undefined
1187
+ // on a string) as not horizontal and moves the context to the label point, while the label module, which
1188
+ // parsed the text, still draws at the absolute point, so the label lands off the canvas
1189
+ const text = target === "edge" && layer.fontText === true ? "" : canonText(layer.label, site("label"));
1190
+ const marked = typeof text === "string" ? visMarkup(text, layer.fontMulti) : null;
1191
+ put("label", marked?.text ?? text);
1192
+ if (marked !== null) {
1193
+ const runs = visRuns(marked.runs, layer, site("labelRuns"));
1194
+ put("labelRuns", runs);
1195
+ if (runs !== null) {
1196
+ put("labelRunAlign", visRunAlign(layer, target));
1197
+ }
1198
+ }
1199
+ // after the face: a label all in mono is drawn in the mono face
1200
+ for (const [p, v] of Object.entries(VIS_MOD_VALUES[marked?.mod ?? "normal"] ?? {})) {
1201
+ put(p, v);
1202
+ }
1203
+ }
1204
+
1205
+ /**
1206
+ * The look of a vis.js layer's label: its color, background box, letter outline and, for an edge, where on the line
1207
+ * it sits.
1208
+ * @param layer - the layer
1209
+ * @param target - nodes or edges
1210
+ * @param put - the row's setter
1211
+ * @param color - the row's color setter
1212
+ * @param site - where its values are read
1213
+ */
1214
+ function visLabelLook(
1215
+ layer: VisLayer,
1216
+ target: StyleTarget,
1217
+ put: PutRow,
1218
+ color: PutColor,
1219
+ site: (property: string) => CanonSite,
1220
+ ): void {
1221
+ color("labelColor", null, layer.fontColor);
1222
+ // font.background "none" (vis-network's default) draws no box
1223
+ color("labelBackground", null, layer.fontBackground === "none" ? undefined : layer.fontBackground);
1224
+ // the box is the words' own: as wide as the text and one font size tall a line (Label._drawBackground: getSize)
1225
+ if (typeof layer.fontBackground === "string" && layer.fontBackground !== "none") {
1226
+ put("labelPadding", 0);
1227
+ put("labelLineHeight", 1);
1228
+ }
1229
+ // a stroke with no color of its own is vis-network's default, white (font.strokeColor "#ffffff")
1230
+ const stroked = typeof layer.fontStrokeWidth === "number" && layer.fontStrokeWidth > 0;
1231
+ color("labelOutlineColor", null, layer.fontStrokeColor ?? (stroked ? "#ffffff" : undefined));
1232
+ if (layer.fontStrokeWidth !== undefined) {
1233
+ put("labelOutlineWidth", canonNumber(layer.fontStrokeWidth, site("labelOutlineWidth")));
1234
+ }
1235
+ if (target === "edge" && typeof layer.fontAlign === "string") {
1236
+ // vis-network draws an edge label on the line ("horizontal", "middle"), above it ("top") or below it ("bottom")
1237
+ put("labelPosition", layer.fontAlign === "top" || layer.fontAlign === "bottom" ? layer.fontAlign : "center");
1238
+ } else if (target === "node" && typeof layer.fontAlign === "string") {
1239
+ // a node label's lines are centered on one another, or flush left or right in the label's box (Label._drawText:
1240
+ // any align but center and right draws flush left); an edge label's lines are always centered
1241
+ put("labelTextAlign", { center: "center", right: "right" }[layer.fontAlign] ?? "left");
1242
+ }
1243
+ // the text is drawn `font.vadjust` lower than its place (Label._drawText: y + block.vadjust)
1244
+ if (typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) && layer.fontVadjust !== 0) {
1245
+ put("labelOffsetY", layer.fontVadjust);
1246
+ }
1247
+ }
1248
+
1249
+ /** The label values of a label whose every word has one vis.js font modifier; vis-network's mono face is monospace. */
1250
+ const VIS_MOD_VALUES: Readonly<Record<string, Row>> = {
1251
+ bold: { labelWeight: "bold" },
1252
+ ital: { labelItalic: true },
1253
+ boldital: { labelWeight: "bold", labelItalic: true },
1254
+ mono: { labelFont: "monospace" },
1255
+ };
1256
+
1257
+ /**
1258
+ * Whether vis-network's node `opacity` replaces a color's alpha: a hex color of 3 or 6 digits or an `rgb()` color, and
1259
+ * vis-network's own default (overrideOpacity; hexToRGB reads no other text).
1260
+ * @param color - the color as the file gives it, or undefined for vis-network's default
1261
+ * @returns true when the opacity applies
1262
+ */
1263
+ function visTakesOpacity(color: unknown): boolean {
1264
+ if (color === undefined) {
1265
+ return true;
1266
+ }
1267
+ return (
1268
+ typeof color === "string" &&
1269
+ !color.includes("rgba") &&
1270
+ (color.includes("rgb") || /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color.trim()))
1271
+ );
1272
+ }
1273
+
1274
+ /**
1275
+ * The line style of an edge's background band: stroked `size` wide (10 by default) in its own dashes, [5, 5] for true
1276
+ * (EdgeBase.drawBackground, setStrokeDashed), with the canvas's line caps.
1277
+ * @param bg - the edge's `background`
1278
+ * @param round - whether the canvas draws round caps by then (see visStyles' edge loop)
1279
+ * @returns the line style, or null for a band with no dashes
1280
+ */
1281
+ function visBandStyle(bg: Readonly<Record<string, unknown>>, round: boolean): string | null {
1282
+ if (bg.dashes === undefined || bg.dashes === false) {
1283
+ return null;
1284
+ }
1285
+ return visDashStyle(bg.dashes === true ? [5, 5] : bg.dashes, typeof bg.size === "number" ? bg.size : 10, round);
1286
+ }
1287
+
1288
+ /**
1289
+ * vis.js dashes (`dashes`, `shapeProperties.borderDashes`) as the line style they draw at a width, read as a dash
1290
+ * array is read anywhere: with round caps each dash a width longer and each gap a width shorter; then gaps under a
1291
+ * tenth of the pattern are solid, dashes no longer than 1.5 widths are dotted, dashes of which the longest is twice
1292
+ * the shortest dash-dot, others dashed; false is solid.
1293
+ * @param dashes - false, or the dash array (dash, gap, dash, gap ...; an odd one repeats)
1294
+ * @param width - the line's width
1295
+ * @param round - whether the dashes have round caps
1296
+ * @returns the line style
1297
+ */
1298
+ function visDashStyle(dashes: unknown, width: number, round: boolean): string {
1299
+ if (!Array.isArray(dashes)) {
1300
+ return dashes === false ? "solid" : "dashed";
1301
+ }
1302
+ const lengths = dashes.filter((d): d is number => typeof d === "number" && Number.isFinite(d));
1303
+ const pairs = lengths.length % 2 === 1 ? [...lengths, ...lengths] : lengths;
1304
+ const grow = round ? width : 0;
1305
+ const on = pairs.filter((_d, i) => i % 2 === 0).map((d) => Math.max(0, d) + grow);
1306
+ const off = pairs.filter((_d, i) => i % 2 === 1).map((d) => Math.max(0, d - grow));
1307
+ const sum = (xs: number[]): number => xs.reduce((a, b) => a + b, 0);
1308
+ // a gap of exactly a tenth still shows (mappings-bypass edge-default: [5, 5] at width 4)
1309
+ if (on.length === 0 || sum(off) < 0.1 * (sum(on) + sum(off))) {
1310
+ return "solid";
1311
+ }
1312
+ if (on.every((d) => d <= 1.5 * Math.max(1, width))) {
1313
+ return "dotted";
1314
+ }
1315
+ return Math.max(...on) >= 2 * Math.min(...on) ? "dash-dot" : "dashed";
1316
+ }
1317
+
1318
+ /**
1319
+ * vis.js label markup (`font.multi`): true or "html" reads `<b>`, `<i>` and `<code>` and the entities `&lt;` and
1320
+ * `&amp;`; "markdown" or "md" reads `*bold*`, `_ital_` and `` `mono` `` at word edges, with `\` escaping a mark
1321
+ * (vis-network 9.1.9 `LabelSplitter.splitHtmlBlocks`, `splitMarkdownBlocks`). The text without its markup, and the
1322
+ * one font modifier every word has when they share one.
1323
+ * @param text - the label
1324
+ * @param multi - `font.multi`
1325
+ * @returns the plain text and the shared modifier, or null when the label is not read as markup
1326
+ */
1327
+ function visMarkup(
1328
+ text: string,
1329
+ multi: unknown,
1330
+ ): { text: string; mod: string | null; runs: { text: string; mod: string }[] } | null {
1331
+ const system = VIS_MARKUP_SYSTEM.get(multi);
1332
+ if (system === undefined) {
1333
+ return null;
1334
+ }
1335
+ const state: VisMarkState = { bold: false, ital: false, mono: false };
1336
+ const runs: { text: string; mod: string }[] = [];
1337
+ const source = text.slice(0, 4096);
1338
+ let i = 0;
1339
+ while (i < source.length) {
1340
+ const tagged = visMarkTag(source, i, system, state);
1341
+ if (tagged > 0) {
1342
+ i += tagged;
1343
+ continue;
1344
+ }
1345
+ const [ch, next] = visMarkChar(source, i, system);
1346
+ runs.push({ text: ch, mod: visMarkMod(state) });
1347
+ i = next;
1348
+ }
1349
+ const mods = new Set(runs.filter((r) => /\S/.test(r.text)).map((r) => r.mod));
1350
+ const only = mods.size === 1 ? [...mods][0] : null;
1351
+ // the characters in runs of one modifier each, as vis-network draws its blocks
1352
+ const merged: { text: string; mod: string }[] = [];
1353
+ for (const r of runs) {
1354
+ const last = merged.at(-1);
1355
+ if (last?.mod === r.mod) {
1356
+ last.text += r.text;
1357
+ } else {
1358
+ merged.push({ ...r });
1359
+ }
1360
+ }
1361
+ return { text: runs.map((r) => r.text).join(""), mod: only === "normal" ? null : only, runs: merged };
1362
+ }
1363
+
1364
+ /**
1365
+ * vis-network's own options of each font modifier (vis-network 9.1.9 `NodesHandler` defaults, `font.mono`); an option
1366
+ * a modifier does not set is the label's own (Label.propagateFonts).
1367
+ */
1368
+ const VIS_MOD_DEFAULTS: Readonly<Record<string, { mod: string; face?: string; size?: number; vadjust?: number }>> = {
1369
+ bold: { mod: "bold" },
1370
+ ital: { mod: "italic" },
1371
+ boldital: { mod: "bold italic" },
1372
+ mono: { mod: "", face: "monospace", size: 15, vadjust: 2 },
1373
+ };
1374
+
1375
+ /**
1376
+ * vis.js multi-font runs as the neutral `labelRuns`: each run's font as vis-network draws it (its modifier's own
1377
+ * `font.bold`, `font.ital`, `font.boldital` or `font.mono` options, an object or text like "16px arial black", over
1378
+ * the label's font; mono is 15 px monospace unless given), each field only where it differs from the label's own, and
1379
+ * `shift`: how much lower than the label's own words its modifier's `vadjust` draws the run (mono's is 2 unless given;
1380
+ * the others' are the label's).
1381
+ * @param runs - the runs, each with its modifier
1382
+ * @param layer - the element's layer
1383
+ * @param site - where a bad color is noted
1384
+ * @returns the runs as JSON, or null when every run is drawn in the label's own font
1385
+ */
1386
+ function visRuns(runs: readonly { text: string; mod: string }[], layer: VisLayer, site: CanonSite): string | null {
1387
+ const own = {
1388
+ face: typeof layer.fontFace === "string" ? layer.fontFace : "arial",
1389
+ size: typeof layer.fontSize === "number" ? layer.fontSize : VIS_FONT_SIZE,
1390
+ color: typeof layer.fontColor === "string" ? layer.fontColor : null,
1391
+ vadjust: typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) ? layer.fontVadjust : 0,
1392
+ };
1393
+ const mods = isJsonObject(layer.fontMods) ? layer.fontMods : {};
1394
+ let differs = false;
1395
+ const out = runs.map((r) => {
1396
+ const run: Record<string, unknown> = { text: r.text };
1397
+ const base = VIS_MOD_DEFAULTS[r.mod];
1398
+ if (base === undefined) {
1399
+ return run;
1400
+ }
1401
+ const given = visModOptions(mods[r.mod]);
1402
+ const modText = typeof given.mod === "string" ? given.mod : base.mod;
1403
+ const face = typeof given.face === "string" ? given.face : (base.face ?? own.face);
1404
+ const size = typeof given.size === "number" ? given.size : (base.size ?? own.size);
1405
+ const vadjust = typeof given.vadjust === "number" ? given.vadjust : (base.vadjust ?? own.vadjust);
1406
+ const color = typeof given.color === "string" ? canonColor(given.color, "vis", site)?.color : null;
1407
+ if (modText.includes("bold")) {
1408
+ run.weight = "bold";
1409
+ }
1410
+ if (modText.includes("italic")) {
1411
+ run.italic = true;
1412
+ }
1413
+ if (face !== null && face !== own.face) {
1414
+ run.font = face;
1415
+ }
1416
+ if (size !== own.size) {
1417
+ run.size = size;
1418
+ }
1419
+ if (Number.isFinite(vadjust) && vadjust !== own.vadjust) {
1420
+ run.shift = vadjust - own.vadjust;
1421
+ }
1422
+ if (typeof color === "string") {
1423
+ run.color = color;
1424
+ }
1425
+ differs ||= Object.keys(run).length > 1;
1426
+ return run;
1427
+ });
1428
+ return differs ? JSON.stringify(out) : null;
1429
+ }
1430
+
1431
+ /**
1432
+ * 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
1433
+ * `textBaseline` (Label._drawText): "hanging" below a node sized by `size` (ShapeBase._drawShape), "middle" inside
1434
+ * one sized by its label and on an edge's line, "alphabetic" above an edge (`font.align` top) and "hanging" below it.
1435
+ * @param layer - the element's merged layer
1436
+ * @param target - nodes or edges
1437
+ * @returns the neutral run alignment
1438
+ */
1439
+ function visRunAlign(layer: VisLayer, target: StyleTarget): "baseline" | "hanging" | "middle" {
1440
+ if (target === "edge") {
1441
+ return (
1442
+ ({ top: "baseline", bottom: "hanging" } as Record<string, "baseline" | "hanging">)[
1443
+ String(layer.fontAlign)
1444
+ ] ?? "middle"
1445
+ );
1446
+ }
1447
+ const shape = typeof layer.shape === "string" ? layer.shape : "ellipse";
1448
+ return VIS_SIZED.has(shape) ? "hanging" : "middle";
1449
+ }
1450
+
1451
+ /**
1452
+ * How much lower than a neutral `bottom` label vis-network draws the label of a node sized by `size`: it hangs the
1453
+ * text's top 4 + size / 2 below the outline, whatever the line count (ShapeBase._drawShape puts the label half its
1454
+ * height below the node and Label.calculateLabelSize moves its first line up by that again, then down 4 and half a
1455
+ * font size more), where a `bottom` label's words start about 2 + size / 3 below it (a small gap, then the space
1456
+ * above the capitals in a line's box). Measured on the vis-network labels sheet, font sizes 6 to 40.
1457
+ * @param size - the label's font size
1458
+ * @returns the extra distance down
1459
+ */
1460
+ function visBelowGap(size: number): number {
1461
+ // ponytail: a fixed fit of the renderer's gap and line box; a neutral "hang the text's top here" anchor would remove it
1462
+ return 2 + size / 6;
1463
+ }
1464
+
1465
+ /**
1466
+ * How much farther below a node sized by `size` vis-network draws a label in runs than one in its own font alone:
1467
+ * half of what its lines are taller (each line as tall as its largest run, Label: line.height) than one font size each
1468
+ * (ShapeBase._drawShape centers the label half its height below the node, then draws its first line at the top).
1469
+ * @param runs - the label's runs, as `visRuns` writes them
1470
+ * @param size - the label's font size
1471
+ * @returns the distance, 0 for runs no larger than the label's font
1472
+ */
1473
+ function visRunsDrop(runs: string, size: number): number {
1474
+ let parsed: unknown;
1475
+ try {
1476
+ parsed = JSON.parse(runs);
1477
+ } catch {
1478
+ return 0;
1479
+ }
1480
+ if (!Array.isArray(parsed)) {
1481
+ return 0;
1482
+ }
1483
+ const lines: number[] = [size];
1484
+ for (const run of parsed as { text?: unknown; size?: unknown }[]) {
1485
+ const runSize = typeof run.size === "number" ? run.size : size;
1486
+ (typeof run.text === "string" ? run.text : "").split("\n").forEach((piece, i) => {
1487
+ if (i > 0) {
1488
+ lines.push(size);
1489
+ }
1490
+ if (piece !== "") {
1491
+ lines[lines.length - 1] = Math.max(lines.at(-1) ?? size, runSize);
1492
+ }
1493
+ });
1494
+ }
1495
+ return lines.reduce((sum, h) => sum + h - size, 0) / 2;
1496
+ }
1497
+
1498
+ /**
1499
+ * A vis.js font modifier's options: an object, or text like "16px arial black" (size, face, color).
1500
+ * @param raw - `font.bold`, `font.ital`, `font.boldital` or `font.mono`
1501
+ * @returns the options
1502
+ */
1503
+ function visModOptions(raw: unknown): Record<string, unknown> {
1504
+ if (isJsonObject(raw)) {
1505
+ return raw;
1506
+ }
1507
+ if (typeof raw === "string") {
1508
+ const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(raw.slice(0, 256));
1509
+ if (m !== null) {
1510
+ return { size: Number(m[1]), face: m[2], ...(m[3] === undefined ? {} : { color: m[3] }) };
1511
+ }
1512
+ }
1513
+ return {};
1514
+ }
1515
+
1516
+ /** vis-network's markup systems by `font.multi`. */
1517
+ const VIS_MARKUP_SYSTEM: ReadonlyMap<unknown, "html" | "md"> = new Map<unknown, "html" | "md">([
1518
+ [true, "html"],
1519
+ ["html", "html"],
1520
+ ["markdown", "md"],
1521
+ ["md", "md"],
1522
+ ]);
1523
+
1524
+ /** Which font modifiers are on, part way through a label. */
1525
+ interface VisMarkState {
1526
+ bold: boolean;
1527
+ ital: boolean;
1528
+ mono: boolean;
1529
+ }
1530
+
1531
+ /** Each system's marks and the modifier each turns on or off. */
1532
+ const VIS_MARKS: Readonly<Record<"html" | "md", Readonly<Record<string, keyof VisMarkState>>>> = {
1533
+ html: { "<b>": "bold", "</b>": "bold", "<i>": "ital", "</i>": "ital", "<code>": "mono", "</code>": "mono" },
1534
+ md: { "*": "bold", _: "ital", "`": "mono" },
1535
+ };
1536
+
1537
+ /**
1538
+ * 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
1539
+ * closes one that is on; a markdown mark opens at the start of a word and closes a modifier that is on.
1540
+ * @param source - the label
1541
+ * @param i - the place
1542
+ * @param system - the markup system
1543
+ * @param state - the modifiers on, changed in place
1544
+ * @returns how many characters the mark takes, 0 for no mark
1545
+ */
1546
+ function visMarkTag(source: string, i: number, system: "html" | "md", state: VisMarkState): number {
1547
+ const marks = VIS_MARKS[system];
1548
+ const tag = Object.keys(marks).find((m) => source.startsWith(m, i));
1549
+ if (tag === undefined) {
1550
+ return 0;
1551
+ }
1552
+ const key = marks[tag];
1553
+ const toggles =
1554
+ system === "html" ? state[key] === tag.startsWith("</") : state[key] || i === 0 || /\s/.test(source[i - 1]);
1555
+ if (!toggles) {
1556
+ return 0;
1557
+ }
1558
+ state[key] = !state[key];
1559
+ return tag.length;
1560
+ }
1561
+
1562
+ /**
1563
+ * The character at a place in a label, an html entity (`&lt;`, `&amp;`) or a markdown escape read as the character it
1564
+ * stands for.
1565
+ * @param source - the label
1566
+ * @param i - the place
1567
+ * @param system - the markup system
1568
+ * @returns the character and the place after it
1569
+ */
1570
+ function visMarkChar(source: string, i: number, system: "html" | "md"): [string, number] {
1571
+ if (system === "md") {
1572
+ return source[i] === "\\" && i + 1 < source.length ? [source[i + 1], i + 2] : [source[i], i + 1];
1573
+ }
1574
+ const entity = [
1575
+ ["&lt;", "<"],
1576
+ ["&amp;", "&"],
1577
+ ].find(([e]) => source.startsWith(e, i));
1578
+ return entity === undefined ? [source[i], i + 1] : [entity[1], i + entity[0].length];
1579
+ }
1580
+
1581
+ /**
1582
+ * The font modifier the modifiers on make: mono over the others, bold and italic together boldital.
1583
+ * @param state - the modifiers on
1584
+ * @returns the modifier
1585
+ */
1586
+ function visMarkMod(state: VisMarkState): string {
1587
+ if (state.mono) {
1588
+ return "mono";
1589
+ }
1590
+ if (state.bold) {
1591
+ return state.ital ? "boldital" : "bold";
1592
+ }
1593
+ return state.ital ? "ital" : "normal";
854
1594
  }
855
1595
 
856
1596
  /** Sets a value of a row when it is not null. */
@@ -866,17 +1606,41 @@ type PutRow = (p: string, v: StyleValue | null) => void;
866
1606
  function visNodeValues(layer: VisLayer, sized: boolean, put: PutRow, site: (property: string) => CanonSite): void {
867
1607
  if (layer.shape !== undefined) {
868
1608
  put("shape", canonEnum("shape", layer.shape, "vis", site("shape")));
1609
+ // vis-network draws a label inside the shapes sized by their label and below the shapes sized by `size`
1610
+ if (typeof layer.shape === "string") {
1611
+ put("labelPosition", VIS_SIZED.has(layer.shape) ? "bottom" : "center");
1612
+ }
869
1613
  }
870
1614
  if (sized && layer.size !== undefined) {
1615
+ // vis-network draws `size` as a radius: a dot of size 25 is 50 across
871
1616
  const v = canonNumber(layer.size, site("width"));
872
- put("width", v);
873
- put("height", v);
1617
+ put("width", v === null ? null : 2 * v);
1618
+ put("height", v === null ? null : 2 * v);
874
1619
  }
875
1620
  if (layer.borderWidth !== undefined) {
876
1621
  put("borderWidth", canonNumber(layer.borderWidth, site("borderWidth")));
877
1622
  }
1623
+ visNodeOpacityAndDashes(layer, put, site);
1624
+ }
1625
+
1626
+ /**
1627
+ * A vis.js node's opacity and border dashes (visNodeValues).
1628
+ * @param layer - the layer
1629
+ * @param put - the row's setter
1630
+ * @param site - where its values are read
1631
+ */
1632
+ function visNodeOpacityAndDashes(layer: VisLayer, put: PutRow, site: (property: string) => CanonSite): void {
878
1633
  if (layer.opacity !== undefined) {
879
- put("opacity", canonOpacity(layer.opacity, "vis", site("opacity")));
1634
+ // vis-network's node opacity replaces the alpha of its border color and its background color alike, but only of
1635
+ // a hex or rgb() color: overrideOpacity leaves rgba() and any other text (a name) as it is (Node.getFormattingValues)
1636
+ const opacity = canonOpacity(layer.opacity, "vis", site("opacity"));
1637
+ put("opacity", visTakesOpacity(layer.fill) ? opacity : null);
1638
+ put("borderOpacity", visTakesOpacity(layer.border) ? opacity : null);
1639
+ }
1640
+ if (layer.borderDashes !== undefined) {
1641
+ // vis-network's default border dashes are [5, 15]
1642
+ const width = typeof layer.borderWidth === "number" ? layer.borderWidth : 1;
1643
+ put("borderStyle", visDashStyle(layer.borderDashes === true ? [5, 15] : layer.borderDashes, width, false));
880
1644
  }
881
1645
  }
882
1646
 
@@ -894,25 +1658,76 @@ function visEdgeValues(layer: VisLayer, put: PutRow, site: (property: string) =>
894
1658
  put("width", canonNumber(layer.width, site("width")));
895
1659
  }
896
1660
  if (layer.dashes !== undefined) {
897
- put("lineStyle", layer.dashes === false || layer.dashes === "false" ? "solid" : "dashed");
1661
+ // vis-network's default dashes are [5, 5]
1662
+ const width = typeof layer.width === "number" ? layer.width : 1;
1663
+ const dashes = layer.dashes === true ? [5, 5] : layer.dashes;
1664
+ // vis-network draws an edge with round caps (EdgeBase: lineCap "round"), each dash a width longer
1665
+ put("lineStyle", layer.dashes === "false" ? "solid" : visDashStyle(dashes, width, true));
898
1666
  }
899
1667
  if (layer.arrows !== undefined) {
900
- const [to, from] = visArrows(layer.arrows, site);
1668
+ const [to, from, middle] = visArrows(layer.arrows, site);
901
1669
  put("targetArrow", to);
902
1670
  put("sourceArrow", from);
1671
+ put("midTargetArrow", middle);
1672
+ // One neutral size for every end: the target's scaleFactor, else the source's, else the middle's.
1673
+ const scaled = (name: string): unknown => {
1674
+ const a = isJsonObject(layer.arrows) ? layer.arrows[name] : undefined;
1675
+ return isJsonObject(a) ? a.scaleFactor : undefined;
1676
+ };
1677
+ const scale = scaled("to") ?? scaled("from") ?? scaled("middle");
1678
+ if (scale !== undefined) {
1679
+ put("arrowSize", canonNumber(scale, site("arrowSize")));
1680
+ }
1681
+ visArrowImages(layer.arrows, put, site);
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * The pictures of vis.js `image` arrows (`{ to: { type: "image", src, imageWidth, imageHeight } }`), each with the
1687
+ * size it is drawn at where the file gives one (vis-network 9.1.9 `Image.draw`: `imageWidth` across the line,
1688
+ * `imageHeight` along it, else the picture's own).
1689
+ * @param arrows - the `arrows` value
1690
+ * @param put - the row's setter
1691
+ * @param site - where its values are read
1692
+ */
1693
+ function visArrowImages(arrows: unknown, put: PutRow, site: (property: string) => CanonSite): void {
1694
+ if (!isJsonObject(arrows)) {
1695
+ return;
1696
+ }
1697
+ for (const [name, property] of [
1698
+ ["to", "targetArrow"],
1699
+ ["from", "sourceArrow"],
1700
+ ["middle", "midTargetArrow"],
1701
+ ] as const) {
1702
+ const a = arrows[name];
1703
+ if (!isJsonObject(a) || a.type !== "image" || typeof a.src !== "string" || a.src === "") {
1704
+ continue;
1705
+ }
1706
+ put(`${property}Image`, a.src);
1707
+ if (a.imageWidth !== undefined) {
1708
+ put(`${property}ImageWidth`, canonNumber(a.imageWidth, site(`${property}ImageWidth`)));
1709
+ }
1710
+ if (a.imageHeight !== undefined) {
1711
+ put(`${property}ImageHeight`, canonNumber(a.imageHeight, site(`${property}ImageHeight`)));
1712
+ }
903
1713
  }
904
1714
  }
905
1715
 
906
1716
  /**
907
- * vis.js `arrows` as the target and source arrows: `"to"`, `"to, from"`, or `{ to: true | { enabled, type } }`.
1717
+ * vis.js `arrows` as the target, source and middle arrows: `"to"`, `"to, from"`, `"middle"`, or
1718
+ * `{ to: true | { enabled, type } }`. The middle one points toward the target.
908
1719
  * @param arrows - the value
909
1720
  * @param site - where it is read
910
- * @returns the target arrow and the source arrow
1721
+ * @returns the target arrow, the source arrow and the middle arrow
911
1722
  */
912
- function visArrows(arrows: unknown, site: (property: string) => CanonSite): [string | null, string | null] {
913
- const end = (name: string, property: string): string | null => {
1723
+ function visArrows(
1724
+ arrows: unknown,
1725
+ site: (property: string) => CanonSite,
1726
+ ): [string | null, string | null, string | null] {
1727
+ // a middle arrow the file does not mention is no rule at all, so a file that names only its ends carries none
1728
+ const end = (name: string, property: string, absent: "none" | null = "none"): string | null => {
914
1729
  if (typeof arrows === "string") {
915
- return new RegExp(String.raw`\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : "none";
1730
+ return new RegExp(String.raw`\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : absent;
916
1731
  }
917
1732
  if (!isJsonObject(arrows)) {
918
1733
  return null;
@@ -921,12 +1736,15 @@ function visArrows(arrows: unknown, site: (property: string) => CanonSite): [str
921
1736
  if (a === true) {
922
1737
  return "triangle";
923
1738
  }
1739
+ if (a === undefined) {
1740
+ return absent;
1741
+ }
924
1742
  if (!isJsonObject(a) || a.enabled === false) {
925
1743
  return "none";
926
1744
  }
927
1745
  return canonEnum("arrow", a.type ?? "arrow", "vis", site(property));
928
1746
  };
929
- return [end("to", "targetArrow"), end("from", "sourceArrow")];
1747
+ return [end("to", "targetArrow"), end("from", "sourceArrow"), end("middle", "midTargetArrow", null)];
930
1748
  }
931
1749
 
932
1750
  /**
@@ -961,13 +1779,17 @@ function visStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: Style
961
1779
 
962
1780
  // what vis-network draws per node: its own keys over its group over options.nodes
963
1781
  const node = valuesOf(n);
964
- const records = nodeRecords(snapshot);
965
- const rawShape = (layer: VisLayer): string | null => (typeof layer.shape === "string" ? layer.shape : null);
1782
+ const shapes: string[] = [];
1783
+ const rawRecords = elementRaw(snapshot, "node");
1784
+ const range = visNodeValueRange(n, groupOf, rawRecords, groupLayers, nodeDefaults);
1785
+ const ownGroup = visOwnGroup(groupLayers, visGroupDefs(src, options));
966
1786
  for (let i = 0; i < n; i++) {
967
1787
  const g = groupOf(i);
968
- const own = records(i);
969
- const group = g === null ? {} : (groupLayers.get(g) ?? {});
970
- const shape = rawShape(own) ?? rawShape(group) ?? rawShape(nodeDefaults) ?? "ellipse";
1788
+ const raw = rawRecords(i);
1789
+ const own = visLayer(raw, "node");
1790
+ const group = g === null ? {} : ownGroup(g, raw);
1791
+ const shape = visShapeOf(own) ?? visShapeOf(group) ?? visShapeOf(nodeDefaults) ?? "ellipse";
1792
+ shapes.push(shape);
971
1793
  const site = (property: string): CanonSite => ({
972
1794
  notes,
973
1795
  target: "node",
@@ -975,8 +1797,8 @@ function visStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: Style
975
1797
  element: i,
976
1798
  budget: context.budget,
977
1799
  });
978
- const merged = { ...nodeDefaults, ...group, ...own };
979
- for (const [p, v] of Object.entries(visValues(merged, "node", site, VIS_SIZED.has(shape)))) {
1800
+ const merged = visMergedNode(nodeDefaults, group, own, g !== null && raw.color !== undefined);
1801
+ for (const [p, v] of Object.entries(visNodeRow(merged, shape, range, site))) {
980
1802
  node.put(p, i, v);
981
1803
  }
982
1804
  }
@@ -984,43 +1806,417 @@ function visStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: Style
984
1806
  if (Object.keys(declaredNode).length > 0) {
985
1807
  nodeRules.push(rule("node.vis.defaults", "node", "defaults", "declared", { all: true }, declaredNode));
986
1808
  }
987
- let k = 0;
988
- for (const [g, layer] of groupLayers) {
989
- const set = paletteRows.get(g) ?? visValues(layer, "node", ruleSite("node"), true);
990
- if (Object.keys(set).length === 0) {
991
- continue;
992
- }
993
- const key = `group.${String(k)}`;
994
- nodeColumns.set(
995
- key,
996
- Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)),
997
- );
998
- const origin = paletteRows.has(g) ? "tool" : "declared";
999
- const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1000
- nodeRules.push({ ...r, facts: { names: [[key, g]] } });
1809
+ visGroupRules(n, groupOf, groupLayers, paletteRows, ruleSite("node"), nodeRules, nodeColumns);
1810
+
1811
+ const toolNode = toolDefaults("vis-network")?.node;
1812
+ const borderOf = (i: number): StyleValue =>
1813
+ node.values.borderColor?.[i] ?? (toolNode?.borderColor?.value as StyleValue | undefined) ?? "#2b7ce9";
1814
+ const geometry = visGeometry(snapshot, node.values, shapes);
1815
+ const support = visSupport(options.physics, options.layout);
1816
+ const edges = visLayer(options.edges, "edge");
1817
+ const edge = visEdges(snapshot, edges, borderOf, { ...geometry, support }, notes, context);
1818
+ const values = { node: node.values, edge: edge.values };
1819
+ exactFromBlock(snapshot, src.block, values, visWritten(snapshot, values.edge));
1820
+ return {
1821
+ canonical: true,
1822
+ dialect: "vis",
1823
+ tool: "vis-network",
1824
+ opacityInColor: true,
1825
+ units: "px",
1826
+ node: { values: values.node, rules: nodeRules, columns: nodeColumns },
1827
+ edge: { values: values.edge, rules: edge.rules, columns: edge.columns },
1828
+ notes: notes.list(),
1829
+ block: src.block,
1830
+ readFrom: {
1831
+ node: VIS_KEYS.node.filter((x) => snapshot.nodes.has(x)),
1832
+ edge: VIS_KEYS.edge.filter((x) => snapshot.edges.has(x)),
1833
+ },
1834
+ };
1835
+ }
1836
+
1837
+ /**
1838
+ * vis.js: a layer's own shape.
1839
+ * @param layer - the layer
1840
+ * @returns its `shape`, or null when it gives none
1841
+ */
1842
+ function visShapeOf(layer: VisLayer): string | null {
1843
+ return typeof layer.shape === "string" ? layer.shape : null;
1844
+ }
1845
+
1846
+ /**
1847
+ * vis.js: the smallest and largest `value` of the nodes, each its own over its group's over `options.nodes`.
1848
+ * @param n - the node count
1849
+ * @param groupOf - each node's group
1850
+ * @param rawRecords - each node's own keys
1851
+ * @param groupLayers - each group's layer (visGroupLayers)
1852
+ * @param nodeDefaults - `options.nodes`
1853
+ * @returns the range, or null when no node has a value
1854
+ */
1855
+ function visNodeValueRange(
1856
+ n: number,
1857
+ groupOf: (i: number) => string | null,
1858
+ rawRecords: (i: number) => Record<string, unknown>,
1859
+ groupLayers: ReadonlyMap<string, VisLayer>,
1860
+ nodeDefaults: VisLayer,
1861
+ ): readonly [number, number] | null {
1862
+ return visValueRange(n, (i) => {
1863
+ const g = groupOf(i);
1864
+ const own = visLayer(rawRecords(i), "node").value;
1865
+ const value = own ?? (g === null ? undefined : groupLayers.get(g)?.value) ?? nodeDefaults.value;
1866
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1867
+ });
1868
+ }
1869
+
1870
+ /**
1871
+ * vis.js: one node's layer, its own keys over its group's over `options.nodes`.
1872
+ * @param nodeDefaults - `options.nodes`
1873
+ * @param group - its group's layer
1874
+ * @param own - its own layer
1875
+ * @param ownColorInGroup - whether it is in a group and gives a color of its own
1876
+ * @returns the merged layer
1877
+ */
1878
+ function visMergedNode(nodeDefaults: VisLayer, group: VisLayer, own: VisLayer, ownColorInGroup: boolean): VisLayer {
1879
+ const merged: VisLayer = { ...nodeDefaults, ...group, ...own };
1880
+ if (ownColorInGroup) {
1881
+ // a color of the node's own fills its gaps from options.nodes and vis-network's defaults, never its group's
1882
+ merged.fill ??= VIS_NODE_FILL;
1883
+ merged.border ??= VIS_NODE_BORDER;
1884
+ }
1885
+ return merged;
1886
+ }
1887
+
1888
+ /**
1889
+ * vis.js: the values one node draws, from its merged layer: its colors and label, then its shape's own.
1890
+ * @param merged - the node's merged layer
1891
+ * @param shape - its vis-network shape
1892
+ * @param range - the smallest and largest `value` of the graph's nodes (visValueRange)
1893
+ * @param site - where its values are read
1894
+ * @returns the values
1895
+ */
1896
+ function visNodeRow(
1897
+ merged: VisLayer,
1898
+ shape: string,
1899
+ range: readonly [number, number] | null,
1900
+ site: (property: string) => CanonSite,
1901
+ ): Row {
1902
+ const sized = VIS_SIZED.has(shape);
1903
+ const row = visValues(merged, "node", site, sized);
1904
+ const shaped = sized
1905
+ ? visSizedShape(merged, shape, visRadius(merged, range, site), site)
1906
+ : visFitShape(merged, shape);
1907
+ if (sized && merged.label !== undefined) {
1908
+ const size =
1909
+ visLabelScaled(merged, range) ?? (typeof merged.fontSize === "number" ? merged.fontSize : VIS_FONT_SIZE);
1910
+ // a label in runs below the node hangs lower by half of what its larger runs make its lines taller
1911
+ const drop = typeof row.labelRuns === "string" ? visRunsDrop(row.labelRuns, size) : 0;
1912
+ const below = typeof row.labelOffsetY === "number" ? row.labelOffsetY : 0;
1913
+ row.labelOffsetY = below + drop + visBelowGap(size);
1914
+ }
1915
+ // the shape's own label place, then font.vadjust below it
1916
+ for (const p of ["labelOffsetX", "labelOffsetY"] as const) {
1917
+ const [a, b] = [row[p], shaped[p]];
1918
+ if (typeof a === "number" && typeof b === "number") {
1919
+ shaped[p] = a + b;
1920
+ }
1921
+ }
1922
+ Object.assign(row, shaped);
1923
+ const labelSize = visLabelScaled(merged, range);
1924
+ if (labelSize !== null) {
1925
+ row.labelSize = labelSize;
1926
+ }
1927
+ return row;
1928
+ }
1929
+
1930
+ /**
1931
+ * vis.js: one rule per node group with values, over a column marking the group's nodes; a group the file does not
1932
+ * define is the tool's palette.
1933
+ * @param n - the node count
1934
+ * @param groupOf - each node's group
1935
+ * @param groupLayers - each group's layer (visGroupLayers)
1936
+ * @param paletteRows - the palette values of the groups the file does not define
1937
+ * @param site - where a group's values are read
1938
+ * @param rules - the node rules, appended to
1939
+ * @param columns - the node columns, written to
1940
+ */
1941
+ function visGroupRules(
1942
+ n: number,
1943
+ groupOf: (i: number) => string | null,
1944
+ groupLayers: ReadonlyMap<string, VisLayer>,
1945
+ paletteRows: ReadonlyMap<string, Row>,
1946
+ site: (property: string) => CanonSite,
1947
+ rules: StyleRule[],
1948
+ columns: Map<string, StyleColumn>,
1949
+ ): void {
1950
+ let k = 0;
1951
+ for (const [g, layer] of groupLayers) {
1952
+ const set = paletteRows.get(g) ?? visValues(layer, "node", site, true);
1953
+ if (Object.keys(set).length === 0) {
1954
+ continue;
1955
+ }
1956
+ const key = `group.${String(k)}`;
1957
+ columns.set(
1958
+ key,
1959
+ Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)),
1960
+ );
1961
+ const origin = paletteRows.has(g) ? "tool" : "declared";
1962
+ const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1963
+ rules.push({ ...r, facts: { names: [[key, g]] } });
1964
+ }
1965
+ }
1966
+
1967
+ /**
1968
+ * vis.js: what a node of a shape sized by `size` draws besides its colors: its box from its radius, its picture or
1969
+ * glyph, and triangleDown turned over.
1970
+ * @param merged - the node's merged layer
1971
+ * @param shape - its vis-network shape
1972
+ * @param radius - its radius (visRadius)
1973
+ * @param site - where its values are read
1974
+ * @returns the values
1975
+ */
1976
+ function visSizedShape(merged: VisLayer, shape: string, radius: number, site: (property: string) => CanonSite): Row {
1977
+ // vis-network's hexagon is regular, its corners `size` from its center on the left and right (drawHexagon): it is
1978
+ // 2 * size wide and sqrt(3) * size tall, where the neutral hexagon fills its box
1979
+ const row: Row = { width: 2 * radius, height: 2 * radius };
1980
+ if (shape === "hexagon") {
1981
+ // its label still hangs below the 2 * size square (ShapeBase._drawShape: y + 0.5 * height + half the label)
1982
+ row.height = Math.sqrt(3) * radius;
1983
+ row.labelOffsetY = ((2 - Math.sqrt(3)) * radius) / 2;
1984
+ }
1985
+ Object.assign(row, visImage(merged, shape, { width: 2 * radius, height: 2 * radius }));
1986
+ if (shape === "icon") {
1987
+ // an icon node is its glyph alone: vis-network draws no body, no fill and no border for it (Icon.draw)
1988
+ Object.assign(row, { shape: "none" }, visIcon(merged, site("glyphColor")));
1989
+ }
1990
+ // vis-network's triangleDown: its triangle turned over, a corner down
1991
+ if (shape === "triangleDown") {
1992
+ row.shape = "polygon";
1993
+ row.shapePoints = "-1,1 0,-1 1,1";
1994
+ }
1995
+ return row;
1996
+ }
1997
+
1998
+ /**
1999
+ * vis.js: what a node of a shape sized by its label draws besides its colors: its growth round its label, nothing at
2000
+ * all with a margin of 0, and its label at the top or bottom under `heightConstraint.valign`.
2001
+ * @param merged - the node's merged layer
2002
+ * @param shape - its vis-network shape
2003
+ * @returns the values
2004
+ */
2005
+ function visFitShape(merged: VisLayer, shape: string): Row {
2006
+ const row = visLabelFit(merged, shape);
2007
+ // a margin of 0 leaves vis-network's margins undefined (`_setMargins` tests `if (options.margin)`), so the node's
2008
+ // size is NaN and neither it nor its label is drawn
2009
+ if (merged.margin === 0 && (shape === "box" || shape === "circle" || shape === "database")) {
2010
+ row.visible = false;
2011
+ }
2012
+ // heightConstraint.valign moves the label to the top or the bottom of a node taller than it (Label._drawText)
2013
+ if (merged.valign === "top" || merged.valign === "bottom") {
2014
+ row.labelPosition = `inside-${merged.valign}`;
2015
+ } else if (shape === "database") {
2016
+ // a database draws its label at its top, its top margin down (Database.draw: this.top + margin.top), not in
2017
+ // the middle of the square it grows to
2018
+ const m = merged.margin;
2019
+ const top = isJsonObject(m) ? m.top : m;
2020
+ row.labelPosition = "inside-top";
2021
+ row.labelOffsetY = typeof top === "number" && Number.isFinite(top) && top >= 0 ? top : VIS_MARGIN;
2022
+ }
2023
+ // a box's corners are rounded at `shapeProperties.borderRadius`, at most half its shorter side (drawRoundRect)
2024
+ if (shape === "box" && typeof merged.borderRadius === "number" && Number.isFinite(merged.borderRadius)) {
2025
+ row.cornerRadius = Math.max(0, merged.borderRadius);
2026
+ }
2027
+ return row;
2028
+ }
2029
+
2030
+ /**
2031
+ * 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
2032
+ * least `widthConstraint.minimum` by `heightConstraint.minimum`, plus the `margin` on each side is the node's width
2033
+ * and height; a circle and a database stay as wide as they are tall; an ellipse is the box's height taller and
2034
+ * wider instead of the margin (`width + height` by twice the height).
2035
+ * @param layer - the node's merged layer
2036
+ * @param shape - its vis-network shape
2037
+ * @returns the values, none for a shape not sized by its label
2038
+ */
2039
+ function visLabelFit(layer: VisLayer, shape: string): Row {
2040
+ const fit = VIS_LABEL_FIT.get(shape);
2041
+ if (fit === undefined) {
2042
+ return {};
2043
+ }
2044
+ const length = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
2045
+ const fontSize = length(layer.fontSize) ?? VIS_FONT_SIZE;
2046
+ const lines = typeof layer.label === "string" ? Math.max(1, layer.label.split("\n").length) : 1;
2047
+ const minW = length(layer.widthMin) ?? 0;
2048
+ const minH = length(layer.heightMin) ?? 0;
2049
+ let padX: number;
2050
+ let padY: number;
2051
+ let offset: Row = {};
2052
+ if (fit.ellipse) {
2053
+ padX = Math.max(lines * fontSize, minH) / 2;
2054
+ padY = padX;
2055
+ } else {
2056
+ ({ padX, padY, offset } = visMarginPads(layer.margin, fit.square));
2057
+ }
2058
+ // a node with no label text is sized as if its label were 14 x 14 (NodeBase.getDimensionsFromLabel)
2059
+ const empty = typeof layer.label !== "string" || layer.label === "";
2060
+ const [boxW, boxH] = empty ? [Math.max(minW, 14), Math.max(minH, 14)] : [minW, minH];
2061
+ if (empty && fit.ellipse) {
2062
+ padX = boxH / 2;
2063
+ padY = padX;
2064
+ }
2065
+ const row: Row = {
2066
+ ...offset,
2067
+ labelFit: "bounds",
2068
+ labelPaddingX: padX,
2069
+ labelPaddingY: padY,
2070
+ width: boxW > 0 ? boxW + 2 * padX : 0,
2071
+ height: boxH > 0 ? boxH + 2 * padY : 0,
2072
+ };
2073
+ if (fit.square) {
2074
+ row.labelFitSquare = true;
2075
+ }
2076
+ return row;
2077
+ }
2078
+
2079
+ /**
2080
+ * vis.js: the padding a node's `margin` puts round its label (visLabelFit). A box or text node draws its label its
2081
+ * left and top margins in from its corner (Box.draw, Text.draw), off center by half the difference of opposite
2082
+ * margins.
2083
+ * @param m - the `margin`: one number for every side, or `{ top, right, bottom, left }`
2084
+ * @param square - whether the shape stays as wide as it is tall
2085
+ * @returns the horizontal and vertical padding, and the label's offset
2086
+ */
2087
+ function visMarginPads(m: unknown, square: boolean): { padX: number; padY: number; offset: Row } {
2088
+ const length = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
2089
+ const side = (key: string): number => (isJsonObject(m) ? length(m[key]) : length(m)) ?? VIS_MARGIN;
2090
+ const offset: Row = {};
2091
+ if (!square && side("left") !== side("right")) {
2092
+ offset.labelOffsetX = (side("left") - side("right")) / 2;
2093
+ }
2094
+ if (!square && side("top") !== side("bottom")) {
2095
+ offset.labelOffsetY = (side("top") - side("bottom")) / 2;
2096
+ }
2097
+ return { padX: (side("left") + side("right")) / 2, padY: (side("top") + side("bottom")) / 2, offset };
2098
+ }
2099
+
2100
+ /**
2101
+ * vis.js `scaling.label`: with `enabled`, the font size of an element with a `value`, mapped as its size is from the
2102
+ * value range onto `scaling.label.min` .. `max` (14 .. 30) (vis-network 9.1.9 `Node.setValueRange`, `Edge.setValueRange`).
2103
+ * @param layer - the element's merged layer
2104
+ * @param range - the value range, or null
2105
+ * @returns the font size, or null when the label is not scaled
2106
+ */
2107
+ function visLabelScaled(layer: VisLayer, range: readonly [number, number] | null): number | null {
2108
+ const value = finiteValue(layer.value);
2109
+ const label = layer.scalingLabel;
2110
+ if (value === null || range === null || !isJsonObject(label) || label.enabled !== true) {
2111
+ return null;
2112
+ }
2113
+ const min = typeof label.min === "number" ? label.min : 14;
2114
+ const max = typeof label.max === "number" ? label.max : 30;
2115
+ return visScaled(value, range, min, max);
2116
+ }
2117
+
2118
+ /** vis-network's default node size, a radius (`size: 25`, vis-network.esm.js NodesHandler defaults). */
2119
+ const VIS_SIZE = 25;
2120
+ /** vis-network's default `scaling.min` and `scaling.max`, the radii `value` is mapped onto. */
2121
+ const VIS_SCALING: readonly [number, number] = [10, 30];
2122
+
2123
+ /**
2124
+ * vis.js: the smallest and largest `value` of the nodes that have one, as vis-network's
2125
+ * `_updateValueRange` takes them; null when no node has one.
2126
+ * @param n - the node count
2127
+ * @param valueOf - each node's value
2128
+ * @returns the range
2129
+ */
2130
+ function visValueRange(n: number, valueOf: (i: number) => number | null): readonly [number, number] | null {
2131
+ let min = Infinity;
2132
+ let max = -Infinity;
2133
+ for (let i = 0; i < n; i++) {
2134
+ const v = valueOf(i);
2135
+ if (v !== null) {
2136
+ min = Math.min(min, v);
2137
+ max = Math.max(max, v);
2138
+ }
2139
+ }
2140
+ return min <= max ? [min, max] : null;
2141
+ }
2142
+
2143
+ /**
2144
+ * A `value` that is a finite number, else null.
2145
+ * @param value - the value
2146
+ * @returns the number, or null
2147
+ */
2148
+ const finiteValue = (value: unknown): number | null =>
2149
+ typeof value === "number" && Number.isFinite(value) ? value : null;
2150
+
2151
+ /**
2152
+ * vis.js: `value` mapped linearly from the value range onto `min` .. `max`, the middle when every value is the same
2153
+ * (vis-network's default `customScalingFunction`).
2154
+ * @param value - the element's value
2155
+ * @param range - the value range
2156
+ * @param min - `scaling.min`
2157
+ * @param max - `scaling.max`
2158
+ * @returns the scaled value
2159
+ */
2160
+ function visScaled(value: number, range: readonly [number, number], min: number, max: number): number {
2161
+ const [lo, hi] = range;
2162
+ const scale = hi === lo ? 0.5 : Math.max(0, (value - lo) / (hi - lo));
2163
+ return min + scale * (max - min);
2164
+ }
2165
+
2166
+ /** vis-network's default edge `scaling.min` and `scaling.max`, the widths `value` is mapped onto. */
2167
+ const VIS_EDGE_SCALING: readonly [number, number] = [1, 15];
2168
+
2169
+ /**
2170
+ * vis.js: the width vis-network draws an edge at once any edge has a `value`. Its `_updateValueRange` then sets every
2171
+ * edge's width: one with a value to the value mapped onto `scaling`, one without to the width it was created with,
2172
+ * which is `options.edges.width` -- an edge's own `width` is dropped (`Edge.baseWidth` is read before the edge's own
2173
+ * options are applied, and nothing updates it).
2174
+ * @param layer - the edge's merged layer
2175
+ * @param defaults - `options.edges`
2176
+ * @param range - the value range over the edges, or null when no edge has a value
2177
+ * @param site - where its values are read
2178
+ * @returns the width, or nothing when no edge has a value (the edge's own width holds)
2179
+ */
2180
+ function visEdgeWidth(
2181
+ layer: VisLayer,
2182
+ defaults: VisLayer,
2183
+ range: readonly [number, number] | null,
2184
+ site: (property: string) => CanonSite,
2185
+ ): { width?: number } {
2186
+ if (range === null) {
2187
+ return {};
2188
+ }
2189
+ const value = finiteValue(layer.value);
2190
+ if (value !== null) {
2191
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_EDGE_SCALING[0];
2192
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_EDGE_SCALING[1];
2193
+ return { width: visScaled(value, range, min, max) };
1001
2194
  }
2195
+ return { width: (defaults.width === undefined ? null : canonNumber(defaults.width, site("width"))) ?? 1 };
2196
+ }
1002
2197
 
1003
- const toolNode = toolDefaults("vis-network")?.node;
1004
- const borderOf = (i: number): StyleValue =>
1005
- node.values.borderColor?.[i] ?? (toolNode?.borderColor?.value as StyleValue | undefined) ?? "#2b7ce9";
1006
- const edge = visEdges(snapshot, visLayer(options.edges, "edge"), borderOf, notes, context);
1007
- const values = { node: node.values, edge: edge.values };
1008
- exactFromBlock(snapshot, src.block, values, VIS_WRITTEN);
1009
- return {
1010
- canonical: true,
1011
- dialect: "vis",
1012
- tool: "vis-network",
1013
- opacityInColor: true,
1014
- units: "px",
1015
- node: { values: values.node, rules: nodeRules, columns: nodeColumns },
1016
- edge: { values: values.edge, rules: edge.rules, columns: edge.columns },
1017
- notes: notes.list(),
1018
- block: src.block,
1019
- readFrom: {
1020
- node: VIS_KEYS.node.filter((x) => snapshot.nodes.has(x)),
1021
- edge: VIS_KEYS.edge.filter((x) => snapshot.edges.has(x)),
1022
- },
1023
- };
2198
+ /**
2199
+ * vis.js: the radius vis-network draws a node of a sized shape at: its `value` mapped linearly from the value range
2200
+ * onto `scaling.min` .. `scaling.max` (the middle when every value is the same), else its `size`, else 25
2201
+ * (vis-network's `setValueRange` and `customScalingFunction`).
2202
+ * @param layer - the node's merged layer
2203
+ * @param range - the value range over the nodes
2204
+ * @param site - where its values are read
2205
+ * @returns the radius
2206
+ */
2207
+ function visRadius(
2208
+ layer: VisLayer,
2209
+ range: readonly [number, number] | null,
2210
+ site: (property: string) => CanonSite,
2211
+ ): number {
2212
+ const value = finiteValue(layer.value);
2213
+ if (value !== null && range !== null) {
2214
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_SCALING[0];
2215
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_SCALING[1];
2216
+ return visScaled(value, range, min, max);
2217
+ }
2218
+ const size = layer.size === undefined ? null : canonNumber(layer.size, site("width"));
2219
+ return size ?? VIS_SIZE;
1024
2220
  }
1025
2221
 
1026
2222
  /**
@@ -1068,12 +2264,49 @@ function visGroupLayers(
1068
2264
  return { groupLayers, paletteRows };
1069
2265
  }
1070
2266
 
2267
+ /**
2268
+ * vis.js: the group layer one node takes. vis-network merges a group's options into a node only for the keys the node
2269
+ * does not set itself, each key whole (vis-network 9.1.9 `Node.updateGroupOptions`: every option of the node's own is
2270
+ * skipped, `font` always), so a node with a `color` of its own takes none of its group's colors, not even a border it
2271
+ * leaves out.
2272
+ * @param groupLayers - each group's whole layer (visGroupLayers)
2273
+ * @param groupDefs - the group definitions the file gives
2274
+ * @returns the layer of a group for a node of the given own keys
2275
+ */
2276
+ function visOwnGroup(
2277
+ groupLayers: ReadonlyMap<string, VisLayer>,
2278
+ groupDefs: Record<string, unknown>,
2279
+ ): (g: string, own: Record<string, unknown>) => VisLayer {
2280
+ const cache = new Map<string, VisLayer>();
2281
+ return (g, own) => {
2282
+ const whole = groupLayers.get(g) ?? {};
2283
+ const def = Object.hasOwn(groupDefs, g) ? groupDefs[g] : null;
2284
+ // a group the file does not define is vis-network's palette color, which a color of the node's own replaces
2285
+ const skip = Object.keys(own).filter(
2286
+ (k) => k !== "font" && (isJsonObject(def) ? Object.hasOwn(def, k) : k === "color"),
2287
+ );
2288
+ if (skip.length === 0) {
2289
+ return whole;
2290
+ }
2291
+ const key = `${g}\u0000${skip.join("\u0000")}`;
2292
+ let layer = cache.get(key);
2293
+ if (layer === undefined) {
2294
+ layer = isJsonObject(def)
2295
+ ? visLayer(Object.fromEntries(Object.entries(def).filter(([k]) => !skip.includes(k))), "node")
2296
+ : {};
2297
+ cache.set(key, layer);
2298
+ }
2299
+ return layer;
2300
+ };
2301
+ }
2302
+
1071
2303
  /**
1072
2304
  * vis.js edges: their own keys over `options.edges`; an edge without a color of its own takes its source node's
1073
2305
  * border color (its target's under `inherit: "to"`).
1074
2306
  * @param snapshot - the graph
1075
2307
  * @param edgeDefaults - `options.edges`
1076
2308
  * @param borderOf - each node's drawn border color
2309
+ * @param geometry - the nodes' positions and boxes, for the edges' routes
1077
2310
  * @param notes - where problems are noted
1078
2311
  * @param context - the work budget
1079
2312
  * @returns the edge values, rules and style columns
@@ -1082,6 +2315,7 @@ function visEdges(
1082
2315
  snapshot: GraphSnapshot,
1083
2316
  edgeDefaults: VisLayer,
1084
2317
  borderOf: (node: number) => StyleValue,
2318
+ geometry: VisGeometry,
1085
2319
  notes: StyleNotes,
1086
2320
  context: StyleReadContext,
1087
2321
  ): { values: Values; rules: StyleRule[]; columns: Map<string, StyleColumn> } {
@@ -1090,9 +2324,17 @@ function visEdges(
1090
2324
  const edgeRecords = elementRecords(snapshot, "edge");
1091
2325
  const inherited: (StyleValue | null)[] = new Array<StyleValue | null>(m).fill(null);
1092
2326
  let inheriting = 0;
2327
+ const range = visValueRange(m, (e) => finiteValue({ ...edgeDefaults, ...edgeRecords(e) }.value));
2328
+ // vis-network sets round caps to draw a dashed line (EdgeBase._drawDashedLine) and never sets them back, so every
2329
+ // band drawn with or after a dashed edge, in the file's order, has round caps, which close gaps narrower than the
2330
+ // band
2331
+ let roundCaps = false;
1093
2332
  for (let e = 0; e < m; e++) {
1094
2333
  const own = edgeRecords(e);
1095
- const merged = { ...edgeDefaults, ...own };
2334
+ // an array of `options.edges.dashes` reaches the edge through bridgeObject as a plain object, no longer an
2335
+ // array, so vis-network draws its default [5, 5] (EdgeBase.setStrokeDashed)
2336
+ const bridged = own.dashes === undefined && Array.isArray(edgeDefaults.dashes) ? { dashes: true } : {};
2337
+ const merged = { ...edgeDefaults, ...own, ...bridged };
1096
2338
  const site = (property: string): CanonSite => ({
1097
2339
  notes,
1098
2340
  target: "edge",
@@ -1100,24 +2342,83 @@ function visEdges(
1100
2342
  element: e,
1101
2343
  budget: context.budget,
1102
2344
  });
1103
- const vals = visValues(merged, "edge", site, true);
2345
+ const [s, t] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
2346
+ const vals: Row = {
2347
+ ...visValues(merged, "edge", site, true),
2348
+ ...visEdgeWidth(merged, edgeDefaults, range, site),
2349
+ ...visRoute(edgeDefaults, own, s, t, geometry),
2350
+ ...visEdgeLabel(merged, range, geometry.at(s), geometry.at(t)),
2351
+ };
2352
+ // a dashed edge sets them before it draws its own band (BezierEdgeBase._bezierCurve draws the band first)
2353
+ roundCaps ||= merged.hidden !== true && ![undefined, false, "false"].includes(merged.dashes as never);
2354
+ const band = roundCaps && isJsonObject(merged.background) ? visBandStyle(merged.background, true) : null;
2355
+ if (band !== null) {
2356
+ vals.outlineStyle = band;
2357
+ }
2358
+ const from = visEdgeInherit(vals, own.inherit ?? edgeDefaults.inherit ?? "from", borderOf, s, t);
1104
2359
  for (const [p, v] of Object.entries(vals)) {
1105
2360
  edge.put(p, e, v);
1106
2361
  }
1107
- const inherit = own.inherit ?? edgeDefaults.inherit ?? "from";
1108
- if (inherit === false || inherit === "false") {
1109
- if (vals.color === undefined) {
1110
- edge.put("color", e, VIS_EDGE_COLOR);
1111
- }
1112
- continue;
1113
- }
1114
- if (inherit === "both") {
1115
- notes.add(STYLE_ISSUE.GRADIENT, "edge", "color", { sample: "inherit both", element: e });
2362
+ if (from !== null) {
2363
+ inherited[e] = from;
2364
+ inheriting++;
1116
2365
  }
1117
- inherited[e] = borderOf(inherit === "to" ? snapshot.edgeTarget(e) : snapshot.edgeSource(e));
1118
- inheriting++;
1119
2366
  }
1120
2367
  context.budget.spend("edge", m);
2368
+ const { rules, columns } = visEdgeRules(edgeDefaults, notes, inherited, inheriting);
2369
+ return { values: edge.values, rules, columns };
2370
+ }
2371
+
2372
+ /**
2373
+ * vis.js: an edge's `inherit`, applied to its values: an edge that inherits draws its node's border color, not a
2374
+ * color `options.edges` gives (that only stops inheriting there), and under "both" a gradient between its nodes'; one
2375
+ * that does not inherit and has no color draws vis-network's default.
2376
+ * @param vals - the edge's values, written in place
2377
+ * @param inherit - its `inherit`
2378
+ * @param borderOf - each node's drawn border color
2379
+ * @param s - its source node
2380
+ * @param t - its target node
2381
+ * @returns the color it inherits, or null for none
2382
+ */
2383
+ function visEdgeInherit(
2384
+ vals: Row,
2385
+ inherit: unknown,
2386
+ borderOf: (node: number) => StyleValue,
2387
+ s: number,
2388
+ t: number,
2389
+ ): StyleValue | null {
2390
+ const inherits = inherit !== false && inherit !== "false";
2391
+ if (inherits) {
2392
+ delete vals.color;
2393
+ }
2394
+ if (inherit === "both") {
2395
+ // vis-network: a linear gradient from the source node's border color to the target's
2396
+ vals.gradientKind = "linear";
2397
+ vals.gradientColors = `${String(borderOf(s))} ${String(borderOf(t))}`;
2398
+ }
2399
+ if (!inherits) {
2400
+ if (vals.color === undefined) {
2401
+ vals.color = VIS_EDGE_COLOR;
2402
+ }
2403
+ return null;
2404
+ }
2405
+ return borderOf(inherit === "to" ? t : s);
2406
+ }
2407
+
2408
+ /**
2409
+ * vis.js: the edge rules: `options.edges` as declared, and the colors edges inherit from their nodes.
2410
+ * @param edgeDefaults - `options.edges`
2411
+ * @param notes - where problems are noted
2412
+ * @param inherited - each edge's inherited color, or null
2413
+ * @param inheriting - how many edges inherit one
2414
+ * @returns the rules and their style columns
2415
+ */
2416
+ function visEdgeRules(
2417
+ edgeDefaults: VisLayer,
2418
+ notes: StyleNotes,
2419
+ inherited: (StyleValue | null)[],
2420
+ inheriting: number,
2421
+ ): { rules: StyleRule[]; columns: Map<string, StyleColumn> } {
1121
2422
  const edgeRules: StyleRule[] = [];
1122
2423
  const edgeColumns = new Map<string, StyleColumn>();
1123
2424
  const declaredEdge = visValues(edgeDefaults, "edge", (property) => ({ notes, target: "edge", property }), true);
@@ -1139,24 +2440,532 @@ function visEdges(
1139
2440
  facts: { elements: inheriting, reason: "SOURCE_NODE_COLOR" },
1140
2441
  });
1141
2442
  }
1142
- return { values: edge.values, rules: edgeRules, columns: edgeColumns };
2443
+ return { rules: edgeRules, columns: edgeColumns };
1143
2444
  }
1144
2445
 
1145
- /** The vis.js keys the reader reads, per target. */
1146
- const VIS_KEYS: Readonly<Record<StyleTarget, readonly string[]>> = {
1147
- node: ["color", "shape", "size", "borderWidth", "opacity", "hidden", "label", "title", "font"],
1148
- edge: ["color", "width", "dashes", "arrows", "hidden", "label", "title", "font"],
1149
- };
2446
+ /** What a vis.js edge's route is drawn from: its nodes' positions and boxes. */
2447
+ interface VisGeometry {
2448
+ /** A node's position as vis-network draws it (`parseInt` of `x` and `y`, y down), or null. */
2449
+ readonly at: (i: number) => readonly [number, number] | null;
2450
+ /** A node's half width, half height, border width and vis-network shape, or null for a node sized by its label. */
2451
+ readonly box: (i: number) => VisBox | null;
2452
+ /** The forces vis-network's physics moves a dynamic edge's support node by, or null where nothing moves it. */
2453
+ readonly support?: Row | null;
2454
+ }
2455
+
2456
+ /** A node's half width, half height, border width and vis-network shape. */
2457
+ type VisBox = readonly [number, number, number, string];
1150
2458
 
1151
2459
  /**
1152
- * Each node's own vis.js keys as a layer.
2460
+ * vis.js: the positions and boxes of the nodes, for the routes of their edges.
1153
2461
  * @param snapshot - the graph
1154
- * @returns the layer of a node
2462
+ * @param nodeValues - the node values read so far (sizes and border widths)
2463
+ * @param shapes - each node's vis-network shape
2464
+ * @returns the geometry
2465
+ */
2466
+ function visGeometry(snapshot: GraphSnapshot, nodeValues: Values, shapes: readonly string[]): VisGeometry {
2467
+ const whole = (v: unknown): number => {
2468
+ if (typeof v === "string") {
2469
+ return Number.parseInt(v, 10);
2470
+ }
2471
+ return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : Number.NaN;
2472
+ };
2473
+ const [xs, ys] = [cells(snapshot, "node", "x"), cells(snapshot, "node", "y")];
2474
+ const num = (v: StyleValue | null | undefined): number | null => (typeof v === "number" && v > 0 ? v : null);
2475
+ return {
2476
+ at: (i) => {
2477
+ const p = [whole(xs?.(i)), whole(ys?.(i))] as const;
2478
+ return Number.isNaN(p[0]) || Number.isNaN(p[1]) ? null : p;
2479
+ },
2480
+ box: (i) => {
2481
+ const [w, h] = [num(nodeValues.width?.[i]), num(nodeValues.height?.[i])];
2482
+ const bw = nodeValues.borderWidth?.[i];
2483
+ return w === null || h === null ? null : [w / 2, h / 2, typeof bw === "number" ? bw : 1, shapes[i]];
2484
+ },
2485
+ };
2486
+ }
2487
+
2488
+ /** vis-network's `smooth` with every layer applied: booleans set `enabled`, objects their own fields. */
2489
+ interface VisSmooth {
2490
+ enabled: boolean;
2491
+ type: string;
2492
+ roundness: number;
2493
+ forceDirection: unknown;
2494
+ }
2495
+
2496
+ /**
2497
+ * vis.js: an edge's `smooth` over `options.edges.smooth` over vis-network's defaults (dynamic, roundness 0.5).
2498
+ * @param layers - the layers, the lowest first
2499
+ * @returns the smooth options
2500
+ */
2501
+ function visSmooth(...layers: unknown[]): VisSmooth {
2502
+ const s: VisSmooth = { enabled: true, type: "dynamic", roundness: 0.5, forceDirection: "none" };
2503
+ for (const l of layers) {
2504
+ if (typeof l === "boolean") {
2505
+ s.enabled = l;
2506
+ } else if (isJsonObject(l)) {
2507
+ s.enabled = typeof l.enabled === "boolean" ? l.enabled : s.enabled;
2508
+ s.type = typeof l.type === "string" ? l.type : s.type;
2509
+ s.roundness = typeof l.roundness === "number" && Number.isFinite(l.roundness) ? l.roundness : s.roundness;
2510
+ s.forceDirection = l.forceDirection ?? s.forceDirection;
2511
+ }
2512
+ }
2513
+ return s;
2514
+ }
2515
+
2516
+ /**
2517
+ * vis.js: an edge label's size under `scaling.label` and its turn along the line.
2518
+ * @param merged - the edge's merged layer
2519
+ * @param range - the edges' value range
2520
+ * @param a - the source node's center, or null
2521
+ * @param b - the target node's center, or null
2522
+ * @returns the values
2523
+ */
2524
+ function visEdgeLabel(
2525
+ merged: VisLayer,
2526
+ range: readonly [number, number] | null,
2527
+ a: VisPoint | null,
2528
+ b: VisPoint | null,
2529
+ ): Row {
2530
+ const row: Row = {};
2531
+ const size = visLabelScaled(merged, range);
2532
+ if (size !== null) {
2533
+ row.labelSize = size;
2534
+ }
2535
+ const turn = visLabelTurn(merged.fontAlign, a, b);
2536
+ if (turn !== null) {
2537
+ row.labelRotation = turn;
2538
+ }
2539
+ return row;
2540
+ }
2541
+
2542
+ /**
2543
+ * vis.js: how far an edge label whose `font.align` is not "horizontal" is turned to run along its edge, kept readable
2544
+ * (vis-network 9.1.9 `Edge._getRotation`), in degrees clockwise on screen.
2545
+ * @param align - `font.align`
2546
+ * @param a - the source node's center, or null
2547
+ * @param b - the target node's center, or null
2548
+ * @returns the turn, or null for a level label
2549
+ */
2550
+ function visLabelTurn(align: unknown, a: VisPoint | null, b: VisPoint | null): number | null {
2551
+ if (typeof align !== "string" || align === "horizontal" || a === null || b === null) {
2552
+ return null;
2553
+ }
2554
+ const [dx, dy] = [a[0] - b[0], a[1] - b[1]];
2555
+ let angle = Math.atan2(dy, dx);
2556
+ if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
2557
+ angle += Math.PI;
2558
+ }
2559
+ const degrees = Math.round(((angle * 180) / Math.PI) * 1e4) / 1e4;
2560
+ return degrees < 0 ? degrees + 360 : degrees;
2561
+ }
2562
+
2563
+ /** A point, y down. */
2564
+ type VisPoint = readonly [number, number];
2565
+
2566
+ /**
2567
+ * vis-network's control point of a static smooth curve (9.1.9 `BezierEdgeStatic._getViaCoordinates`), from the two
2568
+ * node centers.
2569
+ * @param from - the source node's center
2570
+ * @param to - the target node's center
2571
+ * @param smooth - the smooth options
2572
+ * @returns the control point
2573
+ */
2574
+ function visStaticVia(from: VisPoint, to: VisPoint, smooth: VisSmooth): VisPoint {
2575
+ const f = smooth.roundness;
2576
+ const [fx, fy] = from;
2577
+ const [tx, ty] = to;
2578
+ const dx = Math.abs(fx - tx);
2579
+ const dy = Math.abs(fy - ty);
2580
+ // the step a discrete, diagonalCross or continuous curve takes from its source, toward its target in x and in y
2581
+ // (away from it in y when level: `from.y >= to.y` steps up)
2582
+ const step = f * Math.max(dx, dy);
2583
+ const sx = step * (Math.sign(tx - fx) || 1);
2584
+ const sy = step * (Math.sign(ty - fy) || -1);
2585
+ // which way a cross, horizontal or vertical curve's point lies from the target, back toward the source
2586
+ const backX = (1 - f) * dx * (Math.sign(fx - tx) || 1);
2587
+ const backY = (1 - f) * dy * (Math.sign(fy - ty) || 1);
2588
+ switch (smooth.type) {
2589
+ case "discrete":
2590
+ return visDiscreteVia(from, [fx + sx, fy + sy], dx, dy, f);
2591
+ case "diagonalCross":
2592
+ return [fx + sx, fy + sy];
2593
+ case "straightCross":
2594
+ return dx <= dy ? [tx, ty + backY] : [tx + backX, ty];
2595
+ case "horizontal":
2596
+ return [tx + backX, fy];
2597
+ case "vertical":
2598
+ return [fx, ty + backY];
2599
+ case "curvedCW":
2600
+ return visCurvedVia(from, to, f, f * 0.5 + 0.5);
2601
+ case "curvedCCW":
2602
+ return visCurvedVia(from, to, f, -f * 0.5 + 0.5);
2603
+ default:
2604
+ // continuous: the diagonal step, no farther than the target along the longer axis
2605
+ return dx <= dy
2606
+ ? [fx + Math.sign(sx) * Math.min(step, dx), fy + sy]
2607
+ : [fx + sx, fy + Math.sign(sy) * Math.min(step, dy)];
2608
+ }
2609
+ }
2610
+
2611
+ /**
2612
+ * vis-network's discrete curve: the diagonal step, kept at the source's x (or y) when the nodes are nearly in line.
2613
+ * @param from - the source node's center
2614
+ * @param diagonal - the diagonal step's point
2615
+ * @param dx - the distance between the nodes in x
2616
+ * @param dy - the distance in y
2617
+ * @param f - the roundness
2618
+ * @returns the control point
2619
+ */
2620
+ function visDiscreteVia(from: VisPoint, diagonal: VisPoint, dx: number, dy: number, f: number): VisPoint {
2621
+ if (dx <= dy) {
2622
+ return [dx < f * dy ? from[0] : diagonal[0], diagonal[1]];
2623
+ }
2624
+ return [diagonal[0], dy < f * dx ? from[1] : diagonal[1]];
2625
+ }
2626
+
2627
+ /**
2628
+ * vis-network's curvedCW and curvedCCW: a point `(roundness / 2 + 1 / 2)` of the distance from the source, turned
2629
+ * `turn` half turns from the line to the target (vis-network's sin / cos convention, y down).
2630
+ * @param from - the source node's center
2631
+ * @param to - the target node's center
2632
+ * @param f - the roundness
2633
+ * @param turn - the turn, in half turns
2634
+ * @returns the control point
2635
+ */
2636
+ function visCurvedVia(from: VisPoint, to: VisPoint, f: number, turn: number): VisPoint {
2637
+ const ex = to[0] - from[0];
2638
+ const ey = from[1] - to[1];
2639
+ const r = (f * 0.5 + 0.5) * Math.hypot(ex, ey);
2640
+ const angle = (Math.atan2(ey, ex) + turn * Math.PI) % (2 * Math.PI);
2641
+ return [from[0] + r * Math.sin(angle), from[1] + r * Math.cos(angle)];
2642
+ }
2643
+
2644
+ /**
2645
+ * vis-network's two control points of a cubicBezier curve (9.1.9 `CubicBezierEdge._getViaCoordinates`): each
2646
+ * `roundness` of the way across from its own end, horizontally when the nodes are farther apart in x or
2647
+ * `forceDirection` is horizontal (or true), else vertically.
2648
+ * @param from - the source node's center
2649
+ * @param to - the target node's center
2650
+ * @param smooth - the smooth options
2651
+ * @returns the two control points
2652
+ */
2653
+ function visCubicVia(from: VisPoint, to: VisPoint, smooth: VisSmooth): [VisPoint, VisPoint] {
2654
+ const [dx, dy] = [from[0] - to[0], from[1] - to[1]];
2655
+ const r = smooth.roundness;
2656
+ const force = smooth.forceDirection;
2657
+ const across = (Math.abs(dx) > Math.abs(dy) || force === true || force === "horizontal") && force !== "vertical";
2658
+ return across
2659
+ ? [
2660
+ [from[0] - r * dx, from[1]],
2661
+ [to[0] + r * dx, to[1]],
2662
+ ]
2663
+ : [
2664
+ [from[0], from[1] - r * dy],
2665
+ [to[0], to[1] + r * dy],
2666
+ ];
2667
+ }
2668
+
2669
+ /**
2670
+ * A point as the neutral `bends` spell it: `along,across` relative to the line between two centers, y down in, y up
2671
+ * out.
2672
+ * @param a - the source center
2673
+ * @param b - the target center
2674
+ * @param p - the point
2675
+ * @returns `along,across`
2676
+ */
2677
+ function alongAcross(a: VisPoint, b: VisPoint, p: VisPoint): string {
2678
+ const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
2679
+ const [qx, qy] = [p[0] - a[0], p[1] - a[1]];
2680
+ const l2 = dx * dx + dy * dy;
2681
+ return `${fraction((qx * dx + qy * dy) / l2)},${fraction((dy * qx - dx * qy) / l2)}`;
2682
+ }
2683
+
2684
+ /**
2685
+ * A number rounded to four places for a bend, with no negative zero.
2686
+ * @param v - the number
2687
+ * @returns its text
2688
+ */
2689
+ const fraction = (v: number): string => String(Math.round(v * 1e4) / 1e4 || 0);
2690
+
2691
+ /**
2692
+ * vis.js: the route vis-network draws an edge along. A smooth edge is one Bezier curve through its control points
2693
+ * (`bendCurve: "bezier"`): a static type's one point, cubicBezier's two. A dynamic one (vis-network's default) is
2694
+ * drawn through a support node the physics simulation moves; where it never moves (physics off for the network or
2695
+ * the edge) it stays at the middle and the edge is straight. A self-loop is vis-network's circle of
2696
+ * `selfReference.size` (20) centered on its node's outline at `selfReference.angle` (pi / 4).
2697
+ * @param defaults - `options.edges`
2698
+ * @param own - the edge's own keys
2699
+ * @param s - its source node
2700
+ * @param t - its target node
2701
+ * @param g - the nodes' geometry
2702
+ * @returns the route's values, none for a straight edge or a route the file does not decide
2703
+ */
2704
+ function visRoute(defaults: VisLayer, own: VisLayer, s: number, t: number, g: VisGeometry): Row {
2705
+ if (s === t) {
2706
+ return visLoop(defaults, own, g.box(s));
2707
+ }
2708
+ const smooth = visSmooth(defaults.smooth, own.smooth);
2709
+ if (smooth.enabled && smooth.type === "dynamic") {
2710
+ return visDynamic(defaults, own, g.support ?? null);
2711
+ }
2712
+ const [a, b] = [g.at(s), g.at(t)];
2713
+ if (!smooth.enabled || a === null || b === null || (a[0] === b[0] && a[1] === b[1])) {
2714
+ return {};
2715
+ }
2716
+ const controls = smooth.type === "cubicBezier" ? visCubicVia(a, b, smooth) : [visStaticVia(a, b, smooth)];
2717
+ return { curved: true, bendCurve: "bezier", bends: controls.map((p) => alongAcross(a, b, p)).join(" ") };
2718
+ }
2719
+
2720
+ /**
2721
+ * vis.js: a dynamic smooth edge, drawn through the support node vis-network's physics places, wherever its nodes
2722
+ * are (they need no position in the file): its forces, with the edge's own `length` as its springs' rest length.
2723
+ * Straight where physics does not run for it (`physics: false` on the network or the edge), as the support node
2724
+ * then stays at the middle.
2725
+ * @param defaults - `options.edges`
2726
+ * @param own - the edge's own keys
2727
+ * @param support - the network's forces, or null
2728
+ * @returns the route's values
2729
+ */
2730
+ function visDynamic(defaults: VisLayer, own: VisLayer, support: Row | null): Row {
2731
+ if (support === null || own.physics === false || (own.physics === undefined && defaults.physics === false)) {
2732
+ return {};
2733
+ }
2734
+ const length = [own.length, defaults.length].find((v) => typeof v === "number" && Number.isFinite(v) && v >= 0);
2735
+ return { curved: true, ...support, ...(length === undefined ? {} : { supportSpringLength: length as number }) };
2736
+ }
2737
+
2738
+ /** vis-network 9.1.9's physics defaults for the two solvers whose forces a support node is drawn under. */
2739
+ const VIS_SOLVERS: Readonly<Record<string, { model: string; g: number; cg: number; length: number; k: number }>> = {
2740
+ barnesHut: { model: "barnesHut", g: -2000, cg: 0.3, length: 95, k: 0.04 },
2741
+ forceAtlas2Based: { model: "forceAtlas2", g: -50, cg: 0.01, length: 100, k: 0.08 },
2742
+ };
2743
+
2744
+ /**
2745
+ * vis.js: the forces a dynamic edge's support node settles under (9.1.9 BarnesHutSolver,
2746
+ * ForceAtlas2BasedRepulsionSolver, their central gravity solvers and SpringSolver). None where physics is off, where
2747
+ * a hierarchical layout turns every dynamic curve static (`_forceDisableDynamicCurves`), or under the repulsion and
2748
+ * hierarchicalRepulsion solvers (ponytail: drawn straight; their force laws when a file uses them).
2749
+ * @param physics - `options.physics`
2750
+ * @param layout - `options.layout`
2751
+ * @returns the neutral support values, or null
2752
+ */
2753
+ function visSupport(physics: unknown, layout: unknown): Row | null {
2754
+ const hierarchical = isJsonObject(layout) ? layout.hierarchical : undefined;
2755
+ if (hierarchical === true || (isJsonObject(hierarchical) && hierarchical.enabled !== false)) {
2756
+ return null;
2757
+ }
2758
+ if (physics === false || (isJsonObject(physics) && physics.enabled === false)) {
2759
+ return null;
2760
+ }
2761
+ const o = isJsonObject(physics) ? physics : {};
2762
+ const solver = typeof o.solver === "string" ? o.solver : "barnesHut";
2763
+ const base = VIS_SOLVERS[solver] as (typeof VIS_SOLVERS)[string] | undefined;
2764
+ if (base === undefined) {
2765
+ return null;
2766
+ }
2767
+ const given = isJsonObject(o[solver]) ? o[solver] : {};
2768
+ const num = (v: unknown, fallback: number): number => (typeof v === "number" && Number.isFinite(v) ? v : fallback);
2769
+ return {
2770
+ supportModel: base.model,
2771
+ supportSpringLength: Math.max(0, num(given.springLength, base.length)),
2772
+ supportSpringConstant: Math.max(0, num(given.springConstant, base.k)),
2773
+ supportRepulsion: Math.max(0, -num(given.gravitationalConstant, base.g)),
2774
+ supportCentralGravity: Math.max(0, num(given.centralGravity, base.cg)),
2775
+ };
2776
+ }
2777
+
2778
+ /**
2779
+ * vis.js: a self-loop, vis-network's circle (9.1.9 `EdgeBase._getCircleData`, `getSelfRefCoordinates`) of radius
2780
+ * `selfReference.size` (or the older `selfReferenceSize`; 20) centered on the node's outline (visToBorder) along
2781
+ * the ray at `selfReference.angle` (pi / 4, counterclockwise from the right). Written as bends at the corners of the
2782
+ * square round the circle, drawn through their midpoints, which run along the circle.
2783
+ * @param defaults - `options.edges`
2784
+ * @param own - the edge's own keys
2785
+ * @param box - the node's half width, half height, border width and shape, or null when unknown
2786
+ * @returns the loop's values, none for a node whose size is not known here
2787
+ */
2788
+ function visLoop(defaults: VisLayer, own: VisLayer, box: VisBox | null): Row {
2789
+ if (box === null) {
2790
+ return {};
2791
+ }
2792
+ const field = (layer: VisLayer, key: string): unknown =>
2793
+ isJsonObject(layer.selfReference) ? layer.selfReference[key] : undefined;
2794
+ const pick = (...vs: unknown[]): number | null =>
2795
+ (vs.find((v) => typeof v === "number" && Number.isFinite(v)) as number | undefined) ?? null;
2796
+ const radius =
2797
+ pick(field(own, "size"), own.selfReferenceSize, field(defaults, "size"), defaults.selfReferenceSize) ?? 20;
2798
+ const angle = pick(field(own, "angle"), field(defaults, "angle")) ?? Math.PI / 4;
2799
+ const [hw, hh] = box;
2800
+ const reach = visToBorder(box, angle);
2801
+ // the circle's center relative to the node's, y up
2802
+ const [cx, cy] = [Math.cos(angle) * reach, Math.sin(angle) * reach];
2803
+ const behind = [field(own, "renderBehindTheNode"), field(defaults, "renderBehindTheNode")].find(
2804
+ (v) => typeof v === "boolean",
2805
+ );
2806
+ if (behind === false) {
2807
+ return visFrontArc(cx, cy, radius, angle, box);
2808
+ }
2809
+ // the node's farthest reach from its center: its radius when round, its corner otherwise
2810
+ const round = box[3] === "dot" || box[3] === "circle" || box[3] === "circularImage";
2811
+ if (reach + (round ? hw : Math.hypot(hw, hh)) < radius) {
2812
+ return visRing(cx, cy, radius, angle, box);
2813
+ }
2814
+ // the corners of the square round the circle, the first the one facing the node and again last, so the curve
2815
+ // through their midpoints runs the whole circle and its ends, out of and back into the node, bulge as it does
2816
+ const corners: string[] = [];
2817
+ for (let k = 0; k <= 4; k++) {
2818
+ const a = angle + Math.PI + ((k % 4) * Math.PI) / 2;
2819
+ const [x, y] = [cx + Math.SQRT2 * radius * Math.cos(a), cy + Math.SQRT2 * radius * Math.sin(a)];
2820
+ corners.push(`${fraction(x / hw)},${fraction(y / hh)}`);
2821
+ }
2822
+ return { curved: true, bendCurve: "midpoints", bends: corners.join(" ") };
2823
+ }
2824
+
2825
+ /** How many points a vis-network self-loop's circle is written through where it does not meet its node. */
2826
+ const VIS_RING_STEPS = 96;
2827
+
2828
+ /**
2829
+ * vis.js: a self-loop whose circle holds its whole node, so meets it nowhere (a small node, or a large
2830
+ * `selfReference.size`): the circle alone, written as points round it from the side facing the node and back to it,
2831
+ * the first and the last the same point, which graphty draws as that closed ring with no line to the node.
2832
+ * @param cx - the circle's center across from the node's, y up
2833
+ * @param cy - and up from it
2834
+ * @param radius - its radius
2835
+ * @param angle - the ray its center is on, counterclockwise from the right
2836
+ * @param box - the node's half width and half height
2837
+ * @returns the loop's values
2838
+ */
2839
+ function visRing(cx: number, cy: number, radius: number, angle: number, box: VisBox): Row {
2840
+ const [hw, hh] = box;
2841
+ const points: string[] = [];
2842
+ for (let k = 0; k <= VIS_RING_STEPS; k++) {
2843
+ const a = angle + Math.PI + (2 * Math.PI * (k % VIS_RING_STEPS)) / VIS_RING_STEPS;
2844
+ points.push(`${fraction((cx + radius * Math.cos(a)) / hw)},${fraction((cy + radius * Math.sin(a)) / hh)}`);
2845
+ }
2846
+ return { curved: false, bends: points.join(" ") };
2847
+ }
2848
+
2849
+ /**
2850
+ * vis.js: a self-loop drawn in front of its node (`selfReference.renderBehindTheNode: false`): only the arc of its
2851
+ * circle between the two points vis-network 9.1.9 searches for (Edge._circle, `_findBorderPositionCircle`: ten
2852
+ * halvings of a "position" between the angle and the angle plus pi, read as a fraction of a turn), clockwise on screen
2853
+ * from the first to the second, written as points along it.
2854
+ * @param cx - the circle's center across from the node's, y up
2855
+ * @param cy - and up from it
2856
+ * @param radius - its radius
2857
+ * @param angle - `selfReference.angle`
2858
+ * @param box - the node's half width, half height, border width and shape
2859
+ * @returns the loop's values
2860
+ */
2861
+ function visFrontArc(cx: number, cy: number, radius: number, angle: number, box: VisBox): Row {
2862
+ // on screen, y down, the node's center at the origin
2863
+ const [x, y] = [cx, -cy];
2864
+ const search = (direction: -1 | 1): [number, number] => {
2865
+ let [low, high] = [angle, angle + Math.PI];
2866
+ let pos: [number, number] = [x, y];
2867
+ for (let iteration = 0; low <= high && iteration < 10; iteration++) {
2868
+ const middle = (low + high) * 0.5;
2869
+ const turn = middle * 2 * Math.PI;
2870
+ pos = [x + radius * Math.cos(turn), y - radius * Math.sin(turn)];
2871
+ const difference = visToBorder(box, Math.atan2(-pos[1], -pos[0])) - Math.hypot(pos[0], pos[1]);
2872
+ if (Math.abs(difference) < 0.05) {
2873
+ break;
2874
+ }
2875
+ if (difference > 0 === direction > 0) {
2876
+ low = middle;
2877
+ } else {
2878
+ high = middle;
2879
+ }
2880
+ }
2881
+ return pos;
2882
+ };
2883
+ const [from, to] = [search(-1), search(1)];
2884
+ const start = Math.atan2(from[1] - y, from[0] - x);
2885
+ let end = Math.atan2(to[1] - y, to[0] - x);
2886
+ if (end < start) {
2887
+ end += 2 * Math.PI;
2888
+ }
2889
+ const [hw, hh] = box;
2890
+ const steps = Math.max(2, Math.ceil(((end - start) / (2 * Math.PI)) * VIS_RING_STEPS));
2891
+ const points: string[] = [];
2892
+ for (let k = 0; k <= steps; k++) {
2893
+ const a = start + ((end - start) * k) / steps;
2894
+ points.push(`${fraction((x + radius * Math.cos(a)) / hw)},${fraction(-(y + radius * Math.sin(a)) / hh)}`);
2895
+ }
2896
+ // 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
2897
+ // is drawn from the node out to them
2898
+ return {
2899
+ curved: false,
2900
+ sourcePort: points[0],
2901
+ bends: points.slice(1, -1).join(" "),
2902
+ targetPort: points.at(-1) ?? points[0],
2903
+ };
2904
+ }
2905
+
2906
+ /**
2907
+ * vis-network 9.1.9's `distanceToBorder` of a node, how far from its center its outline is along a ray: a dot its
2908
+ * size, a circle or circularImage half its width, an ellipse its ellipse, all with no border; every other shape its
2909
+ * box plus its border width (NodeBase._distanceToBorder, Box).
2910
+ * @param box - the node's half width, half height, border width and shape
2911
+ * @param angle - the ray, counterclockwise from the right
2912
+ * @returns the distance
1155
2913
  */
1156
- function nodeRecords(snapshot: GraphSnapshot): (i: number) => VisLayer {
1157
- return elementRecords(snapshot, "node");
2914
+ function visToBorder(box: VisBox, angle: number): number {
2915
+ const [hw, hh, bw, shape] = box;
2916
+ if (shape === "dot" || shape === "circle" || shape === "circularImage") {
2917
+ return hw;
2918
+ }
2919
+ if (shape === "ellipse") {
2920
+ return (hw * hh) / Math.hypot(Math.sin(angle) * hw, Math.cos(angle) * hh);
2921
+ }
2922
+ return Math.min(Math.abs(hw / Math.cos(angle)), Math.abs(hh / Math.sin(angle))) + bw;
1158
2923
  }
1159
2924
 
2925
+ /** The vis.js keys the reader reads, per target. */
2926
+ const VIS_KEYS: Readonly<Record<StyleTarget, readonly string[]>> = {
2927
+ node: [
2928
+ "color",
2929
+ "shape",
2930
+ "size",
2931
+ "value",
2932
+ "scaling",
2933
+ "borderWidth",
2934
+ "opacity",
2935
+ "hidden",
2936
+ "label",
2937
+ "title",
2938
+ "font",
2939
+ "margin",
2940
+ "widthConstraint",
2941
+ "heightConstraint",
2942
+ "image",
2943
+ "brokenImage",
2944
+ "imagePadding",
2945
+ "icon",
2946
+ "shapeProperties",
2947
+ ],
2948
+ edge: [
2949
+ "color",
2950
+ "width",
2951
+ "value",
2952
+ "scaling",
2953
+ "dashes",
2954
+ "arrows",
2955
+ "hidden",
2956
+ "label",
2957
+ "title",
2958
+ "font",
2959
+ "background",
2960
+ "smooth",
2961
+ "selfReference",
2962
+ "selfReferenceSize",
2963
+ "physics",
2964
+ "length",
2965
+ "widthConstraint",
2966
+ ],
2967
+ };
2968
+
1160
2969
  /**
1161
2970
  * Each element's own vis.js keys as a layer, read from the graph's columns.
1162
2971
  * @param snapshot - the graph
@@ -1164,6 +2973,17 @@ function nodeRecords(snapshot: GraphSnapshot): (i: number) => VisLayer {
1164
2973
  * @returns the layer of an element
1165
2974
  */
1166
2975
  function elementRecords(snapshot: GraphSnapshot, target: StyleTarget): (i: number) => VisLayer {
2976
+ const raw = elementRaw(snapshot, target);
2977
+ return (i) => visLayer(raw(i), target);
2978
+ }
2979
+
2980
+ /**
2981
+ * Each element's own vis.js keys as the file gives them, read from the graph's columns.
2982
+ * @param snapshot - the graph
2983
+ * @param target - nodes or edges
2984
+ * @returns the keys of an element
2985
+ */
2986
+ function elementRaw(snapshot: GraphSnapshot, target: StyleTarget): (i: number) => Record<string, unknown> {
1167
2987
  const readers = VIS_KEYS[target]
1168
2988
  .map((name) => [name, cells(snapshot, target, name)] as const)
1169
2989
  .filter((x): x is readonly [string, (i: number) => unknown] => x[1] !== null);
@@ -1175,7 +2995,7 @@ function elementRecords(snapshot: GraphSnapshot, target: StyleTarget): (i: numbe
1175
2995
  record[name] = v;
1176
2996
  }
1177
2997
  }
1178
- return visLayer(record, target);
2998
+ return record;
1179
2999
  };
1180
3000
  }
1181
3001
 
@@ -1283,10 +3103,100 @@ const visSize = (want: (p: string) => StyleValue | null): StyleValue | null => {
1283
3103
  return VIS_SIZED.has(shape) ? oneSize(want) : null;
1284
3104
  };
1285
3105
 
3106
+ /**
3107
+ * The vis.js shape a written node is read back as.
3108
+ * @param want - the block's values of the element
3109
+ * @returns the shape
3110
+ */
3111
+ const visShapeBack = (want: (p: string) => StyleValue | null): string => {
3112
+ const s = want("shape");
3113
+ return typeof s === "string" ? visShape(s)[0] : "ellipse";
3114
+ };
3115
+
3116
+ /**
3117
+ * What a written vis.js node of a shape that changes what its size gives back reads as (visSizedShape, visFitShape):
3118
+ * a hexagon's height and how far below its square its label hangs, a database's label at its top.
3119
+ * @param property - the fact
3120
+ * @returns the reading back of the element's values, null where the shape changes nothing
3121
+ */
3122
+ const visShapeFactBack =
3123
+ (property: "height" | "labelOffsetY") =>
3124
+ (want: (p: string) => StyleValue | null): StyleValue | null => {
3125
+ const shape = visShapeBack(want);
3126
+ const size = visSize(want);
3127
+ if (shape === "hexagon" && typeof size === "number") {
3128
+ return property === "height" ? (Math.sqrt(3) * size) / 2 : ((2 - Math.sqrt(3)) * size) / 4;
3129
+ }
3130
+ return property === "labelOffsetY" && shape === "database" ? VIS_MARGIN : null;
3131
+ };
3132
+
3133
+ /**
3134
+ * What the reader reads back for a fact of a node the writer wrote in a shape vis-network sizes by its label: the
3135
+ * shape's own growth, from the label and font size written.
3136
+ * @param property - the fact
3137
+ * @returns the reading back of the element's values
3138
+ */
3139
+ const visFitBack =
3140
+ (property: string) =>
3141
+ (want: (p: string) => StyleValue | null): StyleValue | null => {
3142
+ const s = want("shape");
3143
+ const shape = typeof s === "string" ? visShape(s)[0] : "ellipse";
3144
+ return visLabelFit({ fontSize: want("labelSize"), label: want("label") }, shape)[property] ?? null;
3145
+ };
3146
+
3147
+ /**
3148
+ * What reading a file graphty wrote in vis.js gives back: VIS_WRITTEN, and for a self-loop the route the reader draws
3149
+ * it along (vis-network's circle, made from vis-network's own defaults and the node's size, never from the block).
3150
+ * @param snapshot - the graph
3151
+ * @param edgeValues - the edge values the reader read
3152
+ * @returns the readings back
3153
+ */
3154
+ function visWritten(snapshot: GraphSnapshot, edgeValues: Values): Written {
3155
+ const isLoop = (i: number): boolean => snapshot.edgeSource(i) === snapshot.edgeTarget(i);
3156
+ const loop =
3157
+ (property: string) =>
3158
+ (want: (p: string) => StyleValue | null, i: number): StyleValue | null =>
3159
+ isLoop(i) ? (edgeValues[property]?.[i] ?? null) : want(property);
3160
+ // the writer writes no `smooth` and no `options.physics`, so every other edge reads back as vis-network's default,
3161
+ // a dynamic curve under the default barnesHut physics
3162
+ const dynamic = visSupport(undefined, undefined) ?? {};
3163
+ const support = (property: string) => (_want: unknown, i: number) =>
3164
+ isLoop(i) ? (edgeValues[property]?.[i] ?? null) : (dynamic[property] ?? null);
3165
+ const supports = Object.fromEntries(Object.keys(dynamic).map((property) => [property, support(property)]));
3166
+ // an edge's own `length`, kept as data, is its springs' rest length again
3167
+ const lengths = cells(snapshot, "edge", "length");
3168
+ supports.supportSpringLength = (want, i) => {
3169
+ const own = lengths?.(i);
3170
+ return typeof own === "number" && Number.isFinite(own) && own >= 0 && !isLoop(i)
3171
+ ? own
3172
+ : support("supportSpringLength")(want, i);
3173
+ };
3174
+ return {
3175
+ node: VIS_WRITTEN.node,
3176
+ edge: {
3177
+ ...VIS_WRITTEN.edge,
3178
+ ...supports,
3179
+ bends: loop("bends"),
3180
+ bendCurve: loop("bendCurve"),
3181
+ curved: (_want, i) => (isLoop(i) ? (edgeValues.curved?.[i] ?? null) : true),
3182
+ },
3183
+ };
3184
+ }
3185
+
1286
3186
  const VIS_WRITTEN: Written = {
1287
3187
  node: {
1288
- width: visSize,
1289
- height: visSize,
3188
+ width: (want) => visSize(want) ?? visFitBack("width")(want),
3189
+ height: (want) => visShapeFactBack("height")(want) ?? visSize(want) ?? visFitBack("height")(want),
3190
+ labelOffsetY: visShapeFactBack("labelOffsetY"),
3191
+ labelFit: visFitBack("labelFit"),
3192
+ labelPaddingX: visFitBack("labelPaddingX"),
3193
+ labelPaddingY: visFitBack("labelPaddingY"),
3194
+ labelFitSquare: visFitBack("labelFitSquare"),
3195
+ // the writer writes hex colors, whose alpha vis-network's node opacity replaces, the border's too
3196
+ borderOpacity: (want) => {
3197
+ const opacity = want("opacity");
3198
+ return typeof opacity === "number" ? opacity : want("borderOpacity");
3199
+ },
1290
3200
  shape: (want) => {
1291
3201
  const s = want("shape");
1292
3202
  if (typeof s !== "string") {
@@ -1295,6 +3205,17 @@ const VIS_WRITTEN: Written = {
1295
3205
  const site = { notes: new StyleNotes(), target: "node" as const, property: "shape" };
1296
3206
  return canonEnum("shape", visShape(s)[0], "vis", site);
1297
3207
  },
3208
+ labelPosition: (want) => {
3209
+ const s = want("shape");
3210
+ if (typeof s !== "string") {
3211
+ return null;
3212
+ }
3213
+ const shape = visShape(s)[0];
3214
+ if (shape === "database") {
3215
+ return "inside-top";
3216
+ }
3217
+ return VIS_SIZED.has(shape) ? "bottom" : "center";
3218
+ },
1298
3219
  },
1299
3220
  edge: {
1300
3221
  lineStyle: (want) => {
@@ -1302,7 +3223,12 @@ const VIS_WRITTEN: Written = {
1302
3223
  if (s === null) {
1303
3224
  return null;
1304
3225
  }
1305
- return s === "solid" ? "solid" : "dashed";
3226
+ // written as `dashes: true`, vis-network's [5, 5], which a wide edge's round caps close up
3227
+ if (s === "solid") {
3228
+ return "solid";
3229
+ }
3230
+ const width = want("width");
3231
+ return visDashStyle([5, 5], typeof width === "number" ? width : 1, true);
1306
3232
  },
1307
3233
  targetArrow: (want) => arrowBack(want("targetArrow")),
1308
3234
  sourceArrow: (want) => arrowBack(want("sourceArrow")),
@@ -1355,8 +3281,8 @@ export const NATIVE: Readonly<
1355
3281
  edge: cyjsProperties(CYJS_EDGE_PROPERTIES, ["curved", "labelItalic", "visible"]),
1356
3282
  },
1357
3283
  graphology: {
1358
- node: ["fill", "fillOpacity", "width", "height", "label", "visible"],
1359
- edge: ["color", "opacity", "width", "label", "visible"],
3284
+ node: ["fill", "fillOpacity", "width", "height", "label"],
3285
+ edge: ["color", "opacity", "width", "label"],
1360
3286
  },
1361
3287
  vis: {
1362
3288
  node: [
@@ -1928,16 +3854,15 @@ function writeGraphology(
1928
3854
  writeKey(
1929
3855
  table,
1930
3856
  "size",
1931
- target === "node" ? nodeSizes(p("node", "width"), p("node", "height"), approximated) : p("edge", "width"),
3857
+ target === "node"
3858
+ ? nodeSizes(p("node", "width"), p("node", "height"), approximated).map((d) =>
3859
+ d === null ? null : d / 2,
3860
+ )
3861
+ : // sigma draws an edge half as wide as its size (readSigmaRow)
3862
+ p("edge", "width").map((w) => (typeof w === "number" ? w * 2 : w)),
1932
3863
  replaced,
1933
3864
  );
1934
3865
  writeKey(table, "label", p(target, "label"), replaced);
1935
- writeKey(
1936
- table,
1937
- "hidden",
1938
- p(target, "visible").map((v) => (v === false ? true : null)),
1939
- replaced,
1940
- );
1941
3866
  }
1942
3867
  }
1943
3868
 
@@ -1948,6 +3873,24 @@ function writeGraphology(
1948
3873
  * @param approximated - where approximations are counted
1949
3874
  * @param replaced - where replaced data values are counted
1950
3875
  */
3876
+ /**
3877
+ * vis.js: a scaling range of each written size for the elements with a `value`, which vis-network would otherwise
3878
+ * map onto `scaling` instead of drawing the size written (a node's radius, an edge's width).
3879
+ * @param table - the node or edge table being written
3880
+ * @param sizes - each element's written size, or null
3881
+ * @param replaced - the data values replaced so far
3882
+ */
3883
+ function keepScaled(table: Table, sizes: readonly (StyleValue | null)[], replaced: Replaced): void {
3884
+ const value = table.get("value");
3885
+ if (value === null) {
3886
+ return;
3887
+ }
3888
+ const scaling = sizes.map((v, i) =>
3889
+ typeof v !== "number" || typeof plainCell(value.value(i)) !== "number" ? null : { min: v, max: v },
3890
+ );
3891
+ writeKey(table, "scaling", scaling, replaced);
3892
+ }
3893
+
1951
3894
  function writeVis(
1952
3895
  out: ReturnType<GraphSnapshot["withColumns"]>,
1953
3896
  input: ExporterStyleInput,
@@ -2007,7 +3950,9 @@ function writeVis(
2007
3950
  );
2008
3951
  const sizes = nodeSizes(p("node", "width"), p("node", "height"), approximated);
2009
3952
  const shapes = p("node", "shape").map((s, i) => {
2010
- const [shape, exact] = typeof s === "string" ? visShape(s) : [null, true];
3953
+ const [written, exact] = typeof s === "string" ? visShape(s) : [null, true];
3954
+ // an ellipse with no size of its own is vis-network's label-sized `ellipse`, one with a size its `dot`
3955
+ const shape = written === "dot" && s === "ellipse" && sizes[i] === null ? "ellipse" : written;
2011
3956
  if (!exact) {
2012
3957
  approximated.add("node", "shape", s);
2013
3958
  }
@@ -2017,7 +3962,11 @@ function writeVis(
2017
3962
  return shape;
2018
3963
  });
2019
3964
  writeKey(out.nodes, "shape", shapes, replaced);
2020
- writeKey(out.nodes, "size", sizes, replaced);
3965
+ // vis-network draws `size` as a radius
3966
+ const radii = sizes.map((d) => (d === null ? null : d / 2));
3967
+ writeKey(out.nodes, "size", radii, replaced);
3968
+ // a node's `value` would scale its size again on reading: a scaling range of its own radius keeps it
3969
+ keepScaled(out.nodes, radii, replaced);
2021
3970
  writeKey(out.nodes, "borderWidth", p("node", "borderWidth"), replaced);
2022
3971
  writeKey(out.nodes, "opacity", p("node", "opacity"), replaced);
2023
3972
  label(out.nodes, "node");
@@ -2039,7 +3988,10 @@ function writeVis(
2039
3988
  }),
2040
3989
  replaced,
2041
3990
  );
2042
- writeKey(out.edges, "width", p("edge", "width"), replaced);
3991
+ const widths = p("edge", "width");
3992
+ writeKey(out.edges, "width", widths, replaced);
3993
+ // an edge's `value` would scale its width again on reading: a scaling range of its own width keeps it
3994
+ keepScaled(out.edges, widths, replaced);
2043
3995
  writeKey(
2044
3996
  out.edges,
2045
3997
  "dashes",