@graphty/graph-io 0.3.48 → 0.3.49

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