@graphty/graph-io 0.3.47 → 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 (770) hide show
  1. package/dist/chunks/{approximations-wWjCRzKj.js → approximations-Bzn3L-je.js} +2 -2
  2. package/dist/chunks/{approximations-wWjCRzKj.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-DiGq58c3.js → block-iptrlMBb.js} +947 -134
  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-BE2xir2k.js +157 -0
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-BKwqelYD.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-BKwqelYD.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/context-C3p0RQxR.js +664 -0
  14. package/dist/chunks/context-C3p0RQxR.js.map +1 -0
  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-DXHx7nwR.js +349 -0
  18. package/dist/chunks/credentials-DXHx7nwR.js.map +1 -0
  19. package/dist/chunks/cx-export-Bt6NoqTQ.js +259 -0
  20. package/dist/chunks/cx-export-Bt6NoqTQ.js.map +1 -0
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/descriptor-C-YDhtsn.js +48 -0
  24. package/dist/chunks/descriptor-C-YDhtsn.js.map +1 -0
  25. package/dist/chunks/descriptor-DLA5kINx.js +142 -0
  26. package/dist/chunks/descriptor-DLA5kINx.js.map +1 -0
  27. package/dist/chunks/{emit-CwU8cUmG.js → emit-BwHQkZle.js} +1690 -103
  28. package/dist/chunks/emit-BwHQkZle.js.map +1 -0
  29. package/dist/chunks/{escape-CgzxiYPg.js → escape-CuCIzNMB.js} +12 -12
  30. package/dist/chunks/{escape-CgzxiYPg.js.map → escape-CuCIzNMB.js.map} +1 -1
  31. package/dist/chunks/{export-BvqAXCKh.js → export-Cha4dRFz.js} +48 -1780
  32. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  33. package/dist/chunks/{exporter--Mk9gXCJ.js → exporter-BP6otMO6.js} +6 -4
  34. package/dist/chunks/{exporter--Mk9gXCJ.js.map → exporter-BP6otMO6.js.map} +1 -1
  35. package/dist/chunks/{exporter-CrDWIyiq.js → exporter-D2CETOMK.js} +1222 -154
  36. package/dist/chunks/exporter-D2CETOMK.js.map +1 -0
  37. package/dist/chunks/{exporter-DndFlGSa.js → exporter-D9yMtocA.js} +7 -6
  38. package/dist/chunks/{exporter-DndFlGSa.js.map → exporter-D9yMtocA.js.map} +1 -1
  39. package/dist/chunks/exporter-_jrBTnos.js +1009 -0
  40. package/dist/chunks/exporter-_jrBTnos.js.map +1 -0
  41. package/dist/chunks/{exporter-CJgfhF-l.js → exporter-nDfxIK5D.js} +9 -7
  42. package/dist/chunks/{exporter-CJgfhF-l.js.map → exporter-nDfxIK5D.js.map} +1 -1
  43. package/dist/chunks/format-BXiAb-1h.js +408 -0
  44. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  45. package/dist/chunks/{generic-Dq1ntt9q.js → generic-ymWCETAJ.js} +387 -31
  46. package/dist/chunks/generic-ymWCETAJ.js.map +1 -0
  47. package/dist/chunks/{importer-CMmeMWlC.js → importer-B92x1xY5.js} +15 -263
  48. package/dist/chunks/importer-B92x1xY5.js.map +1 -0
  49. package/dist/chunks/{importer-DiZRmOQG.js → importer-CT9mJ1nn.js} +328 -90
  50. package/dist/chunks/importer-CT9mJ1nn.js.map +1 -0
  51. package/dist/chunks/{importer-COTK9Tsl.js → importer-DaLTjbds.js} +116 -1099
  52. package/dist/chunks/importer-DaLTjbds.js.map +1 -0
  53. package/dist/chunks/{importer-iFYPEKaJ.js → importer-GREKMdIf.js} +28 -16
  54. package/dist/chunks/importer-GREKMdIf.js.map +1 -0
  55. package/dist/chunks/{importer-BSvfrX_h.js → importer-PqaaKKnY.js} +23 -1016
  56. package/dist/chunks/importer-PqaaKKnY.js.map +1 -0
  57. package/dist/chunks/{importer-yh_aBCU6.js → importer-Vn5QvmmJ.js} +66 -12
  58. package/dist/chunks/importer-Vn5QvmmJ.js.map +1 -0
  59. package/dist/chunks/{importer-e6LGpqHB.js → importer-vRq4h3j5.js} +1470 -354
  60. package/dist/chunks/importer-vRq4h3j5.js.map +1 -0
  61. package/dist/chunks/input-CAxt_aV-.js +1397 -0
  62. package/dist/chunks/input-CAxt_aV-.js.map +1 -0
  63. package/dist/chunks/{json-elements-DJZRZ6jd.js → json-elements-Cq76WjnZ.js} +4 -4
  64. package/dist/chunks/{json-elements-DJZRZ6jd.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  65. package/dist/chunks/{lexicon-CT69Kd87.js → lexicon-Clf4QIyl.js} +299 -77
  66. package/dist/chunks/lexicon-Clf4QIyl.js.map +1 -0
  67. package/dist/chunks/{ontology-export-G4MqRdux.js → ontology-export-OMo91mb0.js} +4 -3
  68. package/dist/chunks/{ontology-export-G4MqRdux.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  69. package/dist/chunks/{records-CiaWZrFH.js → records-NJiYvekn.js} +2 -2
  70. package/dist/chunks/{records-CiaWZrFH.js.map → records-NJiYvekn.js.map} +1 -1
  71. package/dist/chunks/{format-BpQBx8MZ.js → report-CwMfYa9J.js} +70 -127
  72. package/dist/chunks/report-CwMfYa9J.js.map +1 -0
  73. package/dist/chunks/round-trip-Zm3_ivgq.js +79 -0
  74. package/dist/chunks/round-trip-Zm3_ivgq.js.map +1 -0
  75. package/dist/chunks/sigv4-EokFic1V.js +247 -0
  76. package/dist/chunks/sigv4-EokFic1V.js.map +1 -0
  77. package/dist/chunks/{styles-BQnsga7p.js → styles-CCs214_3.js} +1300 -168
  78. package/dist/chunks/styles-CCs214_3.js.map +1 -0
  79. package/dist/chunks/styles-Cm62yyQX.js +2812 -0
  80. package/dist/chunks/styles-Cm62yyQX.js.map +1 -0
  81. package/dist/chunks/styles-DPQQIig4.js +1928 -0
  82. package/dist/chunks/styles-DPQQIig4.js.map +1 -0
  83. package/dist/chunks/styles-yed-UMFdNqze.js +1213 -0
  84. package/dist/chunks/styles-yed-UMFdNqze.js.map +1 -0
  85. package/dist/chunks/{text-CzJNFOS9.js → text-ystE2bYi.js} +2 -2
  86. package/dist/chunks/{text-CzJNFOS9.js.map → text-ystE2bYi.js.map} +1 -1
  87. package/dist/csv.js +7 -5
  88. package/dist/csv.js.map +1 -1
  89. package/dist/cx.js +5 -4
  90. package/dist/cx.js.map +1 -1
  91. package/dist/cx2.js +2 -2
  92. package/dist/cys.js +1 -1
  93. package/dist/dot.js +5 -4
  94. package/dist/dot.js.map +1 -1
  95. package/dist/gexf.js +42 -10
  96. package/dist/gexf.js.map +1 -1
  97. package/dist/gml.js +850 -193
  98. package/dist/gml.js.map +1 -1
  99. package/dist/graph-io.js +1189 -220
  100. package/dist/graph-io.js.map +1 -1
  101. package/dist/graphml.js +3 -3
  102. package/dist/json.js +3 -2
  103. package/dist/neo4j.js +6 -4
  104. package/dist/neo4j.js.map +1 -1
  105. package/dist/obo.js +1 -1
  106. package/dist/pajek.js +1 -1
  107. package/dist/positions.js +45 -3
  108. package/dist/positions.js.map +1 -1
  109. package/dist/sources-cypher.d.ts +1 -0
  110. package/dist/sources-cypher.js +1351 -0
  111. package/dist/sources-cypher.js.map +1 -0
  112. package/dist/sources-ndex.d.ts +1 -0
  113. package/dist/sources-ndex.js +278 -0
  114. package/dist/sources-ndex.js.map +1 -0
  115. package/dist/sources-rest.d.ts +1 -0
  116. package/dist/sources-rest.js +1377 -0
  117. package/dist/sources-rest.js.map +1 -0
  118. package/dist/sources-sparql.d.ts +1 -0
  119. package/dist/sources-sparql.js +405 -0
  120. package/dist/sources-sparql.js.map +1 -0
  121. package/dist/src/common/report.d.ts +6 -3
  122. package/dist/src/common/report.d.ts.map +1 -1
  123. package/dist/src/common/report.js +15 -9
  124. package/dist/src/common/report.js.map +1 -1
  125. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cx/importer.js +5 -1
  127. package/dist/src/formats/cx/importer.js.map +1 -1
  128. package/dist/src/formats/cx/styles.d.ts +24 -1
  129. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cx/styles.js +118 -11
  131. package/dist/src/formats/cx/styles.js.map +1 -1
  132. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  133. package/dist/src/formats/cx2/importer.js +1 -0
  134. package/dist/src/formats/cx2/importer.js.map +1 -1
  135. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  136. package/dist/src/formats/cx2/styles.js +3 -1
  137. package/dist/src/formats/cx2/styles.js.map +1 -1
  138. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  139. package/dist/src/formats/cys/importer.js +1 -0
  140. package/dist/src/formats/cys/importer.js.map +1 -1
  141. package/dist/src/formats/cys/styles.d.ts +2 -0
  142. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  143. package/dist/src/formats/cys/styles.js +15 -7
  144. package/dist/src/formats/cys/styles.js.map +1 -1
  145. package/dist/src/formats/dot/drawing.d.ts +98 -0
  146. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  147. package/dist/src/formats/dot/drawing.js +734 -0
  148. package/dist/src/formats/dot/drawing.js.map +1 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  150. package/dist/src/formats/dot/importer.js +108 -31
  151. package/dist/src/formats/dot/importer.js.map +1 -1
  152. package/dist/src/formats/dot/label-places.d.ts +34 -0
  153. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  154. package/dist/src/formats/dot/label-places.js +179 -0
  155. package/dist/src/formats/dot/label-places.js.map +1 -0
  156. package/dist/src/formats/dot/record.d.ts +50 -0
  157. package/dist/src/formats/dot/record.d.ts.map +1 -0
  158. package/dist/src/formats/dot/record.js +275 -0
  159. package/dist/src/formats/dot/record.js.map +1 -0
  160. package/dist/src/formats/dot/styles.d.ts +62 -2
  161. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  162. package/dist/src/formats/dot/styles.js +1457 -120
  163. package/dist/src/formats/dot/styles.js.map +1 -1
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  165. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  167. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +7 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  170. package/dist/src/formats/gexf/importer.js +41 -5
  171. package/dist/src/formats/gexf/importer.js.map +1 -1
  172. package/dist/src/formats/gexf/preview.d.ts +77 -0
  173. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/preview.js +191 -0
  175. package/dist/src/formats/gexf/preview.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  177. package/dist/src/formats/gexf/schema.js +2 -1
  178. package/dist/src/formats/gexf/schema.js.map +1 -1
  179. package/dist/src/formats/gexf/styles.d.ts +60 -16
  180. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  181. package/dist/src/formats/gexf/styles.js +883 -131
  182. package/dist/src/formats/gexf/styles.js.map +1 -1
  183. package/dist/src/formats/gml/importer.d.ts +5 -2
  184. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  185. package/dist/src/formats/gml/importer.js +258 -71
  186. package/dist/src/formats/gml/importer.js.map +1 -1
  187. package/dist/src/formats/gml/index.d.ts +1 -1
  188. package/dist/src/formats/gml/index.d.ts.map +1 -1
  189. package/dist/src/formats/gml/index.js +1 -1
  190. package/dist/src/formats/gml/index.js.map +1 -1
  191. package/dist/src/formats/gml/positions.d.ts +7 -0
  192. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  193. package/dist/src/formats/gml/positions.js +29 -7
  194. package/dist/src/formats/gml/positions.js.map +1 -1
  195. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  196. package/dist/src/formats/gml/styles.js +756 -103
  197. package/dist/src/formats/gml/styles.js.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  199. package/dist/src/formats/graphml/styles-plain.js +8 -12
  200. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  201. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  202. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  203. package/dist/src/formats/graphml/styles-yed.js +749 -148
  204. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  207. package/dist/src/formats/graphml/styles-yfiles3.js +429 -64
  208. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  209. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  210. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  211. package/dist/src/formats/graphml/yfiles.js +16 -6
  212. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  213. package/dist/src/formats/json/dialect.d.ts +3 -2
  214. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  215. package/dist/src/formats/json/dialect.js +39 -5
  216. package/dist/src/formats/json/dialect.js.map +1 -1
  217. package/dist/src/formats/json/importer.d.ts +13 -2
  218. package/dist/src/formats/json/importer.d.ts.map +1 -1
  219. package/dist/src/formats/json/importer.js +42 -6
  220. package/dist/src/formats/json/importer.js.map +1 -1
  221. package/dist/src/formats/json/index.d.ts +1 -1
  222. package/dist/src/formats/json/index.d.ts.map +1 -1
  223. package/dist/src/formats/json/index.js +1 -1
  224. package/dist/src/formats/json/index.js.map +1 -1
  225. package/dist/src/formats/json/positions.d.ts +5 -2
  226. package/dist/src/formats/json/positions.d.ts.map +1 -1
  227. package/dist/src/formats/json/positions.js +20 -4
  228. package/dist/src/formats/json/positions.js.map +1 -1
  229. package/dist/src/formats/json/styles.d.ts +12 -1
  230. package/dist/src/formats/json/styles.d.ts.map +1 -1
  231. package/dist/src/formats/json/styles.js +1755 -136
  232. package/dist/src/formats/json/styles.js.map +1 -1
  233. package/dist/src/formats/pajek/draw.d.ts +96 -0
  234. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  235. package/dist/src/formats/pajek/draw.js +372 -0
  236. package/dist/src/formats/pajek/draw.js.map +1 -0
  237. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  238. package/dist/src/formats/pajek/exporter.js +70 -51
  239. package/dist/src/formats/pajek/exporter.js.map +1 -1
  240. package/dist/src/formats/pajek/importer.d.ts +9 -0
  241. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  242. package/dist/src/formats/pajek/importer.js +233 -102
  243. package/dist/src/formats/pajek/importer.js.map +1 -1
  244. package/dist/src/formats/pajek/index.d.ts +1 -0
  245. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  246. package/dist/src/formats/pajek/index.js.map +1 -1
  247. package/dist/src/formats/pajek/styles.d.ts +15 -7
  248. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  249. package/dist/src/formats/pajek/styles.js +1279 -114
  250. package/dist/src/formats/pajek/styles.js.map +1 -1
  251. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  252. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  253. package/dist/src/formats/pajek/syntax.js +26 -2
  254. package/dist/src/formats/pajek/syntax.js.map +1 -1
  255. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  256. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  257. package/dist/src/formats/xgmml/emit.js +65 -5
  258. package/dist/src/formats/xgmml/emit.js.map +1 -1
  259. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  260. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  261. package/dist/src/formats/xgmml/styles.js +278 -60
  262. package/dist/src/formats/xgmml/styles.js.map +1 -1
  263. package/dist/src/index.d.ts +10 -2
  264. package/dist/src/index.d.ts.map +1 -1
  265. package/dist/src/index.js +11 -2
  266. package/dist/src/index.js.map +1 -1
  267. package/dist/src/positions/convert.d.ts.map +1 -1
  268. package/dist/src/positions/convert.js +11 -1
  269. package/dist/src/positions/convert.js.map +1 -1
  270. package/dist/src/positions/extract.d.ts.map +1 -1
  271. package/dist/src/positions/extract.js +53 -1
  272. package/dist/src/positions/extract.js.map +1 -1
  273. package/dist/src/positions/index.d.ts +1 -1
  274. package/dist/src/positions/index.d.ts.map +1 -1
  275. package/dist/src/positions/types.d.ts +59 -0
  276. package/dist/src/positions/types.d.ts.map +1 -1
  277. package/dist/src/registry.d.ts +173 -1
  278. package/dist/src/registry.d.ts.map +1 -1
  279. package/dist/src/registry.js +196 -1
  280. package/dist/src/registry.js.map +1 -1
  281. package/dist/src/sources/builtins.d.ts +11 -0
  282. package/dist/src/sources/builtins.d.ts.map +1 -0
  283. package/dist/src/sources/builtins.js +53 -0
  284. package/dist/src/sources/builtins.js.map +1 -0
  285. package/dist/src/sources/codes.d.ts +230 -0
  286. package/dist/src/sources/codes.d.ts.map +1 -0
  287. package/dist/src/sources/codes.js +58 -0
  288. package/dist/src/sources/codes.js.map +1 -0
  289. package/dist/src/sources/context.d.ts +112 -0
  290. package/dist/src/sources/context.d.ts.map +1 -0
  291. package/dist/src/sources/context.js +740 -0
  292. package/dist/src/sources/context.js.map +1 -0
  293. package/dist/src/sources/credentials.d.ts +179 -0
  294. package/dist/src/sources/credentials.d.ts.map +1 -0
  295. package/dist/src/sources/credentials.js +301 -0
  296. package/dist/src/sources/credentials.js.map +1 -0
  297. package/dist/src/sources/cypher/descriptor.d.ts +21 -0
  298. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -0
  299. package/dist/src/sources/cypher/descriptor.js +124 -0
  300. package/dist/src/sources/cypher/descriptor.js.map +1 -0
  301. package/dist/src/sources/cypher/flatten.d.ts +72 -0
  302. package/dist/src/sources/cypher/flatten.d.ts.map +1 -0
  303. package/dist/src/sources/cypher/flatten.js +279 -0
  304. package/dist/src/sources/cypher/flatten.js.map +1 -0
  305. package/dist/src/sources/cypher/index.d.ts +33 -0
  306. package/dist/src/sources/cypher/index.d.ts.map +1 -0
  307. package/dist/src/sources/cypher/index.js +511 -0
  308. package/dist/src/sources/cypher/index.js.map +1 -0
  309. package/dist/src/sources/cypher/params.d.ts +40 -0
  310. package/dist/src/sources/cypher/params.d.ts.map +1 -0
  311. package/dist/src/sources/cypher/params.js +6 -0
  312. package/dist/src/sources/cypher/params.js.map +1 -0
  313. package/dist/src/sources/cypher/profiles.d.ts +44 -0
  314. package/dist/src/sources/cypher/profiles.d.ts.map +1 -0
  315. package/dist/src/sources/cypher/profiles.js +61 -0
  316. package/dist/src/sources/cypher/profiles.js.map +1 -0
  317. package/dist/src/sources/cypher/query-builder.d.ts +123 -0
  318. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -0
  319. package/dist/src/sources/cypher/query-builder.js +267 -0
  320. package/dist/src/sources/cypher/query-builder.js.map +1 -0
  321. package/dist/src/sources/cypher/transports/bolt.d.ts +21 -0
  322. package/dist/src/sources/cypher/transports/bolt.d.ts.map +1 -0
  323. package/dist/src/sources/cypher/transports/bolt.js +185 -0
  324. package/dist/src/sources/cypher/transports/bolt.js.map +1 -0
  325. package/dist/src/sources/cypher/transports/neptune-analytics.d.ts +15 -0
  326. package/dist/src/sources/cypher/transports/neptune-analytics.d.ts.map +1 -0
  327. package/dist/src/sources/cypher/transports/neptune-analytics.js +38 -0
  328. package/dist/src/sources/cypher/transports/neptune-analytics.js.map +1 -0
  329. package/dist/src/sources/cypher/transports/neptune-https.d.ts +31 -0
  330. package/dist/src/sources/cypher/transports/neptune-https.d.ts.map +1 -0
  331. package/dist/src/sources/cypher/transports/neptune-https.js +104 -0
  332. package/dist/src/sources/cypher/transports/neptune-https.js.map +1 -0
  333. package/dist/src/sources/cypher/transports/query-api.d.ts +14 -0
  334. package/dist/src/sources/cypher/transports/query-api.d.ts.map +1 -0
  335. package/dist/src/sources/cypher/transports/query-api.js +80 -0
  336. package/dist/src/sources/cypher/transports/query-api.js.map +1 -0
  337. package/dist/src/sources/cypher/transports/transport.d.ts +78 -0
  338. package/dist/src/sources/cypher/transports/transport.d.ts.map +1 -0
  339. package/dist/src/sources/cypher/transports/transport.js +77 -0
  340. package/dist/src/sources/cypher/transports/transport.js.map +1 -0
  341. package/dist/src/sources/cypher/types-map.d.ts +91 -0
  342. package/dist/src/sources/cypher/types-map.d.ts.map +1 -0
  343. package/dist/src/sources/cypher/types-map.js +306 -0
  344. package/dist/src/sources/cypher/types-map.js.map +1 -0
  345. package/dist/src/sources/define.d.ts +47 -0
  346. package/dist/src/sources/define.d.ts.map +1 -0
  347. package/dist/src/sources/define.js +173 -0
  348. package/dist/src/sources/define.js.map +1 -0
  349. package/dist/src/sources/fail.d.ts +33 -0
  350. package/dist/src/sources/fail.d.ts.map +1 -0
  351. package/dist/src/sources/fail.js +49 -0
  352. package/dist/src/sources/fail.js.map +1 -0
  353. package/dist/src/sources/ids.d.ts +69 -0
  354. package/dist/src/sources/ids.d.ts.map +1 -0
  355. package/dist/src/sources/ids.js +69 -0
  356. package/dist/src/sources/ids.js.map +1 -0
  357. package/dist/src/sources/ndex/client.d.ts +31 -0
  358. package/dist/src/sources/ndex/client.d.ts.map +1 -0
  359. package/dist/src/sources/ndex/client.js +90 -0
  360. package/dist/src/sources/ndex/client.js.map +1 -0
  361. package/dist/src/sources/ndex/descriptor.d.ts +10 -0
  362. package/dist/src/sources/ndex/descriptor.d.ts.map +1 -0
  363. package/dist/src/sources/ndex/descriptor.js +55 -0
  364. package/dist/src/sources/ndex/descriptor.js.map +1 -0
  365. package/dist/src/sources/ndex/expand.d.ts +20 -0
  366. package/dist/src/sources/ndex/expand.d.ts.map +1 -0
  367. package/dist/src/sources/ndex/expand.js +87 -0
  368. package/dist/src/sources/ndex/expand.js.map +1 -0
  369. package/dist/src/sources/ndex/index.d.ts +25 -0
  370. package/dist/src/sources/ndex/index.d.ts.map +1 -0
  371. package/dist/src/sources/ndex/index.js +191 -0
  372. package/dist/src/sources/ndex/index.js.map +1 -0
  373. package/dist/src/sources/ndex/params.d.ts +17 -0
  374. package/dist/src/sources/ndex/params.d.ts.map +1 -0
  375. package/dist/src/sources/ndex/params.js +6 -0
  376. package/dist/src/sources/ndex/params.js.map +1 -0
  377. package/dist/src/sources/ndex/share-url.d.ts +40 -0
  378. package/dist/src/sources/ndex/share-url.d.ts.map +1 -0
  379. package/dist/src/sources/ndex/share-url.js +64 -0
  380. package/dist/src/sources/ndex/share-url.js.map +1 -0
  381. package/dist/src/sources/params.d.ts +31 -0
  382. package/dist/src/sources/params.d.ts.map +1 -0
  383. package/dist/src/sources/params.js +6 -0
  384. package/dist/src/sources/params.js.map +1 -0
  385. package/dist/src/sources/registry.d.ts +118 -0
  386. package/dist/src/sources/registry.d.ts.map +1 -0
  387. package/dist/src/sources/registry.js +193 -0
  388. package/dist/src/sources/registry.js.map +1 -0
  389. package/dist/src/sources/rest/descriptor.d.ts +16 -0
  390. package/dist/src/sources/rest/descriptor.d.ts.map +1 -0
  391. package/dist/src/sources/rest/descriptor.js +76 -0
  392. package/dist/src/sources/rest/descriptor.js.map +1 -0
  393. package/dist/src/sources/rest/index.d.ts +14 -0
  394. package/dist/src/sources/rest/index.d.ts.map +1 -0
  395. package/dist/src/sources/rest/index.js +13 -0
  396. package/dist/src/sources/rest/index.js.map +1 -0
  397. package/dist/src/sources/rest/mapping.d.ts +30 -0
  398. package/dist/src/sources/rest/mapping.d.ts.map +1 -0
  399. package/dist/src/sources/rest/mapping.js +362 -0
  400. package/dist/src/sources/rest/mapping.js.map +1 -0
  401. package/dist/src/sources/rest/openalex.d.ts +27 -0
  402. package/dist/src/sources/rest/openalex.d.ts.map +1 -0
  403. package/dist/src/sources/rest/openalex.js +269 -0
  404. package/dist/src/sources/rest/openalex.js.map +1 -0
  405. package/dist/src/sources/rest/paging.d.ts +53 -0
  406. package/dist/src/sources/rest/paging.d.ts.map +1 -0
  407. package/dist/src/sources/rest/paging.js +138 -0
  408. package/dist/src/sources/rest/paging.js.map +1 -0
  409. package/dist/src/sources/rest/params.d.ts +216 -0
  410. package/dist/src/sources/rest/params.d.ts.map +1 -0
  411. package/dist/src/sources/rest/params.js +7 -0
  412. package/dist/src/sources/rest/params.js.map +1 -0
  413. package/dist/src/sources/rest/records.d.ts +41 -0
  414. package/dist/src/sources/rest/records.d.ts.map +1 -0
  415. package/dist/src/sources/rest/records.js +240 -0
  416. package/dist/src/sources/rest/records.js.map +1 -0
  417. package/dist/src/sources/rest/samples/deps-dev.d.ts +10 -0
  418. package/dist/src/sources/rest/samples/deps-dev.d.ts.map +1 -0
  419. package/dist/src/sources/rest/samples/deps-dev.js +47 -0
  420. package/dist/src/sources/rest/samples/deps-dev.js.map +1 -0
  421. package/dist/src/sources/rest/samples/string.d.ts +9 -0
  422. package/dist/src/sources/rest/samples/string.d.ts.map +1 -0
  423. package/dist/src/sources/rest/samples/string.js +42 -0
  424. package/dist/src/sources/rest/samples/string.js.map +1 -0
  425. package/dist/src/sources/rest/selectors.d.ts +41 -0
  426. package/dist/src/sources/rest/selectors.d.ts.map +1 -0
  427. package/dist/src/sources/rest/selectors.js +81 -0
  428. package/dist/src/sources/rest/selectors.js.map +1 -0
  429. package/dist/src/sources/rest/source.d.ts +67 -0
  430. package/dist/src/sources/rest/source.d.ts.map +1 -0
  431. package/dist/src/sources/rest/source.js +650 -0
  432. package/dist/src/sources/rest/source.js.map +1 -0
  433. package/dist/src/sources/rest/templates.d.ts +38 -0
  434. package/dist/src/sources/rest/templates.d.ts.map +1 -0
  435. package/dist/src/sources/rest/templates.js +125 -0
  436. package/dist/src/sources/rest/templates.js.map +1 -0
  437. package/dist/src/sources/run.d.ts +93 -0
  438. package/dist/src/sources/run.d.ts.map +1 -0
  439. package/dist/src/sources/run.js +673 -0
  440. package/dist/src/sources/run.js.map +1 -0
  441. package/dist/src/sources/sha256.d.ts +12 -0
  442. package/dist/src/sources/sha256.d.ts.map +1 -0
  443. package/dist/src/sources/sha256.js +95 -0
  444. package/dist/src/sources/sha256.js.map +1 -0
  445. package/dist/src/sources/sigv4.d.ts +26 -0
  446. package/dist/src/sources/sigv4.d.ts.map +1 -0
  447. package/dist/src/sources/sigv4.js +184 -0
  448. package/dist/src/sources/sigv4.js.map +1 -0
  449. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  450. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  451. package/dist/src/sources/sparql/descriptor.js +64 -0
  452. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  453. package/dist/src/sources/sparql/index.d.ts +28 -0
  454. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  455. package/dist/src/sources/sparql/index.js +314 -0
  456. package/dist/src/sources/sparql/index.js.map +1 -0
  457. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  458. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  459. package/dist/src/sources/sparql/ntriples.js +180 -0
  460. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  461. package/dist/src/sources/sparql/params.d.ts +27 -0
  462. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  463. package/dist/src/sources/sparql/params.js +5 -0
  464. package/dist/src/sources/sparql/params.js.map +1 -0
  465. package/dist/src/sources/sparql/query.d.ts +72 -0
  466. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  467. package/dist/src/sources/sparql/query.js +113 -0
  468. package/dist/src/sources/sparql/query.js.map +1 -0
  469. package/dist/src/sources/testing.d.ts +96 -0
  470. package/dist/src/sources/testing.d.ts.map +1 -0
  471. package/dist/src/sources/testing.js +138 -0
  472. package/dist/src/sources/testing.js.map +1 -0
  473. package/dist/src/sources/types.d.ts +696 -0
  474. package/dist/src/sources/types.d.ts.map +1 -0
  475. package/dist/src/sources/types.js +10 -0
  476. package/dist/src/sources/types.js.map +1 -0
  477. package/dist/src/styles/annotations.d.ts +61 -5
  478. package/dist/src/styles/annotations.d.ts.map +1 -1
  479. package/dist/src/styles/annotations.js +130 -22
  480. package/dist/src/styles/annotations.js.map +1 -1
  481. package/dist/src/styles/canon/bends.d.ts +46 -0
  482. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  483. package/dist/src/styles/canon/bends.js +123 -0
  484. package/dist/src/styles/canon/bends.js.map +1 -1
  485. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  486. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  487. package/dist/src/styles/canon/color-tables.js +10 -4
  488. package/dist/src/styles/canon/color-tables.js.map +1 -1
  489. package/dist/src/styles/canon/color.d.ts +3 -1
  490. package/dist/src/styles/canon/color.d.ts.map +1 -1
  491. package/dist/src/styles/canon/color.js +103 -3
  492. package/dist/src/styles/canon/color.js.map +1 -1
  493. package/dist/src/styles/canon/enums.d.ts +2 -2
  494. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  495. package/dist/src/styles/canon/enums.js +82 -7
  496. package/dist/src/styles/canon/enums.js.map +1 -1
  497. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  498. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  499. package/dist/src/styles/canon/graphviz.js +57 -9
  500. package/dist/src/styles/canon/graphviz.js.map +1 -1
  501. package/dist/src/styles/canon/number.d.ts +8 -0
  502. package/dist/src/styles/canon/number.d.ts.map +1 -1
  503. package/dist/src/styles/canon/number.js +18 -0
  504. package/dist/src/styles/canon/number.js.map +1 -1
  505. package/dist/src/styles/canon/text.d.ts +29 -2
  506. package/dist/src/styles/canon/text.d.ts.map +1 -1
  507. package/dist/src/styles/canon/text.js +156 -16
  508. package/dist/src/styles/canon/text.js.map +1 -1
  509. package/dist/src/styles/codes.d.ts +2 -0
  510. package/dist/src/styles/codes.d.ts.map +1 -1
  511. package/dist/src/styles/codes.js +2 -0
  512. package/dist/src/styles/codes.js.map +1 -1
  513. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  514. package/dist/src/styles/cyjs/cascade.js +1037 -35
  515. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  516. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  517. package/dist/src/styles/cyjs/defaults-table.js +5 -0
  518. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  519. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  520. package/dist/src/styles/cyjs/emit.js +216 -12
  521. package/dist/src/styles/cyjs/emit.js.map +1 -1
  522. package/dist/src/styles/cyjs/face.d.ts +20 -0
  523. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  524. package/dist/src/styles/cyjs/face.js +332 -0
  525. package/dist/src/styles/cyjs/face.js.map +1 -0
  526. package/dist/src/styles/cyjs/mappers.d.ts +48 -2
  527. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  528. package/dist/src/styles/cyjs/mappers.js +463 -26
  529. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  530. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  531. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  532. package/dist/src/styles/cyjs/routes.js +360 -0
  533. package/dist/src/styles/cyjs/routes.js.map +1 -0
  534. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  535. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  536. package/dist/src/styles/cytoscape/charts.js +560 -0
  537. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  538. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  539. package/dist/src/styles/cytoscape/emit.js +204 -44
  540. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  541. package/dist/src/styles/cytoscape/lexicon.d.ts +97 -3
  542. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  543. package/dist/src/styles/cytoscape/lexicon.js +413 -79
  544. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  545. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  546. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  547. package/dist/src/styles/cytoscape/mappings.js +92 -0
  548. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  549. package/dist/src/styles/cytoscape/resolve.d.ts +46 -3
  550. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  551. package/dist/src/styles/cytoscape/resolve.js +632 -22
  552. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  553. package/dist/src/styles/cytoscape/translate.d.ts.map +1 -1
  554. package/dist/src/styles/cytoscape/translate.js +10 -10
  555. package/dist/src/styles/cytoscape/translate.js.map +1 -1
  556. package/dist/src/styles/defaults.d.ts +50 -0
  557. package/dist/src/styles/defaults.d.ts.map +1 -1
  558. package/dist/src/styles/defaults.js +315 -37
  559. package/dist/src/styles/defaults.js.map +1 -1
  560. package/dist/src/styles/index.d.ts +4 -3
  561. package/dist/src/styles/index.d.ts.map +1 -1
  562. package/dist/src/styles/index.js +3 -2
  563. package/dist/src/styles/index.js.map +1 -1
  564. package/dist/src/styles/internal.d.ts +1 -0
  565. package/dist/src/styles/internal.d.ts.map +1 -1
  566. package/dist/src/styles/internal.js +1 -0
  567. package/dist/src/styles/internal.js.map +1 -1
  568. package/dist/src/styles/io/block.d.ts.map +1 -1
  569. package/dist/src/styles/io/block.js +9 -0
  570. package/dist/src/styles/io/block.js.map +1 -1
  571. package/dist/src/styles/io/generic.d.ts.map +1 -1
  572. package/dist/src/styles/io/generic.js +56 -16
  573. package/dist/src/styles/io/generic.js.map +1 -1
  574. package/dist/src/styles/io/round-trip.d.ts +56 -0
  575. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  576. package/dist/src/styles/io/round-trip.js +138 -0
  577. package/dist/src/styles/io/round-trip.js.map +1 -0
  578. package/dist/src/styles/io/writer.d.ts.map +1 -1
  579. package/dist/src/styles/io/writer.js +13 -4
  580. package/dist/src/styles/io/writer.js.map +1 -1
  581. package/dist/src/styles/notes.d.ts.map +1 -1
  582. package/dist/src/styles/notes.js +1 -0
  583. package/dist/src/styles/notes.js.map +1 -1
  584. package/dist/src/styles/plan/features.d.ts +1 -1
  585. package/dist/src/styles/plan/features.d.ts.map +1 -1
  586. package/dist/src/styles/plan/features.js +22 -2
  587. package/dist/src/styles/plan/features.js.map +1 -1
  588. package/dist/src/styles/plan/index.d.ts +1 -1
  589. package/dist/src/styles/plan/index.d.ts.map +1 -1
  590. package/dist/src/styles/plan/index.js +1 -1
  591. package/dist/src/styles/plan/index.js.map +1 -1
  592. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  593. package/dist/src/styles/plan/plan.js +61 -4
  594. package/dist/src/styles/plan/plan.js.map +1 -1
  595. package/dist/src/styles/plan/resolve.d.ts +13 -1
  596. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  597. package/dist/src/styles/plan/resolve.js +53 -6
  598. package/dist/src/styles/plan/resolve.js.map +1 -1
  599. package/dist/src/styles/slots.d.ts +14 -0
  600. package/dist/src/styles/slots.d.ts.map +1 -0
  601. package/dist/src/styles/slots.js +20 -0
  602. package/dist/src/styles/slots.js.map +1 -0
  603. package/dist/src/styles/types.d.ts +638 -5
  604. package/dist/src/styles/types.d.ts.map +1 -1
  605. package/dist/src/styles/vocabulary.d.ts +3 -2
  606. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  607. package/dist/src/styles/vocabulary.js +218 -1
  608. package/dist/src/styles/vocabulary.js.map +1 -1
  609. package/dist/src/types.d.ts +19 -5
  610. package/dist/src/types.d.ts.map +1 -1
  611. package/dist/src/types.js.map +1 -1
  612. package/dist/styles.js +29 -238
  613. package/dist/styles.js.map +1 -1
  614. package/dist/xgmml.js +3 -3
  615. package/docs/samples/people.json +6 -0
  616. package/package.json +53 -8
  617. package/src/common/report.ts +27 -7
  618. package/src/formats/cx/importer.ts +6 -1
  619. package/src/formats/cx/styles.ts +144 -9
  620. package/src/formats/cx2/importer.ts +1 -0
  621. package/src/formats/cx2/styles.ts +3 -1
  622. package/src/formats/cys/importer.ts +1 -0
  623. package/src/formats/cys/styles.ts +27 -5
  624. package/src/formats/dot/drawing.ts +799 -0
  625. package/src/formats/dot/importer.ts +120 -35
  626. package/src/formats/dot/label-places.ts +228 -0
  627. package/src/formats/dot/record.ts +336 -0
  628. package/src/formats/dot/styles.ts +1657 -154
  629. package/src/formats/gexf/dialog-metrics.ts +61 -0
  630. package/src/formats/gexf/importer.ts +50 -5
  631. package/src/formats/gexf/preview.ts +217 -0
  632. package/src/formats/gexf/schema.ts +2 -1
  633. package/src/formats/gexf/styles.ts +1144 -142
  634. package/src/formats/gml/importer.ts +291 -76
  635. package/src/formats/gml/index.ts +1 -1
  636. package/src/formats/gml/positions.ts +29 -7
  637. package/src/formats/gml/styles.ts +854 -111
  638. package/src/formats/graphml/styles-plain.ts +8 -23
  639. package/src/formats/graphml/styles-yed.ts +821 -159
  640. package/src/formats/graphml/styles-yfiles3.ts +485 -63
  641. package/src/formats/graphml/yfiles.ts +19 -6
  642. package/src/formats/json/dialect.ts +41 -5
  643. package/src/formats/json/importer.ts +59 -7
  644. package/src/formats/json/index.ts +1 -0
  645. package/src/formats/json/positions.ts +22 -5
  646. package/src/formats/json/styles.ts +2041 -185
  647. package/src/formats/pajek/draw.ts +458 -0
  648. package/src/formats/pajek/exporter.ts +81 -49
  649. package/src/formats/pajek/importer.ts +278 -103
  650. package/src/formats/pajek/index.ts +1 -0
  651. package/src/formats/pajek/styles.ts +1382 -133
  652. package/src/formats/pajek/syntax.ts +27 -2
  653. package/src/formats/xgmml/emit.ts +73 -5
  654. package/src/formats/xgmml/styles.ts +331 -71
  655. package/src/index.ts +94 -0
  656. package/src/positions/convert.ts +12 -1
  657. package/src/positions/extract.ts +55 -1
  658. package/src/positions/index.ts +1 -0
  659. package/src/positions/types.ts +60 -0
  660. package/src/registry.ts +326 -1
  661. package/src/sources/builtins.ts +63 -0
  662. package/src/sources/codes.ts +235 -0
  663. package/src/sources/context.ts +894 -0
  664. package/src/sources/credentials.ts +409 -0
  665. package/src/sources/cypher/descriptor.ts +131 -0
  666. package/src/sources/cypher/flatten.ts +323 -0
  667. package/src/sources/cypher/index.ts +628 -0
  668. package/src/sources/cypher/params.ts +41 -0
  669. package/src/sources/cypher/profiles.ts +78 -0
  670. package/src/sources/cypher/query-builder.ts +327 -0
  671. package/src/sources/cypher/transports/bolt.ts +228 -0
  672. package/src/sources/cypher/transports/neptune-analytics.ts +41 -0
  673. package/src/sources/cypher/transports/neptune-https.ts +122 -0
  674. package/src/sources/cypher/transports/query-api.ts +100 -0
  675. package/src/sources/cypher/transports/transport.ts +139 -0
  676. package/src/sources/cypher/types-map.ts +418 -0
  677. package/src/sources/define.ts +195 -0
  678. package/src/sources/fail.ts +70 -0
  679. package/src/sources/ids.ts +80 -0
  680. package/src/sources/ndex/client.ts +102 -0
  681. package/src/sources/ndex/descriptor.ts +64 -0
  682. package/src/sources/ndex/expand.ts +100 -0
  683. package/src/sources/ndex/index.ts +235 -0
  684. package/src/sources/ndex/params.ts +17 -0
  685. package/src/sources/ndex/share-url.ts +84 -0
  686. package/src/sources/params.ts +32 -0
  687. package/src/sources/registry.ts +325 -0
  688. package/src/sources/rest/descriptor.ts +79 -0
  689. package/src/sources/rest/index.ts +22 -0
  690. package/src/sources/rest/mapping.ts +401 -0
  691. package/src/sources/rest/openalex.ts +288 -0
  692. package/src/sources/rest/paging.ts +180 -0
  693. package/src/sources/rest/params.ts +243 -0
  694. package/src/sources/rest/records.ts +289 -0
  695. package/src/sources/rest/samples/deps-dev.ts +49 -0
  696. package/src/sources/rest/samples/string.ts +44 -0
  697. package/src/sources/rest/selectors.ts +91 -0
  698. package/src/sources/rest/source.ts +776 -0
  699. package/src/sources/rest/templates.ts +151 -0
  700. package/src/sources/run.ts +793 -0
  701. package/src/sources/sha256.ts +99 -0
  702. package/src/sources/sigv4.ts +209 -0
  703. package/src/sources/sparql/descriptor.ts +67 -0
  704. package/src/sources/sparql/index.ts +356 -0
  705. package/src/sources/sparql/ntriples.ts +219 -0
  706. package/src/sources/sparql/params.ts +28 -0
  707. package/src/sources/sparql/query.ts +162 -0
  708. package/src/sources/testing.ts +210 -0
  709. package/src/sources/types.ts +776 -0
  710. package/src/styles/annotations.ts +201 -27
  711. package/src/styles/canon/bends.ts +137 -0
  712. package/src/styles/canon/color-tables.ts +10 -4
  713. package/src/styles/canon/color.ts +105 -3
  714. package/src/styles/canon/enums.ts +99 -8
  715. package/src/styles/canon/graphviz.ts +71 -10
  716. package/src/styles/canon/number.ts +19 -0
  717. package/src/styles/canon/text.ts +187 -16
  718. package/src/styles/codes.ts +2 -0
  719. package/src/styles/cyjs/cascade.ts +1193 -38
  720. package/src/styles/cyjs/defaults-table.ts +5 -0
  721. package/src/styles/cyjs/emit.ts +247 -16
  722. package/src/styles/cyjs/face.ts +372 -0
  723. package/src/styles/cyjs/mappers.ts +498 -28
  724. package/src/styles/cyjs/routes.ts +432 -0
  725. package/src/styles/cytoscape/charts.ts +684 -0
  726. package/src/styles/cytoscape/emit.ts +220 -44
  727. package/src/styles/cytoscape/lexicon.ts +459 -80
  728. package/src/styles/cytoscape/mappings.ts +108 -0
  729. package/src/styles/cytoscape/resolve.ts +763 -23
  730. package/src/styles/cytoscape/translate.ts +11 -11
  731. package/src/styles/defaults.ts +436 -38
  732. package/src/styles/index.ts +5 -0
  733. package/src/styles/internal.ts +1 -0
  734. package/src/styles/io/block.ts +10 -0
  735. package/src/styles/io/generic.ts +79 -23
  736. package/src/styles/io/round-trip.ts +202 -0
  737. package/src/styles/io/writer.ts +14 -4
  738. package/src/styles/notes.ts +1 -0
  739. package/src/styles/plan/features.ts +23 -2
  740. package/src/styles/plan/index.ts +1 -1
  741. package/src/styles/plan/plan.ts +69 -5
  742. package/src/styles/plan/resolve.ts +61 -6
  743. package/src/styles/slots.ts +25 -0
  744. package/src/styles/types.ts +658 -4
  745. package/src/styles/vocabulary.ts +226 -3
  746. package/src/types.ts +19 -5
  747. package/dist/chunks/block-DiGq58c3.js.map +0 -1
  748. package/dist/chunks/choose--jM0yYCo.js +0 -57
  749. package/dist/chunks/choose--jM0yYCo.js.map +0 -1
  750. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  751. package/dist/chunks/emit-CwU8cUmG.js.map +0 -1
  752. package/dist/chunks/export-BvqAXCKh.js.map +0 -1
  753. package/dist/chunks/exporter-CrDWIyiq.js.map +0 -1
  754. package/dist/chunks/format-BpQBx8MZ.js.map +0 -1
  755. package/dist/chunks/generic-Dq1ntt9q.js.map +0 -1
  756. package/dist/chunks/importer-BSvfrX_h.js.map +0 -1
  757. package/dist/chunks/importer-CMmeMWlC.js.map +0 -1
  758. package/dist/chunks/importer-COTK9Tsl.js.map +0 -1
  759. package/dist/chunks/importer-DiZRmOQG.js.map +0 -1
  760. package/dist/chunks/importer-e6LGpqHB.js.map +0 -1
  761. package/dist/chunks/importer-iFYPEKaJ.js.map +0 -1
  762. package/dist/chunks/importer-yh_aBCU6.js.map +0 -1
  763. package/dist/chunks/lexicon-CT69Kd87.js.map +0 -1
  764. package/dist/chunks/styles-BQnsga7p.js.map +0 -1
  765. package/dist/chunks/styles-D3fRphrO.js +0 -690
  766. package/dist/chunks/styles-D3fRphrO.js.map +0 -1
  767. package/dist/chunks/styles-yed-BF5x3_Y5.js +0 -842
  768. package/dist/chunks/styles-yed-BF5x3_Y5.js.map +0 -1
  769. package/dist/chunks/writer-DMKvthQE.js +0 -448
  770. package/dist/chunks/writer-DMKvthQE.js.map +0 -1
@@ -5,7 +5,7 @@
5
5
  * - Cytoscape.js: the stylesheet (an array or a CSS-like text) over the raw element JSON, through graph-io's
6
6
  * Cytoscape.js engine, with Cytoscape.js's defaults beneath it; an element's own `style` is a bypass. Cytoscape
7
7
  * desktop's export without a stylesheet gets Cytoscape desktop's default style instead.
8
- * - graphology: the keys sigma.js draws (`color`, `size`, `label`, `hidden`), with sigma's defaults. Gephi's JSON
8
+ * - graphology: the keys sigma.js draws (`color`, `size` (a radius), `label`), with sigma's defaults. Gephi's JSON
9
9
  * export for sigma.js is read the same way.
10
10
  * - vis.js: the keys vis-network draws, `options.nodes` and `options.edges` as declared defaults and
11
11
  * `options.groups` as group rules, with vis-network's defaults, its palette for groups the file does not define,
@@ -36,11 +36,12 @@ import {
36
36
  import { CYJS_EDGE_PROPERTIES, CYJS_NODE_PROPERTIES } from "../../styles/cyjs/mappers.js";
37
37
  import { toolDefaults } from "../../styles/defaults.js";
38
38
  import { StyleApproximations } from "../../styles/io/approximations.js";
39
- import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
39
+ import { parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
40
+ import { takeFromBlock } from "../../styles/io/round-trip.js";
40
41
  import type { ExporterStyleInput } from "../../styles/io/writer.js";
41
42
  import { StyleNotes } from "../../styles/notes.js";
42
43
  import { styleFingerprint } from "../../styles/plan/features.js";
43
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
44
+ import { plainCell } from "../../styles/plan/resolve.js";
44
45
  import type {
45
46
  ColumnRef,
46
47
  EdgeStyleProperty,
@@ -81,10 +82,23 @@ export interface JsonStyleSource {
81
82
  readonly groups?: unknown;
82
83
  /** graphology: whether the document is Gephi's JSON export (`id` instead of `key`). */
83
84
  readonly gephi?: boolean;
85
+ /** graphology: the sigma.js settings the drawing uses (the `sigmaSettings` import option). */
86
+ readonly sigma?: SigmaSettings;
84
87
  /** node-link: whether the document has NetworkX's `directed`, `multigraph` and `graph` keys. */
85
88
  readonly networkx?: boolean;
86
89
  }
87
90
 
91
+ /** The sigma.js settings that change what graph-io reads from a graphology document; the rest are the application's. */
92
+ export interface SigmaSettings {
93
+ /** Whether sigma draws edge labels (`renderEdgeLabels`, off by default). */
94
+ readonly renderEdgeLabels: boolean;
95
+ /** Whose edge labels it draws (`edgeLabelAnchors`): between two labeled nodes ("nodeLabels", the default), or every edge's. */
96
+ readonly edgeLabelAnchors: "nodeLabels" | "allNodes";
97
+ }
98
+
99
+ /** sigma.js's own settings. */
100
+ export const SIGMA_DEFAULT_SETTINGS: SigmaSettings = { renderEdgeLabels: false, edgeLabelAnchors: "nodeLabels" };
101
+
88
102
  /** The writable values of one row. */
89
103
  type Row = Record<string, StyleValue | null>;
90
104
  /** Per-property values of one target, by element index. */
@@ -316,14 +330,17 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
316
330
  const notes = new StyleNotes();
317
331
  const out: Record<StyleTarget, Values> = { node: {}, edge: {} };
318
332
  const readFrom: Record<StyleTarget, string[]> = { node: [], edge: [] };
333
+ const edgeLabel = sigmaEdgeLabels(snapshot, src.sigma ?? SIGMA_DEFAULT_SETTINGS);
319
334
  for (const target of TARGETS) {
320
335
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
321
336
  const { values, put } = valuesOf(n);
322
337
  const keys: SigmaKeys = {
323
338
  color: cells(snapshot, target, "color"),
324
339
  size: cells(snapshot, target, "size"),
340
+ // sigma 4's default styles bind nothing else: it draws an element marked `hidden: true` (sigma 3's key)
341
+ // or `visibility: "hidden"` (its own display-data name) like any other (specimen sheets
342
+ // graphology-specimen-node-sizes-opacity, -edges-lines)
325
343
  label: cells(snapshot, target, "label"),
326
- hidden: cells(snapshot, target, "hidden"),
327
344
  };
328
345
  readFrom[target].push(...Object.entries(keys).flatMap(([name, c]) => (c === null ? [] : [name])));
329
346
  for (let i = 0; i < n; i++) {
@@ -334,7 +351,7 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
334
351
  element: i,
335
352
  budget: context.budget,
336
353
  });
337
- readSigmaRow(keys, target, i, put, site);
354
+ readSigmaRow(keys, target, target === "node" || edgeLabel(i), i, put, site);
338
355
  }
339
356
  out[target] = values;
340
357
  }
@@ -352,18 +369,42 @@ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context
352
369
  };
353
370
  }
354
371
 
372
+ /**
373
+ * Which edges sigma.js draws the label of: none unless `renderEdgeLabels` is on; then, under `edgeLabelAnchors`
374
+ * "nodeLabels", only an edge whose two end nodes show a label.
375
+ * @param snapshot - the graph
376
+ * @param settings - sigma's settings
377
+ * @returns whether edge `i`'s label is drawn
378
+ */
379
+ function sigmaEdgeLabels(snapshot: GraphSnapshot, settings: SigmaSettings): (i: number) => boolean {
380
+ if (!settings.renderEdgeLabels) {
381
+ return () => false;
382
+ }
383
+ if (settings.edgeLabelAnchors === "allNodes") {
384
+ return () => true;
385
+ }
386
+ // ponytail: a node "shows" its label when it has one; sigma also hides labels by its label grid and
387
+ // labelRenderedSizeThreshold, which depend on its framing. Model those once graphty frames a sigma file as sigma does.
388
+ const label = cells(snapshot, "node", "label");
389
+ const shows = (node: number): boolean => {
390
+ const text = label?.(node);
391
+ return typeof text === "string" ? text !== "" : text !== null && text !== undefined;
392
+ };
393
+ return (i) => shows(snapshot.edgeSource(i)) && shows(snapshot.edgeTarget(i));
394
+ }
395
+
355
396
  /** The cell readers of the keys sigma.js draws, null for a key the graph does not have. */
356
397
  interface SigmaKeys {
357
398
  readonly color: ((i: number) => unknown) | null;
358
399
  readonly size: ((i: number) => unknown) | null;
359
400
  readonly label: ((i: number) => unknown) | null;
360
- readonly hidden: ((i: number) => unknown) | null;
361
401
  }
362
402
 
363
403
  /**
364
404
  * Reads one element's sigma.js keys.
365
405
  * @param keys - the cell readers
366
406
  * @param target - nodes or edges
407
+ * @param labeled - whether sigma draws the element's label
367
408
  * @param i - the element
368
409
  * @param put - the setter
369
410
  * @param site - where a value of a property was read
@@ -371,32 +412,61 @@ interface SigmaKeys {
371
412
  function readSigmaRow(
372
413
  keys: SigmaKeys,
373
414
  target: StyleTarget,
415
+ labeled: boolean,
374
416
  i: number,
375
417
  put: (p: string, i: number, v: StyleValue | null) => void,
376
418
  site: (property: string) => CanonSite,
377
419
  ): void {
378
- const { color, size, label, hidden } = keys;
420
+ const { color, size, label } = keys;
379
421
  if (color !== null) {
380
422
  const [fill, opacity] = target === "node" ? ["fill", "fillOpacity"] : ["color", "opacity"];
381
423
  putColor(put, fill, opacity, i, color(i), site(fill));
382
424
  }
383
425
  if (size !== null) {
384
- const v = canonNumber(size(i), site("width"));
385
- put("width", i, v);
386
- if (target === "node") {
387
- put("height", i, v);
388
- }
426
+ putSigmaSize(put, target, i, size(i), site);
389
427
  }
390
- if (label !== null) {
391
- put("label", i, canonText(label(i), site("label")));
428
+ // an edge's label is drawn only under sigma's renderEdgeLabels setting, which a document never carries (the
429
+ // `sigmaSettings` import option); otherwise it is data (graphology-specimen-edge-labels, -p2, -p3)
430
+ if (label !== null && labeled) {
431
+ const text = canonText(label(i), site("label"));
432
+ // sigma draws a label with the canvas's fillText, which draws each ASCII whitespace as a space: one line
433
+ // (graphology-specimen-labels, label-multiline)
434
+ put("label", i, text === null ? null : text.replaceAll(/[\t\n\f\r]/g, " "));
392
435
  }
393
- if (hidden !== null && canonBoolean(hidden(i), site("visible")) === true) {
394
- put("visible", i, false);
436
+ }
437
+
438
+ /**
439
+ * One element's sigma.js size, as its width (and a node's height).
440
+ * @param put - the setter
441
+ * @param target - nodes or edges
442
+ * @param i - the element
443
+ * @param raw - the size value
444
+ * @param site - where a value of a property was read
445
+ */
446
+ function putSigmaSize(
447
+ put: (p: string, i: number, v: StyleValue | null) => void,
448
+ target: StyleTarget,
449
+ i: number,
450
+ raw: unknown,
451
+ site: (property: string) => CanonSite,
452
+ ): void {
453
+ if (target === "node") {
454
+ // sigma draws a node's size as its radius, and a node of negative size not at all
455
+ // (graphology-specimen-node-sizes-opacity, size-negative)
456
+ const v = Number(raw) < 0 ? 0 : canonNumber(raw, site("width"));
457
+ const d = v === null ? null : v * 2;
458
+ put("width", i, d);
459
+ put("height", i, d);
460
+ } else {
461
+ // sigma's edge shader draws an edge half as wide as a node of the same size is in radius: size 6 draws
462
+ // 3 px where a node of size 6 is 5.8 px in radius (graphology-specimen-arrows edge-size-6)
463
+ const w = canonNumber(raw, site("width"));
464
+ put("width", i, w === null ? null : w / 2);
395
465
  }
396
466
  }
397
467
 
398
468
  /**
399
- * A CSS color into a property and its opacity.
469
+ * A color as sigma.js reads it, into a property and its opacity.
400
470
  * @param put - the setter
401
471
  * @param property - the color property
402
472
  * @param opacity - its opacity property, or null when it has none
@@ -412,10 +482,11 @@ function putColor(
412
482
  raw: unknown,
413
483
  site: CanonSite,
414
484
  ): void {
415
- if (raw === null || raw === undefined || raw === "") {
485
+ // sigma draws a missing color with its default, and any string it cannot read ("" too) black
486
+ if (raw === null || raw === undefined) {
416
487
  return;
417
488
  }
418
- const c = canonColor(raw, "css", site);
489
+ const c = canonColor(raw, "graphology", site);
419
490
  if (c === null) {
420
491
  return;
421
492
  }
@@ -475,25 +546,8 @@ function exactFromBlock(
475
546
  written: Written,
476
547
  holdsExactly?: (target: StyleTarget, property: string, value: StyleValue) => boolean,
477
548
  ): void {
478
- if (text === null) {
479
- return;
480
- }
481
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
482
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "json") : null;
483
- if (ex === null) {
484
- return;
485
- }
486
- for (const target of TARGETS) {
549
+ takeFromBlock(snapshot, text, "json", (target, block, ex) => {
487
550
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
488
- const cache = new Map<string, readonly (StyleValue | null)[]>();
489
- const resolved = (p: string): readonly (StyleValue | null)[] => {
490
- let r = cache.get(p);
491
- if (r === undefined) {
492
- r = resolveStyles(ex, target, p as never, snapshot);
493
- cache.set(p, r);
494
- }
495
- return r;
496
- };
497
551
  if (holdsExactly !== undefined) {
498
552
  const properties = new Set(
499
553
  ex.styles.rules
@@ -502,42 +556,19 @@ function exactFromBlock(
502
556
  );
503
557
  for (const property of properties) {
504
558
  const held = (w: StyleValue): boolean => holdsExactly(target, property, w);
505
- takeUnheld(values[target], property, resolved(property), n, held);
559
+ takeUnheld(values[target], property, block(target, property), n, held);
506
560
  }
507
561
  }
508
- for (const [property, write] of Object.entries(written[target] ?? {})) {
509
- takeWritten(values[target], property, n, resolved, write, snapshot);
510
- }
511
- }
512
- }
513
-
514
- /**
515
- * Takes the block's value of a property wherever the native value is what the writer writes for the block's values.
516
- * @param column - the native values of the target, changed in place
517
- * @param property - the property
518
- * @param n - the element count
519
- * @param resolved - the block's values of a property
520
- * @param write - what the writer writes for an element, from the block's values
521
- * @param snapshot - the graph
522
- */
523
- function takeWritten(
524
- column: Values,
525
- property: string,
526
- n: number,
527
- resolved: (p: string) => readonly (StyleValue | null)[],
528
- write: (want: (p: string) => StyleValue | null, i: number, snapshot: GraphSnapshot) => StyleValue | null,
529
- snapshot: GraphSnapshot,
530
- ): void {
531
- const want = resolved(property);
532
- for (let i = 0; i < n; i++) {
533
- const w = want[i];
534
- const native = column[property]?.[i] ?? null;
535
- // a native value the writer made from the block's other values (vis.js's one size for a width) is the
536
- // block's own value, none included
537
- if (native !== w && native !== null && native === write((p) => resolved(p)[i], i, snapshot)) {
538
- setValue(column, property, n, i, w);
539
- }
540
- }
562
+ const writes = written[target] ?? {};
563
+ return {
564
+ values: values[target],
565
+ properties: Object.keys(writes),
566
+ // a native value the writer made from the block's other values (vis.js's one size for a width) is the
567
+ // block's own value, none included
568
+ readBack: (i, want) => (property) => writes[property](want, i, snapshot),
569
+ take: (_property, _wanted, native) => native !== null,
570
+ };
571
+ });
541
572
  }
542
573
 
543
574
  /**
@@ -592,6 +623,19 @@ const VIS_SIZED: ReadonlySet<string> = new Set([
592
623
  "icon",
593
624
  ]);
594
625
 
626
+ /** vis-network's node shapes sized by their label, as the `labelFit` facts of each (vis-network 9.1.9 `resize`). */
627
+ const VIS_LABEL_FIT: ReadonlyMap<string, { readonly square: boolean; readonly ellipse: boolean }> = new Map([
628
+ ["box", { square: false, ellipse: false }],
629
+ ["text", { square: false, ellipse: false }],
630
+ ["circle", { square: true, ellipse: false }],
631
+ ["database", { square: true, ellipse: false }],
632
+ ["ellipse", { square: false, ellipse: true }],
633
+ ]);
634
+
635
+ /** vis-network's default `margin` and font size (vis-network.esm.js NodesHandler defaults). */
636
+ const VIS_MARGIN = 5;
637
+ const VIS_FONT_SIZE = 14;
638
+
595
639
  /**
596
640
  * vis-network's palette for a group the file does not define, in the order groups first appear (vis-network
597
641
  * 10.1.2, `Groups._defaultGroups`): border, background.
@@ -619,6 +663,10 @@ const VIS_GROUP_PALETTE: readonly (readonly [string, string])[] = [
619
663
  ["#EE0000", "#990000"],
620
664
  ];
621
665
 
666
+ /** vis-network's node colors where nothing sets them (vis-network 9.1.9 `NodesHandler` defaults `color`). */
667
+ const VIS_NODE_FILL = "#97C2FC";
668
+ const VIS_NODE_BORDER = "#2B7CE9";
669
+
622
670
  /** vis-network's edge color when an edge inherits none (vis-network 10.1.2, `color.color`). */
623
671
  const VIS_EDGE_COLOR = "#848484";
624
672
 
@@ -690,20 +738,65 @@ interface VisLayer {
690
738
  border?: unknown;
691
739
  shape?: unknown;
692
740
  size?: unknown;
741
+ value?: unknown;
742
+ scalingMin?: unknown;
743
+ scalingMax?: unknown;
693
744
  borderWidth?: unknown;
694
745
  opacity?: unknown;
695
746
  hidden?: unknown;
696
747
  label?: unknown;
697
748
  title?: unknown;
749
+ /** edges: whether `font` was given as text, which leaves the edge's label off the canvas (visValues) */
750
+ fontText?: unknown;
751
+ /** `font.multi`: whether the label's text holds html or markdown markup (visLabelText) */
752
+ fontMulti?: unknown;
753
+ fontStrokeWidth?: unknown;
754
+ fontStrokeColor?: unknown;
755
+ fontBackground?: unknown;
756
+ fontAlign?: unknown;
698
757
  fontColor?: unknown;
699
758
  fontSize?: unknown;
700
759
  fontFace?: unknown;
760
+ /** `font.vadjust`: how far down the label's text is drawn */
761
+ fontVadjust?: unknown;
762
+ /** `font.bold`, `font.ital`, `font.boldital` and `font.mono`: each font modifier's own options */
763
+ fontMods?: unknown;
764
+ margin?: unknown;
765
+ /** `widthConstraint` (a number) or its `maximum`: the width the label's lines wrap at */
766
+ wrapWidth?: unknown;
767
+ /** `scaling.label`: the label's font size from `value` when enabled */
768
+ scalingLabel?: unknown;
769
+ /** nodes: `shapeProperties.borderDashes` */
770
+ borderDashes?: unknown;
771
+ /** nodes: `shapeProperties.borderRadius`, a box's corner radius */
772
+ borderRadius?: unknown;
773
+ widthMin?: unknown;
774
+ heightMin?: unknown;
775
+ /** nodes: `heightConstraint.valign` */
776
+ valign?: unknown;
701
777
  width?: unknown;
702
778
  dashes?: unknown;
703
779
  arrows?: unknown;
704
780
  edgeColor?: unknown;
705
781
  edgeOpacity?: unknown;
706
782
  inherit?: unknown;
783
+ background?: unknown;
784
+ smooth?: unknown;
785
+ selfReference?: unknown;
786
+ selfReferenceSize?: unknown;
787
+ physics?: unknown;
788
+ image?: unknown;
789
+ brokenImage?: unknown;
790
+ useImageSize?: unknown;
791
+ useBorderWithImage?: unknown;
792
+ interpolation?: unknown;
793
+ imagePadding?: unknown;
794
+ coordinateOrigin?: unknown;
795
+ iconCode?: unknown;
796
+ iconFace?: unknown;
797
+ iconSize?: unknown;
798
+ iconColor?: unknown;
799
+ iconWeight?: unknown;
707
800
  }
708
801
 
709
802
  /**
@@ -730,7 +823,14 @@ function visLayer(o: unknown, target: StyleTarget): VisLayer {
730
823
  set("hidden", o.hidden);
731
824
  set("label", o.label);
732
825
  set("title", o.title);
826
+ set("wrapWidth", isJsonObject(o.widthConstraint) ? o.widthConstraint.maximum : o.widthConstraint);
827
+ if (isJsonObject(o.scaling)) {
828
+ set("scalingLabel", o.scaling.label);
829
+ }
733
830
  visFontLayer(o.font, set);
831
+ if (target === "edge" && o.font !== undefined && o.font !== null) {
832
+ set("fontText", typeof o.font === "string");
833
+ }
734
834
  return layer;
735
835
  }
736
836
 
@@ -754,8 +854,146 @@ function visNodeLayer(o: Record<string, unknown>, set: SetLayer): void {
754
854
  }
755
855
  set("shape", o.shape);
756
856
  set("size", o.size);
857
+ set("value", o.value);
858
+ if (isJsonObject(o.scaling)) {
859
+ set("scalingMin", o.scaling.min);
860
+ set("scalingMax", o.scaling.max);
861
+ }
757
862
  set("borderWidth", o.borderWidth);
758
863
  set("opacity", o.opacity);
864
+ set("margin", o.margin);
865
+ set("widthMin", isJsonObject(o.widthConstraint) ? o.widthConstraint.minimum : o.widthConstraint);
866
+ set("heightMin", isJsonObject(o.heightConstraint) ? o.heightConstraint.minimum : o.heightConstraint);
867
+ set("valign", isJsonObject(o.heightConstraint) ? o.heightConstraint.valign : undefined);
868
+ set("image", isJsonObject(o.image) ? o.image.unselected : o.image);
869
+ set("brokenImage", o.brokenImage);
870
+ set("imagePadding", o.imagePadding);
871
+ if (isJsonObject(o.icon)) {
872
+ set("iconCode", o.icon.code);
873
+ set("iconFace", o.icon.face);
874
+ set("iconSize", o.icon.size);
875
+ set("iconColor", o.icon.color);
876
+ set("iconWeight", o.icon.weight);
877
+ }
878
+ if (isJsonObject(o.shapeProperties)) {
879
+ set("useImageSize", o.shapeProperties.useImageSize);
880
+ set("useBorderWithImage", o.shapeProperties.useBorderWithImage);
881
+ set("interpolation", o.shapeProperties.interpolation);
882
+ set("coordinateOrigin", o.shapeProperties.coordinateOrigin);
883
+ set("borderDashes", o.shapeProperties.borderDashes);
884
+ set("borderRadius", o.shapeProperties.borderRadius);
885
+ }
886
+ }
887
+
888
+ /**
889
+ * What a vis-network `image` or `circularImage` node draws (vis-network 9.1.9 `Image`, `CircularImage`,
890
+ * `CircleImageBase._resizeImage`): its picture, the broken image in its place when it has none, with its shorter
891
+ * side across the node's `2 * size` square (or at its own pixels with `shapeProperties.useImageSize`); an `image`
892
+ * node is the picture alone, reaching past that square, or over a box of its background with
893
+ * `shapeProperties.useBorderWithImage`; a `circularImage` is cut to a circle and ringed by its border. Its
894
+ * `imagePadding` keeps the picture that far inside the node's box on each side, stretched to what is left
895
+ * (`_drawImageAtPosition`); the node's `opacity` fades the picture too; with `shapeProperties.coordinateOrigin`
896
+ * "top-left" an `image` node with no box of its own draws its picture with its top left corner at the node's position.
897
+ * @param layer - the node's merged layer
898
+ * @param shape - its vis-network shape
899
+ * @param box - the node's width and height, in the file's units
900
+ * @param box.width - its width
901
+ * @param box.height - its height
902
+ * @returns the values, none for another shape or a node with no picture
903
+ */
904
+ function visImage(layer: VisLayer, shape: string, box: { width: number; height: number }): Row {
905
+ const text = (v: unknown): string | null => (typeof v === "string" && v !== "" ? v : null);
906
+ const [own, broken] = [text(layer.image), text(layer.brokenImage)];
907
+ const src = own ?? broken;
908
+ if ((shape !== "image" && shape !== "circularImage") || src === null) {
909
+ return {};
910
+ }
911
+ const circular = shape === "circularImage";
912
+ const row: Row = {
913
+ image: src,
914
+ ...(own !== null && broken !== null ? { imageFallback: broken } : {}),
915
+ imageFit: layer.useImageSize === true ? "none" : "cover",
916
+ imageClip: circular ? "node" : "none",
917
+ shape: "ellipse",
918
+ };
919
+ if (!circular) {
920
+ row.shape = layer.useBorderWithImage === true ? "rectangle" : "none";
921
+ }
922
+ if (layer.interpolation === false) {
923
+ row.imageSmoothing = false;
924
+ }
925
+ const opacity = typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity < 1 ? layer.opacity : null;
926
+ if (opacity !== null) {
927
+ row.imageOpacity = opacity;
928
+ }
929
+ visImagePlace(row, layer, box);
930
+ return row;
931
+ }
932
+
933
+ /**
934
+ * Where a vis-network picture sits in its node's box: inside its `imagePadding`, and from the top left corner under
935
+ * `shapeProperties.coordinateOrigin` "top-left" (visImage).
936
+ * @param row - the picture's values so far, written in place
937
+ * @param layer - the node's merged layer
938
+ * @param box - the node's width and height, in the file's units
939
+ * @param box.width - its width
940
+ * @param box.height - its height
941
+ */
942
+ function visImagePlace(row: Row, layer: VisLayer, box: { width: number; height: number }): void {
943
+ const pad = visImagePadding(layer.imagePadding);
944
+ if (pad !== null && layer.useImageSize !== true) {
945
+ row.imageFit = "none";
946
+ row.imageWidth = Math.max(0, box.width - pad.left - pad.right);
947
+ row.imageHeight = Math.max(0, box.height - pad.top - pad.bottom);
948
+ row.imageOffsetX = (pad.left - pad.right) / 2;
949
+ row.imageOffsetY = (pad.top - pad.bottom) / 2;
950
+ }
951
+ if (layer.coordinateOrigin === "top-left" && row.shape === "none") {
952
+ row.imageOffsetX = box.width / 2 + (typeof row.imageOffsetX === "number" ? row.imageOffsetX : 0);
953
+ row.imageOffsetY = box.height / 2 + (typeof row.imageOffsetY === "number" ? row.imageOffsetY : 0);
954
+ }
955
+ }
956
+
957
+ /**
958
+ * What a vis-network `icon` node draws (vis-network 9.1.9 `Icon`): its `icon.code` in `icon.face` (FontAwesome
959
+ * unless given) at `icon.size` (50) pixels and `icon.weight`, in `icon.color` (#2B7CE9), centered on the node; no
960
+ * code draws nothing.
961
+ * @param layer - the node's merged layer
962
+ * @param site - where a bad color is noted
963
+ * @returns the values, none for an icon with no code
964
+ */
965
+ function visIcon(layer: VisLayer, site: CanonSite): Row {
966
+ const code = typeof layer.iconCode === "string" && layer.iconCode !== "" ? layer.iconCode : null;
967
+ if (code === null) {
968
+ return {};
969
+ }
970
+ const size = typeof layer.iconSize === "number" && layer.iconSize >= 0 ? layer.iconSize : 50;
971
+ const color = typeof layer.iconColor === "string" ? canonColor(layer.iconColor, "vis", site) : null;
972
+ const weight = layer.iconWeight;
973
+ const bold = weight === "bold" || weight === "bolder" || (typeof weight === "number" && weight >= 600);
974
+ return {
975
+ glyph: code,
976
+ glyphFont: typeof layer.iconFace === "string" && layer.iconFace !== "" ? layer.iconFace : "FontAwesome",
977
+ glyphSize: size,
978
+ glyphColor: color?.color ?? "#2b7ce9",
979
+ glyphWeight: bold ? "bold" : "normal",
980
+ };
981
+ }
982
+
983
+ /**
984
+ * vis-network's `imagePadding`: one number for every side, or `{ top, right, bottom, left }` (each 0 unless given).
985
+ * @param raw - the option
986
+ * @returns the four sides, or null for no padding
987
+ */
988
+ function visImagePadding(raw: unknown): { top: number; right: number; bottom: number; left: number } | null {
989
+ const side = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
990
+ let pad: { top: number; right: number; bottom: number; left: number } | null = null;
991
+ if (typeof raw === "number") {
992
+ pad = { top: side(raw), right: side(raw), bottom: side(raw), left: side(raw) };
993
+ } else if (isJsonObject(raw)) {
994
+ pad = { top: side(raw.top), right: side(raw.right), bottom: side(raw.bottom), left: side(raw.left) };
995
+ }
996
+ return pad === null || pad.top + pad.right + pad.bottom + pad.left === 0 ? null : pad;
759
997
  }
760
998
 
761
999
  /**
@@ -775,8 +1013,18 @@ function visEdgeLayer(o: Record<string, unknown>, set: SetLayer): void {
775
1013
  set("inherit", own ? false : color.inherit);
776
1014
  }
777
1015
  set("width", o.width);
1016
+ set("value", o.value);
1017
+ if (isJsonObject(o.scaling)) {
1018
+ set("scalingMin", o.scaling.min);
1019
+ set("scalingMax", o.scaling.max);
1020
+ }
778
1021
  set("dashes", o.dashes);
779
1022
  set("arrows", o.arrows);
1023
+ set("background", o.background);
1024
+ set("smooth", o.smooth);
1025
+ set("selfReference", o.selfReference);
1026
+ set("selfReferenceSize", o.selfReferenceSize);
1027
+ set("physics", o.physics);
780
1028
  }
781
1029
 
782
1030
  /**
@@ -786,9 +1034,19 @@ function visEdgeLayer(o: Record<string, unknown>, set: SetLayer): void {
786
1034
  */
787
1035
  function visFontLayer(font: unknown, set: SetLayer): void {
788
1036
  if (isJsonObject(font)) {
1037
+ set("fontMulti", font.multi);
1038
+ set("fontStrokeWidth", font.strokeWidth);
1039
+ set("fontStrokeColor", font.strokeColor);
1040
+ set("fontBackground", font.background);
1041
+ set("fontAlign", font.align);
789
1042
  set("fontColor", font.color);
790
1043
  set("fontSize", font.size);
791
1044
  set("fontFace", font.face);
1045
+ set("fontVadjust", font.vadjust);
1046
+ const mods = ["bold", "ital", "boldital", "mono"].filter((m) => font[m] !== undefined && font[m] !== null);
1047
+ if (mods.length > 0) {
1048
+ set("fontMods", Object.fromEntries(mods.map((m) => [m, font[m]])));
1049
+ }
792
1050
  } else if (typeof font === "string") {
793
1051
  const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(font.slice(0, 256));
794
1052
  if (m !== null) {
@@ -814,7 +1072,7 @@ function visValues(layer: VisLayer, target: StyleTarget, site: (property: string
814
1072
  out[p] = v;
815
1073
  }
816
1074
  };
817
- const color = (p: string, opacity: string | null, raw: unknown): void => {
1075
+ const color: PutColor = (p, opacity, raw) => {
818
1076
  if (raw === undefined || raw === null || raw === "") {
819
1077
  return;
820
1078
  }
@@ -828,29 +1086,508 @@ function visValues(layer: VisLayer, target: StyleTarget, site: (property: string
828
1086
  };
829
1087
  if (target === "node") {
830
1088
  color("fill", "fillOpacity", layer.fill);
831
- color("borderColor", null, layer.border);
1089
+ color("borderColor", "borderOpacity", layer.border);
832
1090
  visNodeValues(layer, sized, put, site);
833
1091
  } else {
834
1092
  color("color", "opacity", layer.edgeColor);
835
1093
  visEdgeValues(layer, put, site);
1094
+ visEdgeBackground(layer, put, color);
836
1095
  }
837
1096
  if (layer.hidden !== undefined && canonBoolean(layer.hidden, site("visible")) === true) {
838
1097
  put("visible", false);
839
1098
  }
840
- if (layer.label !== undefined) {
841
- put("label", canonText(layer.label, site("label")));
1099
+ visLabelValues(layer, target, put, color, site);
1100
+ return out;
1101
+ }
1102
+
1103
+ /**
1104
+ * An edge's background: a band `size` wide under the line, its outline beyond the line. vis-network draws it only
1105
+ * under a smooth (bezier) edge (BezierEdgeBase._bezierCurve calls drawBackground; StraightEdge never does).
1106
+ * @param layer - the layer
1107
+ * @param put - the row's setter
1108
+ * @param color - the row's color setter
1109
+ */
1110
+ function visEdgeBackground(layer: VisLayer, put: PutRow, color: PutColor): void {
1111
+ // ponytail: `smooth` is read from this layer alone, else vis-network's default (smooth)
1112
+ if (!isJsonObject(layer.background)) {
1113
+ return;
842
1114
  }
843
- if (layer.title !== undefined) {
844
- put("tooltip", canonText(layer.title, site("tooltip")));
1115
+ const bg = layer.background;
1116
+ const straight = layer.smooth === false || (isJsonObject(layer.smooth) && layer.smooth.enabled === false);
1117
+ color("outlineColor", "outlineOpacity", bg.color);
1118
+ if (bg.enabled !== undefined) {
1119
+ const size = typeof bg.size === "number" ? bg.size : 10;
1120
+ const width = typeof layer.width === "number" ? layer.width : 1;
1121
+ put("outlineWidth", bg.enabled === true && !straight ? Math.max(0, size - width) : 0);
845
1122
  }
846
- color("labelColor", null, layer.fontColor);
1123
+ const band = visBandStyle(bg, false);
1124
+ if (band !== null) {
1125
+ put("outlineStyle", band);
1126
+ }
1127
+ }
1128
+
1129
+ /** Puts a color of a row, and its alpha as an opacity when it has one (visValues). */
1130
+ type PutColor = (p: string, opacity: string | null, raw: unknown) => void;
1131
+
1132
+ /**
1133
+ * The label values of a vis.js layer: its text (markup read), wrap width, font, outline, background and, for an edge,
1134
+ * where on the line it sits.
1135
+ * @param layer - the layer
1136
+ * @param target - nodes or edges
1137
+ * @param put - the row's setter
1138
+ * @param color - the row's color setter
1139
+ * @param site - where its values are read
1140
+ */
1141
+ function visLabelValues(
1142
+ layer: VisLayer,
1143
+ target: StyleTarget,
1144
+ put: PutRow,
1145
+ color: PutColor,
1146
+ site: (property: string) => CanonSite,
1147
+ ): void {
847
1148
  if (layer.fontSize !== undefined) {
848
1149
  put("labelSize", canonNumber(layer.fontSize, site("labelSize")));
849
1150
  }
850
1151
  if (layer.fontFace !== undefined) {
851
1152
  put("labelFont", canonText(layer.fontFace, site("labelFont")));
852
1153
  }
853
- return out;
1154
+ if (layer.label !== undefined) {
1155
+ visLabelText(layer, target, put, site);
1156
+ }
1157
+ // vis-network wraps a label's lines at `widthConstraint.maximum` (or a number), margins not taken off:
1158
+ // Label.adjustSizes subtracts them only under constrainWidth, which nothing sets. LabelSplitter keeps each blank
1159
+ // as a word of its own and joins words with a blank (getLongestFit), so a blank between two words counts three.
1160
+ if (typeof layer.wrapWidth === "number" && Number.isFinite(layer.wrapWidth) && layer.wrapWidth > 0) {
1161
+ put("labelWrapWidth", layer.wrapWidth);
1162
+ put("labelWrapSpaceCount", 3);
1163
+ }
1164
+ if (layer.title !== undefined) {
1165
+ put("tooltip", canonText(layer.title, site("tooltip")));
1166
+ }
1167
+ // a label drawn under `scaling.label.drawThreshold - 1` px on screen is not drawn (Label.draw)
1168
+ const threshold = isJsonObject(layer.scalingLabel) ? layer.scalingLabel.drawThreshold : undefined;
1169
+ if (typeof threshold === "number" && Number.isFinite(threshold)) {
1170
+ put("labelMinScreenSize", Math.max(0, threshold - 1));
1171
+ }
1172
+ visLabelLook(layer, target, put, color, site);
1173
+ }
1174
+
1175
+ /**
1176
+ * The text of a vis.js layer's label, its markup read into runs.
1177
+ * @param layer - the layer
1178
+ * @param target - nodes or edges
1179
+ * @param put - the row's setter
1180
+ * @param site - where its values are read
1181
+ */
1182
+ function visLabelText(layer: VisLayer, target: StyleTarget, put: PutRow, site: (property: string) => CanonSite): void {
1183
+ // vis-network 9.1.9 draws no edge label whose `font` is text: Edge.drawLabel reads `options.font.align` (undefined
1184
+ // on a string) as not horizontal and moves the context to the label point, while the label module, which
1185
+ // parsed the text, still draws at the absolute point, so the label lands off the canvas
1186
+ const text = target === "edge" && layer.fontText === true ? "" : canonText(layer.label, site("label"));
1187
+ const marked = typeof text === "string" ? visMarkup(text, layer.fontMulti) : null;
1188
+ put("label", marked?.text ?? text);
1189
+ if (marked !== null) {
1190
+ const runs = visRuns(marked.runs, layer, site("labelRuns"));
1191
+ put("labelRuns", runs);
1192
+ if (runs !== null) {
1193
+ put("labelRunAlign", visRunAlign(layer, target));
1194
+ }
1195
+ }
1196
+ // after the face: a label all in mono is drawn in the mono face
1197
+ for (const [p, v] of Object.entries(VIS_MOD_VALUES[marked?.mod ?? "normal"] ?? {})) {
1198
+ put(p, v);
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * The look of a vis.js layer's label: its color, background box, letter outline and, for an edge, where on the line
1204
+ * it sits.
1205
+ * @param layer - the layer
1206
+ * @param target - nodes or edges
1207
+ * @param put - the row's setter
1208
+ * @param color - the row's color setter
1209
+ * @param site - where its values are read
1210
+ */
1211
+ function visLabelLook(
1212
+ layer: VisLayer,
1213
+ target: StyleTarget,
1214
+ put: PutRow,
1215
+ color: PutColor,
1216
+ site: (property: string) => CanonSite,
1217
+ ): void {
1218
+ color("labelColor", null, layer.fontColor);
1219
+ // font.background "none" (vis-network's default) draws no box
1220
+ color("labelBackground", null, layer.fontBackground === "none" ? undefined : layer.fontBackground);
1221
+ // the box is the words' own: as wide as the text and one font size tall a line (Label._drawBackground: getSize)
1222
+ if (typeof layer.fontBackground === "string" && layer.fontBackground !== "none") {
1223
+ put("labelPadding", 0);
1224
+ put("labelLineHeight", 1);
1225
+ }
1226
+ // a stroke with no color of its own is vis-network's default, white (font.strokeColor "#ffffff")
1227
+ const stroked = typeof layer.fontStrokeWidth === "number" && layer.fontStrokeWidth > 0;
1228
+ color("labelOutlineColor", null, layer.fontStrokeColor ?? (stroked ? "#ffffff" : undefined));
1229
+ if (layer.fontStrokeWidth !== undefined) {
1230
+ put("labelOutlineWidth", canonNumber(layer.fontStrokeWidth, site("labelOutlineWidth")));
1231
+ }
1232
+ if (target === "edge" && typeof layer.fontAlign === "string") {
1233
+ // vis-network draws an edge label on the line ("horizontal", "middle"), above it ("top") or below it ("bottom")
1234
+ put("labelPosition", layer.fontAlign === "top" || layer.fontAlign === "bottom" ? layer.fontAlign : "center");
1235
+ } else if (target === "node" && typeof layer.fontAlign === "string") {
1236
+ // a node label's lines are centered on one another, or flush left or right in the label's box (Label._drawText:
1237
+ // any align but center and right draws flush left); an edge label's lines are always centered
1238
+ put("labelTextAlign", { center: "center", right: "right" }[layer.fontAlign] ?? "left");
1239
+ }
1240
+ // the text is drawn `font.vadjust` lower than its place (Label._drawText: y + block.vadjust)
1241
+ if (typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) && layer.fontVadjust !== 0) {
1242
+ put("labelOffsetY", layer.fontVadjust);
1243
+ }
1244
+ }
1245
+
1246
+ /** The label values of a label whose every word has one vis.js font modifier; vis-network's mono face is monospace. */
1247
+ const VIS_MOD_VALUES: Readonly<Record<string, Row>> = {
1248
+ bold: { labelWeight: "bold" },
1249
+ ital: { labelItalic: true },
1250
+ boldital: { labelWeight: "bold", labelItalic: true },
1251
+ mono: { labelFont: "monospace" },
1252
+ };
1253
+
1254
+ /**
1255
+ * Whether vis-network's node `opacity` replaces a color's alpha: a hex color of 3 or 6 digits or an `rgb()` color, and
1256
+ * vis-network's own default (overrideOpacity; hexToRGB reads no other text).
1257
+ * @param color - the color as the file gives it, or undefined for vis-network's default
1258
+ * @returns true when the opacity applies
1259
+ */
1260
+ function visTakesOpacity(color: unknown): boolean {
1261
+ if (color === undefined) {
1262
+ return true;
1263
+ }
1264
+ return (
1265
+ typeof color === "string" &&
1266
+ !color.includes("rgba") &&
1267
+ (color.includes("rgb") || /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color.trim()))
1268
+ );
1269
+ }
1270
+
1271
+ /**
1272
+ * The line style of an edge's background band: stroked `size` wide (10 by default) in its own dashes, [5, 5] for true
1273
+ * (EdgeBase.drawBackground, setStrokeDashed), with the canvas's line caps.
1274
+ * @param bg - the edge's `background`
1275
+ * @param round - whether the canvas draws round caps by then (see visStyles' edge loop)
1276
+ * @returns the line style, or null for a band with no dashes
1277
+ */
1278
+ function visBandStyle(bg: Readonly<Record<string, unknown>>, round: boolean): string | null {
1279
+ if (bg.dashes === undefined || bg.dashes === false) {
1280
+ return null;
1281
+ }
1282
+ return visDashStyle(bg.dashes === true ? [5, 5] : bg.dashes, typeof bg.size === "number" ? bg.size : 10, round);
1283
+ }
1284
+
1285
+ /**
1286
+ * vis.js dashes (`dashes`, `shapeProperties.borderDashes`) as the line style they draw at a width, read as a dash
1287
+ * array is read anywhere: with round caps each dash a width longer and each gap a width shorter; then gaps under a
1288
+ * tenth of the pattern are solid, dashes no longer than 1.5 widths are dotted, dashes of which the longest is twice
1289
+ * the shortest dash-dot, others dashed; false is solid.
1290
+ * @param dashes - false, or the dash array (dash, gap, dash, gap ...; an odd one repeats)
1291
+ * @param width - the line's width
1292
+ * @param round - whether the dashes have round caps
1293
+ * @returns the line style
1294
+ */
1295
+ function visDashStyle(dashes: unknown, width: number, round: boolean): string {
1296
+ if (!Array.isArray(dashes)) {
1297
+ return dashes === false ? "solid" : "dashed";
1298
+ }
1299
+ const lengths = dashes.filter((d): d is number => typeof d === "number" && Number.isFinite(d));
1300
+ const pairs = lengths.length % 2 === 1 ? [...lengths, ...lengths] : lengths;
1301
+ const grow = round ? width : 0;
1302
+ const on = pairs.filter((_d, i) => i % 2 === 0).map((d) => Math.max(0, d) + grow);
1303
+ const off = pairs.filter((_d, i) => i % 2 === 1).map((d) => Math.max(0, d - grow));
1304
+ const sum = (xs: number[]): number => xs.reduce((a, b) => a + b, 0);
1305
+ // a gap of exactly a tenth still shows (mappings-bypass edge-default: [5, 5] at width 4)
1306
+ if (on.length === 0 || sum(off) < 0.1 * (sum(on) + sum(off))) {
1307
+ return "solid";
1308
+ }
1309
+ if (on.every((d) => d <= 1.5 * Math.max(1, width))) {
1310
+ return "dotted";
1311
+ }
1312
+ return Math.max(...on) >= 2 * Math.min(...on) ? "dash-dot" : "dashed";
1313
+ }
1314
+
1315
+ /**
1316
+ * vis.js label markup (`font.multi`): true or "html" reads `<b>`, `<i>` and `<code>` and the entities `&lt;` and
1317
+ * `&amp;`; "markdown" or "md" reads `*bold*`, `_ital_` and `` `mono` `` at word edges, with `\` escaping a mark
1318
+ * (vis-network 9.1.9 `LabelSplitter.splitHtmlBlocks`, `splitMarkdownBlocks`). The text without its markup, and the
1319
+ * one font modifier every word has when they share one.
1320
+ * @param text - the label
1321
+ * @param multi - `font.multi`
1322
+ * @returns the plain text and the shared modifier, or null when the label is not read as markup
1323
+ */
1324
+ function visMarkup(
1325
+ text: string,
1326
+ multi: unknown,
1327
+ ): { text: string; mod: string | null; runs: { text: string; mod: string }[] } | null {
1328
+ const system = VIS_MARKUP_SYSTEM.get(multi);
1329
+ if (system === undefined) {
1330
+ return null;
1331
+ }
1332
+ const state: VisMarkState = { bold: false, ital: false, mono: false };
1333
+ const runs: { text: string; mod: string }[] = [];
1334
+ const source = text.slice(0, 4096);
1335
+ let i = 0;
1336
+ while (i < source.length) {
1337
+ const tagged = visMarkTag(source, i, system, state);
1338
+ if (tagged > 0) {
1339
+ i += tagged;
1340
+ continue;
1341
+ }
1342
+ const [ch, next] = visMarkChar(source, i, system);
1343
+ runs.push({ text: ch, mod: visMarkMod(state) });
1344
+ i = next;
1345
+ }
1346
+ const mods = new Set(runs.filter((r) => /\S/.test(r.text)).map((r) => r.mod));
1347
+ const only = mods.size === 1 ? [...mods][0] : null;
1348
+ // the characters in runs of one modifier each, as vis-network draws its blocks
1349
+ const merged: { text: string; mod: string }[] = [];
1350
+ for (const r of runs) {
1351
+ const last = merged.at(-1);
1352
+ if (last?.mod === r.mod) {
1353
+ last.text += r.text;
1354
+ } else {
1355
+ merged.push({ ...r });
1356
+ }
1357
+ }
1358
+ return { text: runs.map((r) => r.text).join(""), mod: only === "normal" ? null : only, runs: merged };
1359
+ }
1360
+
1361
+ /**
1362
+ * vis-network's own options of each font modifier (vis-network 9.1.9 `NodesHandler` defaults, `font.mono`); an option
1363
+ * a modifier does not set is the label's own (Label.propagateFonts).
1364
+ */
1365
+ const VIS_MOD_DEFAULTS: Readonly<Record<string, { mod: string; face?: string; size?: number; vadjust?: number }>> = {
1366
+ bold: { mod: "bold" },
1367
+ ital: { mod: "italic" },
1368
+ boldital: { mod: "bold italic" },
1369
+ mono: { mod: "", face: "monospace", size: 15, vadjust: 2 },
1370
+ };
1371
+
1372
+ /**
1373
+ * vis.js multi-font runs as the neutral `labelRuns`: each run's font as vis-network draws it (its modifier's own
1374
+ * `font.bold`, `font.ital`, `font.boldital` or `font.mono` options, an object or text like "16px arial black", over
1375
+ * the label's font; mono is 15 px monospace unless given), each field only where it differs from the label's own, and
1376
+ * `shift`: how much lower than the label's own words its modifier's `vadjust` draws the run (mono's is 2 unless given;
1377
+ * the others' are the label's).
1378
+ * @param runs - the runs, each with its modifier
1379
+ * @param layer - the element's layer
1380
+ * @param site - where a bad color is noted
1381
+ * @returns the runs as JSON, or null when every run is drawn in the label's own font
1382
+ */
1383
+ function visRuns(runs: readonly { text: string; mod: string }[], layer: VisLayer, site: CanonSite): string | null {
1384
+ const own = {
1385
+ face: typeof layer.fontFace === "string" ? layer.fontFace : "arial",
1386
+ size: typeof layer.fontSize === "number" ? layer.fontSize : VIS_FONT_SIZE,
1387
+ color: typeof layer.fontColor === "string" ? layer.fontColor : null,
1388
+ vadjust: typeof layer.fontVadjust === "number" && Number.isFinite(layer.fontVadjust) ? layer.fontVadjust : 0,
1389
+ };
1390
+ const mods = isJsonObject(layer.fontMods) ? layer.fontMods : {};
1391
+ let differs = false;
1392
+ const out = runs.map((r) => {
1393
+ const run: Record<string, unknown> = { text: r.text };
1394
+ const base = VIS_MOD_DEFAULTS[r.mod];
1395
+ if (base === undefined) {
1396
+ return run;
1397
+ }
1398
+ const given = visModOptions(mods[r.mod]);
1399
+ const modText = typeof given.mod === "string" ? given.mod : base.mod;
1400
+ const face = typeof given.face === "string" ? given.face : (base.face ?? own.face);
1401
+ const size = typeof given.size === "number" ? given.size : (base.size ?? own.size);
1402
+ const vadjust = typeof given.vadjust === "number" ? given.vadjust : (base.vadjust ?? own.vadjust);
1403
+ const color = typeof given.color === "string" ? canonColor(given.color, "vis", site)?.color : null;
1404
+ if (modText.includes("bold")) {
1405
+ run.weight = "bold";
1406
+ }
1407
+ if (modText.includes("italic")) {
1408
+ run.italic = true;
1409
+ }
1410
+ if (face !== null && face !== own.face) {
1411
+ run.font = face;
1412
+ }
1413
+ if (size !== own.size) {
1414
+ run.size = size;
1415
+ }
1416
+ if (Number.isFinite(vadjust) && vadjust !== own.vadjust) {
1417
+ run.shift = vadjust - own.vadjust;
1418
+ }
1419
+ if (typeof color === "string") {
1420
+ run.color = color;
1421
+ }
1422
+ differs ||= Object.keys(run).length > 1;
1423
+ return run;
1424
+ });
1425
+ return differs ? JSON.stringify(out) : null;
1426
+ }
1427
+
1428
+ /**
1429
+ * 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
1430
+ * `textBaseline` (Label._drawText): "hanging" below a node sized by `size` (ShapeBase._drawShape), "middle" inside
1431
+ * one sized by its label and on an edge's line, "alphabetic" above an edge (`font.align` top) and "hanging" below it.
1432
+ * @param layer - the element's merged layer
1433
+ * @param target - nodes or edges
1434
+ * @returns the neutral run alignment
1435
+ */
1436
+ function visRunAlign(layer: VisLayer, target: StyleTarget): "baseline" | "hanging" | "middle" {
1437
+ if (target === "edge") {
1438
+ return (
1439
+ ({ top: "baseline", bottom: "hanging" } as Record<string, "baseline" | "hanging">)[
1440
+ String(layer.fontAlign)
1441
+ ] ?? "middle"
1442
+ );
1443
+ }
1444
+ const shape = typeof layer.shape === "string" ? layer.shape : "ellipse";
1445
+ return VIS_SIZED.has(shape) ? "hanging" : "middle";
1446
+ }
1447
+
1448
+ /**
1449
+ * How much lower than a neutral `bottom` label vis-network draws the label of a node sized by `size`: it hangs the
1450
+ * text's top 4 + size / 2 below the outline, whatever the line count (ShapeBase._drawShape puts the label half its
1451
+ * height below the node and Label.calculateLabelSize moves its first line up by that again, then down 4 and half a
1452
+ * font size more), where a `bottom` label's words start about 2 + size / 3 below it (a small gap, then the space
1453
+ * above the capitals in a line's box). Measured on the vis-network labels sheet, font sizes 6 to 40.
1454
+ * @param size - the label's font size
1455
+ * @returns the extra distance down
1456
+ */
1457
+ function visBelowGap(size: number): number {
1458
+ // ponytail: a fixed fit of the renderer's gap and line box; a neutral "hang the text's top here" anchor would remove it
1459
+ return 2 + size / 6;
1460
+ }
1461
+
1462
+ /**
1463
+ * How much farther below a node sized by `size` vis-network draws a label in runs than one in its own font alone:
1464
+ * half of what its lines are taller (each line as tall as its largest run, Label: line.height) than one font size each
1465
+ * (ShapeBase._drawShape centers the label half its height below the node, then draws its first line at the top).
1466
+ * @param runs - the label's runs, as `visRuns` writes them
1467
+ * @param size - the label's font size
1468
+ * @returns the distance, 0 for runs no larger than the label's font
1469
+ */
1470
+ function visRunsDrop(runs: string, size: number): number {
1471
+ let parsed: unknown;
1472
+ try {
1473
+ parsed = JSON.parse(runs);
1474
+ } catch {
1475
+ return 0;
1476
+ }
1477
+ if (!Array.isArray(parsed)) {
1478
+ return 0;
1479
+ }
1480
+ const lines: number[] = [size];
1481
+ for (const run of parsed as { text?: unknown; size?: unknown }[]) {
1482
+ const runSize = typeof run.size === "number" ? run.size : size;
1483
+ (typeof run.text === "string" ? run.text : "").split("\n").forEach((piece, i) => {
1484
+ if (i > 0) {
1485
+ lines.push(size);
1486
+ }
1487
+ if (piece !== "") {
1488
+ lines[lines.length - 1] = Math.max(lines.at(-1) ?? size, runSize);
1489
+ }
1490
+ });
1491
+ }
1492
+ return lines.reduce((sum, h) => sum + h - size, 0) / 2;
1493
+ }
1494
+
1495
+ /**
1496
+ * A vis.js font modifier's options: an object, or text like "16px arial black" (size, face, color).
1497
+ * @param raw - `font.bold`, `font.ital`, `font.boldital` or `font.mono`
1498
+ * @returns the options
1499
+ */
1500
+ function visModOptions(raw: unknown): Record<string, unknown> {
1501
+ if (isJsonObject(raw)) {
1502
+ return raw;
1503
+ }
1504
+ if (typeof raw === "string") {
1505
+ const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(raw.slice(0, 256));
1506
+ if (m !== null) {
1507
+ return { size: Number(m[1]), face: m[2], ...(m[3] === undefined ? {} : { color: m[3] }) };
1508
+ }
1509
+ }
1510
+ return {};
1511
+ }
1512
+
1513
+ /** vis-network's markup systems by `font.multi`. */
1514
+ const VIS_MARKUP_SYSTEM: ReadonlyMap<unknown, "html" | "md"> = new Map<unknown, "html" | "md">([
1515
+ [true, "html"],
1516
+ ["html", "html"],
1517
+ ["markdown", "md"],
1518
+ ["md", "md"],
1519
+ ]);
1520
+
1521
+ /** Which font modifiers are on, part way through a label. */
1522
+ interface VisMarkState {
1523
+ bold: boolean;
1524
+ ital: boolean;
1525
+ mono: boolean;
1526
+ }
1527
+
1528
+ /** Each system's marks and the modifier each turns on or off. */
1529
+ const VIS_MARKS: Readonly<Record<"html" | "md", Readonly<Record<string, keyof VisMarkState>>>> = {
1530
+ html: { "<b>": "bold", "</b>": "bold", "<i>": "ital", "</i>": "ital", "<code>": "mono", "</code>": "mono" },
1531
+ md: { "*": "bold", _: "ital", "`": "mono" },
1532
+ };
1533
+
1534
+ /**
1535
+ * 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
1536
+ * closes one that is on; a markdown mark opens at the start of a word and closes a modifier that is on.
1537
+ * @param source - the label
1538
+ * @param i - the place
1539
+ * @param system - the markup system
1540
+ * @param state - the modifiers on, changed in place
1541
+ * @returns how many characters the mark takes, 0 for no mark
1542
+ */
1543
+ function visMarkTag(source: string, i: number, system: "html" | "md", state: VisMarkState): number {
1544
+ const marks = VIS_MARKS[system];
1545
+ const tag = Object.keys(marks).find((m) => source.startsWith(m, i));
1546
+ if (tag === undefined) {
1547
+ return 0;
1548
+ }
1549
+ const key = marks[tag];
1550
+ const toggles =
1551
+ system === "html" ? state[key] === tag.startsWith("</") : state[key] || i === 0 || /\s/.test(source[i - 1]);
1552
+ if (!toggles) {
1553
+ return 0;
1554
+ }
1555
+ state[key] = !state[key];
1556
+ return tag.length;
1557
+ }
1558
+
1559
+ /**
1560
+ * The character at a place in a label, an html entity (`&lt;`, `&amp;`) or a markdown escape read as the character it
1561
+ * stands for.
1562
+ * @param source - the label
1563
+ * @param i - the place
1564
+ * @param system - the markup system
1565
+ * @returns the character and the place after it
1566
+ */
1567
+ function visMarkChar(source: string, i: number, system: "html" | "md"): [string, number] {
1568
+ if (system === "md") {
1569
+ return source[i] === "\\" && i + 1 < source.length ? [source[i + 1], i + 2] : [source[i], i + 1];
1570
+ }
1571
+ const entity = [
1572
+ ["&lt;", "<"],
1573
+ ["&amp;", "&"],
1574
+ ].find(([e]) => source.startsWith(e, i));
1575
+ return entity === undefined ? [source[i], i + 1] : [entity[1], i + entity[0].length];
1576
+ }
1577
+
1578
+ /**
1579
+ * The font modifier the modifiers on make: mono over the others, bold and italic together boldital.
1580
+ * @param state - the modifiers on
1581
+ * @returns the modifier
1582
+ */
1583
+ function visMarkMod(state: VisMarkState): string {
1584
+ if (state.mono) {
1585
+ return "mono";
1586
+ }
1587
+ if (state.bold) {
1588
+ return state.ital ? "boldital" : "bold";
1589
+ }
1590
+ return state.ital ? "ital" : "normal";
854
1591
  }
855
1592
 
856
1593
  /** Sets a value of a row when it is not null. */
@@ -866,17 +1603,41 @@ type PutRow = (p: string, v: StyleValue | null) => void;
866
1603
  function visNodeValues(layer: VisLayer, sized: boolean, put: PutRow, site: (property: string) => CanonSite): void {
867
1604
  if (layer.shape !== undefined) {
868
1605
  put("shape", canonEnum("shape", layer.shape, "vis", site("shape")));
1606
+ // vis-network draws a label inside the shapes sized by their label and below the shapes sized by `size`
1607
+ if (typeof layer.shape === "string") {
1608
+ put("labelPosition", VIS_SIZED.has(layer.shape) ? "bottom" : "center");
1609
+ }
869
1610
  }
870
1611
  if (sized && layer.size !== undefined) {
1612
+ // vis-network draws `size` as a radius: a dot of size 25 is 50 across
871
1613
  const v = canonNumber(layer.size, site("width"));
872
- put("width", v);
873
- put("height", v);
1614
+ put("width", v === null ? null : 2 * v);
1615
+ put("height", v === null ? null : 2 * v);
874
1616
  }
875
1617
  if (layer.borderWidth !== undefined) {
876
1618
  put("borderWidth", canonNumber(layer.borderWidth, site("borderWidth")));
877
1619
  }
1620
+ visNodeOpacityAndDashes(layer, put, site);
1621
+ }
1622
+
1623
+ /**
1624
+ * A vis.js node's opacity and border dashes (visNodeValues).
1625
+ * @param layer - the layer
1626
+ * @param put - the row's setter
1627
+ * @param site - where its values are read
1628
+ */
1629
+ function visNodeOpacityAndDashes(layer: VisLayer, put: PutRow, site: (property: string) => CanonSite): void {
878
1630
  if (layer.opacity !== undefined) {
879
- put("opacity", canonOpacity(layer.opacity, "vis", site("opacity")));
1631
+ // vis-network's node opacity replaces the alpha of its border color and its background color alike, but only of
1632
+ // a hex or rgb() color: overrideOpacity leaves rgba() and any other text (a name) as it is (Node.getFormattingValues)
1633
+ const opacity = canonOpacity(layer.opacity, "vis", site("opacity"));
1634
+ put("opacity", visTakesOpacity(layer.fill) ? opacity : null);
1635
+ put("borderOpacity", visTakesOpacity(layer.border) ? opacity : null);
1636
+ }
1637
+ if (layer.borderDashes !== undefined) {
1638
+ // vis-network's default border dashes are [5, 15]
1639
+ const width = typeof layer.borderWidth === "number" ? layer.borderWidth : 1;
1640
+ put("borderStyle", visDashStyle(layer.borderDashes === true ? [5, 15] : layer.borderDashes, width, false));
880
1641
  }
881
1642
  }
882
1643
 
@@ -894,25 +1655,76 @@ function visEdgeValues(layer: VisLayer, put: PutRow, site: (property: string) =>
894
1655
  put("width", canonNumber(layer.width, site("width")));
895
1656
  }
896
1657
  if (layer.dashes !== undefined) {
897
- put("lineStyle", layer.dashes === false || layer.dashes === "false" ? "solid" : "dashed");
1658
+ // vis-network's default dashes are [5, 5]
1659
+ const width = typeof layer.width === "number" ? layer.width : 1;
1660
+ const dashes = layer.dashes === true ? [5, 5] : layer.dashes;
1661
+ // vis-network draws an edge with round caps (EdgeBase: lineCap "round"), each dash a width longer
1662
+ put("lineStyle", layer.dashes === "false" ? "solid" : visDashStyle(dashes, width, true));
898
1663
  }
899
1664
  if (layer.arrows !== undefined) {
900
- const [to, from] = visArrows(layer.arrows, site);
1665
+ const [to, from, middle] = visArrows(layer.arrows, site);
901
1666
  put("targetArrow", to);
902
1667
  put("sourceArrow", from);
1668
+ put("midTargetArrow", middle);
1669
+ // One neutral size for every end: the target's scaleFactor, else the source's, else the middle's.
1670
+ const scaled = (name: string): unknown => {
1671
+ const a = isJsonObject(layer.arrows) ? layer.arrows[name] : undefined;
1672
+ return isJsonObject(a) ? a.scaleFactor : undefined;
1673
+ };
1674
+ const scale = scaled("to") ?? scaled("from") ?? scaled("middle");
1675
+ if (scale !== undefined) {
1676
+ put("arrowSize", canonNumber(scale, site("arrowSize")));
1677
+ }
1678
+ visArrowImages(layer.arrows, put, site);
1679
+ }
1680
+ }
1681
+
1682
+ /**
1683
+ * The pictures of vis.js `image` arrows (`{ to: { type: "image", src, imageWidth, imageHeight } }`), each with the
1684
+ * size it is drawn at where the file gives one (vis-network 9.1.9 `Image.draw`: `imageWidth` across the line,
1685
+ * `imageHeight` along it, else the picture's own).
1686
+ * @param arrows - the `arrows` value
1687
+ * @param put - the row's setter
1688
+ * @param site - where its values are read
1689
+ */
1690
+ function visArrowImages(arrows: unknown, put: PutRow, site: (property: string) => CanonSite): void {
1691
+ if (!isJsonObject(arrows)) {
1692
+ return;
1693
+ }
1694
+ for (const [name, property] of [
1695
+ ["to", "targetArrow"],
1696
+ ["from", "sourceArrow"],
1697
+ ["middle", "midTargetArrow"],
1698
+ ] as const) {
1699
+ const a = arrows[name];
1700
+ if (!isJsonObject(a) || a.type !== "image" || typeof a.src !== "string" || a.src === "") {
1701
+ continue;
1702
+ }
1703
+ put(`${property}Image`, a.src);
1704
+ if (a.imageWidth !== undefined) {
1705
+ put(`${property}ImageWidth`, canonNumber(a.imageWidth, site(`${property}ImageWidth`)));
1706
+ }
1707
+ if (a.imageHeight !== undefined) {
1708
+ put(`${property}ImageHeight`, canonNumber(a.imageHeight, site(`${property}ImageHeight`)));
1709
+ }
903
1710
  }
904
1711
  }
905
1712
 
906
1713
  /**
907
- * vis.js `arrows` as the target and source arrows: `"to"`, `"to, from"`, or `{ to: true | { enabled, type } }`.
1714
+ * vis.js `arrows` as the target, source and middle arrows: `"to"`, `"to, from"`, `"middle"`, or
1715
+ * `{ to: true | { enabled, type } }`. The middle one points toward the target.
908
1716
  * @param arrows - the value
909
1717
  * @param site - where it is read
910
- * @returns the target arrow and the source arrow
1718
+ * @returns the target arrow, the source arrow and the middle arrow
911
1719
  */
912
- function visArrows(arrows: unknown, site: (property: string) => CanonSite): [string | null, string | null] {
913
- const end = (name: string, property: string): string | null => {
1720
+ function visArrows(
1721
+ arrows: unknown,
1722
+ site: (property: string) => CanonSite,
1723
+ ): [string | null, string | null, string | null] {
1724
+ // a middle arrow the file does not mention is no rule at all, so a file that names only its ends carries none
1725
+ const end = (name: string, property: string, absent: "none" | null = "none"): string | null => {
914
1726
  if (typeof arrows === "string") {
915
- return new RegExp(String.raw`\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : "none";
1727
+ return new RegExp(String.raw`\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : absent;
916
1728
  }
917
1729
  if (!isJsonObject(arrows)) {
918
1730
  return null;
@@ -921,12 +1733,15 @@ function visArrows(arrows: unknown, site: (property: string) => CanonSite): [str
921
1733
  if (a === true) {
922
1734
  return "triangle";
923
1735
  }
1736
+ if (a === undefined) {
1737
+ return absent;
1738
+ }
924
1739
  if (!isJsonObject(a) || a.enabled === false) {
925
1740
  return "none";
926
1741
  }
927
1742
  return canonEnum("arrow", a.type ?? "arrow", "vis", site(property));
928
1743
  };
929
- return [end("to", "targetArrow"), end("from", "sourceArrow")];
1744
+ return [end("to", "targetArrow"), end("from", "sourceArrow"), end("middle", "midTargetArrow", null)];
930
1745
  }
931
1746
 
932
1747
  /**
@@ -959,68 +1774,444 @@ function visStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: Style
959
1774
  const { groupLayers, paletteRows } = visGroupLayers(n, groupOf, visGroupDefs(src, options));
960
1775
  context.budget.spend("node", n);
961
1776
 
962
- // what vis-network draws per node: its own keys over its group over options.nodes
963
- const node = valuesOf(n);
964
- const records = nodeRecords(snapshot);
965
- const rawShape = (layer: VisLayer): string | null => (typeof layer.shape === "string" ? layer.shape : null);
1777
+ // what vis-network draws per node: its own keys over its group over options.nodes
1778
+ const node = valuesOf(n);
1779
+ const shapes: string[] = [];
1780
+ const rawRecords = elementRaw(snapshot, "node");
1781
+ const range = visNodeValueRange(n, groupOf, rawRecords, groupLayers, nodeDefaults);
1782
+ const ownGroup = visOwnGroup(groupLayers, visGroupDefs(src, options));
1783
+ for (let i = 0; i < n; i++) {
1784
+ const g = groupOf(i);
1785
+ const raw = rawRecords(i);
1786
+ const own = visLayer(raw, "node");
1787
+ const group = g === null ? {} : ownGroup(g, raw);
1788
+ const shape = visShapeOf(own) ?? visShapeOf(group) ?? visShapeOf(nodeDefaults) ?? "ellipse";
1789
+ shapes.push(shape);
1790
+ const site = (property: string): CanonSite => ({
1791
+ notes,
1792
+ target: "node",
1793
+ property,
1794
+ element: i,
1795
+ budget: context.budget,
1796
+ });
1797
+ const merged = visMergedNode(nodeDefaults, group, own, g !== null && raw.color !== undefined);
1798
+ for (const [p, v] of Object.entries(visNodeRow(merged, shape, range, site))) {
1799
+ node.put(p, i, v);
1800
+ }
1801
+ }
1802
+ const declaredNode = visValues(nodeDefaults, "node", ruleSite("node"), true);
1803
+ if (Object.keys(declaredNode).length > 0) {
1804
+ nodeRules.push(rule("node.vis.defaults", "node", "defaults", "declared", { all: true }, declaredNode));
1805
+ }
1806
+ visGroupRules(n, groupOf, groupLayers, paletteRows, ruleSite("node"), nodeRules, nodeColumns);
1807
+
1808
+ const toolNode = toolDefaults("vis-network")?.node;
1809
+ const borderOf = (i: number): StyleValue =>
1810
+ node.values.borderColor?.[i] ?? (toolNode?.borderColor?.value as StyleValue | undefined) ?? "#2b7ce9";
1811
+ const geometry = visGeometry(snapshot, node.values, shapes);
1812
+ const edge = visEdges(snapshot, visLayer(options.edges, "edge"), borderOf, geometry, notes, context);
1813
+ const values = { node: node.values, edge: edge.values };
1814
+ exactFromBlock(snapshot, src.block, values, visWritten(snapshot, values.edge));
1815
+ return {
1816
+ canonical: true,
1817
+ dialect: "vis",
1818
+ tool: "vis-network",
1819
+ opacityInColor: true,
1820
+ units: "px",
1821
+ node: { values: values.node, rules: nodeRules, columns: nodeColumns },
1822
+ edge: { values: values.edge, rules: edge.rules, columns: edge.columns },
1823
+ notes: notes.list(),
1824
+ block: src.block,
1825
+ readFrom: {
1826
+ node: VIS_KEYS.node.filter((x) => snapshot.nodes.has(x)),
1827
+ edge: VIS_KEYS.edge.filter((x) => snapshot.edges.has(x)),
1828
+ },
1829
+ };
1830
+ }
1831
+
1832
+ /**
1833
+ * vis.js: a layer's own shape.
1834
+ * @param layer - the layer
1835
+ * @returns its `shape`, or null when it gives none
1836
+ */
1837
+ function visShapeOf(layer: VisLayer): string | null {
1838
+ return typeof layer.shape === "string" ? layer.shape : null;
1839
+ }
1840
+
1841
+ /**
1842
+ * vis.js: the smallest and largest `value` of the nodes, each its own over its group's over `options.nodes`.
1843
+ * @param n - the node count
1844
+ * @param groupOf - each node's group
1845
+ * @param rawRecords - each node's own keys
1846
+ * @param groupLayers - each group's layer (visGroupLayers)
1847
+ * @param nodeDefaults - `options.nodes`
1848
+ * @returns the range, or null when no node has a value
1849
+ */
1850
+ function visNodeValueRange(
1851
+ n: number,
1852
+ groupOf: (i: number) => string | null,
1853
+ rawRecords: (i: number) => Record<string, unknown>,
1854
+ groupLayers: ReadonlyMap<string, VisLayer>,
1855
+ nodeDefaults: VisLayer,
1856
+ ): readonly [number, number] | null {
1857
+ return visValueRange(n, (i) => {
1858
+ const g = groupOf(i);
1859
+ const own = visLayer(rawRecords(i), "node").value;
1860
+ const value = own ?? (g === null ? undefined : groupLayers.get(g)?.value) ?? nodeDefaults.value;
1861
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1862
+ });
1863
+ }
1864
+
1865
+ /**
1866
+ * vis.js: one node's layer, its own keys over its group's over `options.nodes`.
1867
+ * @param nodeDefaults - `options.nodes`
1868
+ * @param group - its group's layer
1869
+ * @param own - its own layer
1870
+ * @param ownColorInGroup - whether it is in a group and gives a color of its own
1871
+ * @returns the merged layer
1872
+ */
1873
+ function visMergedNode(nodeDefaults: VisLayer, group: VisLayer, own: VisLayer, ownColorInGroup: boolean): VisLayer {
1874
+ const merged: VisLayer = { ...nodeDefaults, ...group, ...own };
1875
+ if (ownColorInGroup) {
1876
+ // a color of the node's own fills its gaps from options.nodes and vis-network's defaults, never its group's
1877
+ merged.fill ??= VIS_NODE_FILL;
1878
+ merged.border ??= VIS_NODE_BORDER;
1879
+ }
1880
+ return merged;
1881
+ }
1882
+
1883
+ /**
1884
+ * vis.js: the values one node draws, from its merged layer: its colors and label, then its shape's own.
1885
+ * @param merged - the node's merged layer
1886
+ * @param shape - its vis-network shape
1887
+ * @param range - the smallest and largest `value` of the graph's nodes (visValueRange)
1888
+ * @param site - where its values are read
1889
+ * @returns the values
1890
+ */
1891
+ function visNodeRow(
1892
+ merged: VisLayer,
1893
+ shape: string,
1894
+ range: readonly [number, number] | null,
1895
+ site: (property: string) => CanonSite,
1896
+ ): Row {
1897
+ const sized = VIS_SIZED.has(shape);
1898
+ const row = visValues(merged, "node", site, sized);
1899
+ const shaped = sized
1900
+ ? visSizedShape(merged, shape, visRadius(merged, range, site), site)
1901
+ : visFitShape(merged, shape);
1902
+ if (sized && merged.label !== undefined) {
1903
+ const size =
1904
+ visLabelScaled(merged, range) ?? (typeof merged.fontSize === "number" ? merged.fontSize : VIS_FONT_SIZE);
1905
+ // a label in runs below the node hangs lower by half of what its larger runs make its lines taller
1906
+ const drop = typeof row.labelRuns === "string" ? visRunsDrop(row.labelRuns, size) : 0;
1907
+ const below = typeof row.labelOffsetY === "number" ? row.labelOffsetY : 0;
1908
+ row.labelOffsetY = below + drop + visBelowGap(size);
1909
+ }
1910
+ // the shape's own label place, then font.vadjust below it
1911
+ for (const p of ["labelOffsetX", "labelOffsetY"] as const) {
1912
+ const [a, b] = [row[p], shaped[p]];
1913
+ if (typeof a === "number" && typeof b === "number") {
1914
+ shaped[p] = a + b;
1915
+ }
1916
+ }
1917
+ Object.assign(row, shaped);
1918
+ const labelSize = visLabelScaled(merged, range);
1919
+ if (labelSize !== null) {
1920
+ row.labelSize = labelSize;
1921
+ }
1922
+ return row;
1923
+ }
1924
+
1925
+ /**
1926
+ * vis.js: one rule per node group with values, over a column marking the group's nodes; a group the file does not
1927
+ * define is the tool's palette.
1928
+ * @param n - the node count
1929
+ * @param groupOf - each node's group
1930
+ * @param groupLayers - each group's layer (visGroupLayers)
1931
+ * @param paletteRows - the palette values of the groups the file does not define
1932
+ * @param site - where a group's values are read
1933
+ * @param rules - the node rules, appended to
1934
+ * @param columns - the node columns, written to
1935
+ */
1936
+ function visGroupRules(
1937
+ n: number,
1938
+ groupOf: (i: number) => string | null,
1939
+ groupLayers: ReadonlyMap<string, VisLayer>,
1940
+ paletteRows: ReadonlyMap<string, Row>,
1941
+ site: (property: string) => CanonSite,
1942
+ rules: StyleRule[],
1943
+ columns: Map<string, StyleColumn>,
1944
+ ): void {
1945
+ let k = 0;
1946
+ for (const [g, layer] of groupLayers) {
1947
+ const set = paletteRows.get(g) ?? visValues(layer, "node", site, true);
1948
+ if (Object.keys(set).length === 0) {
1949
+ continue;
1950
+ }
1951
+ const key = `group.${String(k)}`;
1952
+ columns.set(
1953
+ key,
1954
+ Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)),
1955
+ );
1956
+ const origin = paletteRows.has(g) ? "tool" : "declared";
1957
+ const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1958
+ rules.push({ ...r, facts: { names: [[key, g]] } });
1959
+ }
1960
+ }
1961
+
1962
+ /**
1963
+ * vis.js: what a node of a shape sized by `size` draws besides its colors: its box from its radius, its picture or
1964
+ * glyph, and triangleDown turned over.
1965
+ * @param merged - the node's merged layer
1966
+ * @param shape - its vis-network shape
1967
+ * @param radius - its radius (visRadius)
1968
+ * @param site - where its values are read
1969
+ * @returns the values
1970
+ */
1971
+ function visSizedShape(merged: VisLayer, shape: string, radius: number, site: (property: string) => CanonSite): Row {
1972
+ // vis-network's hexagon is regular, its corners `size` from its center on the left and right (drawHexagon): it is
1973
+ // 2 * size wide and sqrt(3) * size tall, where the neutral hexagon fills its box
1974
+ const row: Row = { width: 2 * radius, height: 2 * radius };
1975
+ if (shape === "hexagon") {
1976
+ // its label still hangs below the 2 * size square (ShapeBase._drawShape: y + 0.5 * height + half the label)
1977
+ row.height = Math.sqrt(3) * radius;
1978
+ row.labelOffsetY = ((2 - Math.sqrt(3)) * radius) / 2;
1979
+ }
1980
+ Object.assign(row, visImage(merged, shape, { width: 2 * radius, height: 2 * radius }));
1981
+ if (shape === "icon") {
1982
+ // an icon node is its glyph alone: vis-network draws no body, no fill and no border for it (Icon.draw)
1983
+ Object.assign(row, { shape: "none" }, visIcon(merged, site("glyphColor")));
1984
+ }
1985
+ // vis-network's triangleDown: its triangle turned over, a corner down
1986
+ if (shape === "triangleDown") {
1987
+ row.shape = "polygon";
1988
+ row.shapePoints = "-1,1 0,-1 1,1";
1989
+ }
1990
+ return row;
1991
+ }
1992
+
1993
+ /**
1994
+ * vis.js: what a node of a shape sized by its label draws besides its colors: its growth round its label, nothing at
1995
+ * all with a margin of 0, and its label at the top or bottom under `heightConstraint.valign`.
1996
+ * @param merged - the node's merged layer
1997
+ * @param shape - its vis-network shape
1998
+ * @returns the values
1999
+ */
2000
+ function visFitShape(merged: VisLayer, shape: string): Row {
2001
+ const row = visLabelFit(merged, shape);
2002
+ // a margin of 0 leaves vis-network's margins undefined (`_setMargins` tests `if (options.margin)`), so the node's
2003
+ // size is NaN and neither it nor its label is drawn
2004
+ if (merged.margin === 0 && (shape === "box" || shape === "circle" || shape === "database")) {
2005
+ row.visible = false;
2006
+ }
2007
+ // heightConstraint.valign moves the label to the top or the bottom of a node taller than it (Label._drawText)
2008
+ if (merged.valign === "top" || merged.valign === "bottom") {
2009
+ row.labelPosition = `inside-${merged.valign}`;
2010
+ } else if (shape === "database") {
2011
+ // a database draws its label at its top, its top margin down (Database.draw: this.top + margin.top), not in
2012
+ // the middle of the square it grows to
2013
+ const m = merged.margin;
2014
+ const top = isJsonObject(m) ? m.top : m;
2015
+ row.labelPosition = "inside-top";
2016
+ row.labelOffsetY = typeof top === "number" && Number.isFinite(top) && top >= 0 ? top : VIS_MARGIN;
2017
+ }
2018
+ // a box's corners are rounded at `shapeProperties.borderRadius`, at most half its shorter side (drawRoundRect)
2019
+ if (shape === "box" && typeof merged.borderRadius === "number" && Number.isFinite(merged.borderRadius)) {
2020
+ row.cornerRadius = Math.max(0, merged.borderRadius);
2021
+ }
2022
+ return row;
2023
+ }
2024
+
2025
+ /**
2026
+ * 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
2027
+ * least `widthConstraint.minimum` by `heightConstraint.minimum`, plus the `margin` on each side is the node's width
2028
+ * and height; a circle and a database stay as wide as they are tall; an ellipse is the box's height taller and
2029
+ * wider instead of the margin (`width + height` by twice the height).
2030
+ * @param layer - the node's merged layer
2031
+ * @param shape - its vis-network shape
2032
+ * @returns the values, none for a shape not sized by its label
2033
+ */
2034
+ function visLabelFit(layer: VisLayer, shape: string): Row {
2035
+ const fit = VIS_LABEL_FIT.get(shape);
2036
+ if (fit === undefined) {
2037
+ return {};
2038
+ }
2039
+ const length = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
2040
+ const fontSize = length(layer.fontSize) ?? VIS_FONT_SIZE;
2041
+ const lines = typeof layer.label === "string" ? Math.max(1, layer.label.split("\n").length) : 1;
2042
+ const minW = length(layer.widthMin) ?? 0;
2043
+ const minH = length(layer.heightMin) ?? 0;
2044
+ let padX: number;
2045
+ let padY: number;
2046
+ let offset: Row = {};
2047
+ if (fit.ellipse) {
2048
+ padX = Math.max(lines * fontSize, minH) / 2;
2049
+ padY = padX;
2050
+ } else {
2051
+ ({ padX, padY, offset } = visMarginPads(layer.margin, fit.square));
2052
+ }
2053
+ // a node with no label text is sized as if its label were 14 x 14 (NodeBase.getDimensionsFromLabel)
2054
+ const empty = typeof layer.label !== "string" || layer.label === "";
2055
+ const [boxW, boxH] = empty ? [Math.max(minW, 14), Math.max(minH, 14)] : [minW, minH];
2056
+ if (empty && fit.ellipse) {
2057
+ padX = boxH / 2;
2058
+ padY = padX;
2059
+ }
2060
+ const row: Row = {
2061
+ ...offset,
2062
+ labelFit: "bounds",
2063
+ labelPaddingX: padX,
2064
+ labelPaddingY: padY,
2065
+ width: boxW > 0 ? boxW + 2 * padX : 0,
2066
+ height: boxH > 0 ? boxH + 2 * padY : 0,
2067
+ };
2068
+ if (fit.square) {
2069
+ row.labelFitSquare = true;
2070
+ }
2071
+ return row;
2072
+ }
2073
+
2074
+ /**
2075
+ * vis.js: the padding a node's `margin` puts round its label (visLabelFit). A box or text node draws its label its
2076
+ * left and top margins in from its corner (Box.draw, Text.draw), off center by half the difference of opposite
2077
+ * margins.
2078
+ * @param m - the `margin`: one number for every side, or `{ top, right, bottom, left }`
2079
+ * @param square - whether the shape stays as wide as it is tall
2080
+ * @returns the horizontal and vertical padding, and the label's offset
2081
+ */
2082
+ function visMarginPads(m: unknown, square: boolean): { padX: number; padY: number; offset: Row } {
2083
+ const length = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);
2084
+ const side = (key: string): number => (isJsonObject(m) ? length(m[key]) : length(m)) ?? VIS_MARGIN;
2085
+ const offset: Row = {};
2086
+ if (!square && side("left") !== side("right")) {
2087
+ offset.labelOffsetX = (side("left") - side("right")) / 2;
2088
+ }
2089
+ if (!square && side("top") !== side("bottom")) {
2090
+ offset.labelOffsetY = (side("top") - side("bottom")) / 2;
2091
+ }
2092
+ return { padX: (side("left") + side("right")) / 2, padY: (side("top") + side("bottom")) / 2, offset };
2093
+ }
2094
+
2095
+ /**
2096
+ * vis.js `scaling.label`: with `enabled`, the font size of an element with a `value`, mapped as its size is from the
2097
+ * value range onto `scaling.label.min` .. `max` (14 .. 30) (vis-network 9.1.9 `Node.setValueRange`, `Edge.setValueRange`).
2098
+ * @param layer - the element's merged layer
2099
+ * @param range - the value range, or null
2100
+ * @returns the font size, or null when the label is not scaled
2101
+ */
2102
+ function visLabelScaled(layer: VisLayer, range: readonly [number, number] | null): number | null {
2103
+ const value = finiteValue(layer.value);
2104
+ const label = layer.scalingLabel;
2105
+ if (value === null || range === null || !isJsonObject(label) || label.enabled !== true) {
2106
+ return null;
2107
+ }
2108
+ const min = typeof label.min === "number" ? label.min : 14;
2109
+ const max = typeof label.max === "number" ? label.max : 30;
2110
+ return visScaled(value, range, min, max);
2111
+ }
2112
+
2113
+ /** vis-network's default node size, a radius (`size: 25`, vis-network.esm.js NodesHandler defaults). */
2114
+ const VIS_SIZE = 25;
2115
+ /** vis-network's default `scaling.min` and `scaling.max`, the radii `value` is mapped onto. */
2116
+ const VIS_SCALING: readonly [number, number] = [10, 30];
2117
+
2118
+ /**
2119
+ * vis.js: the smallest and largest `value` of the nodes that have one, as vis-network's
2120
+ * `_updateValueRange` takes them; null when no node has one.
2121
+ * @param n - the node count
2122
+ * @param valueOf - each node's value
2123
+ * @returns the range
2124
+ */
2125
+ function visValueRange(n: number, valueOf: (i: number) => number | null): readonly [number, number] | null {
2126
+ let min = Infinity;
2127
+ let max = -Infinity;
966
2128
  for (let i = 0; i < n; i++) {
967
- const g = groupOf(i);
968
- const own = records(i);
969
- const group = g === null ? {} : (groupLayers.get(g) ?? {});
970
- const shape = rawShape(own) ?? rawShape(group) ?? rawShape(nodeDefaults) ?? "ellipse";
971
- const site = (property: string): CanonSite => ({
972
- notes,
973
- target: "node",
974
- property,
975
- element: i,
976
- budget: context.budget,
977
- });
978
- const merged = { ...nodeDefaults, ...group, ...own };
979
- for (const [p, v] of Object.entries(visValues(merged, "node", site, VIS_SIZED.has(shape)))) {
980
- node.put(p, i, v);
2129
+ const v = valueOf(i);
2130
+ if (v !== null) {
2131
+ min = Math.min(min, v);
2132
+ max = Math.max(max, v);
981
2133
  }
982
2134
  }
983
- const declaredNode = visValues(nodeDefaults, "node", ruleSite("node"), true);
984
- if (Object.keys(declaredNode).length > 0) {
985
- nodeRules.push(rule("node.vis.defaults", "node", "defaults", "declared", { all: true }, declaredNode));
2135
+ return min <= max ? [min, max] : null;
2136
+ }
2137
+
2138
+ /**
2139
+ * A `value` that is a finite number, else null.
2140
+ * @param value - the value
2141
+ * @returns the number, or null
2142
+ */
2143
+ const finiteValue = (value: unknown): number | null =>
2144
+ typeof value === "number" && Number.isFinite(value) ? value : null;
2145
+
2146
+ /**
2147
+ * vis.js: `value` mapped linearly from the value range onto `min` .. `max`, the middle when every value is the same
2148
+ * (vis-network's default `customScalingFunction`).
2149
+ * @param value - the element's value
2150
+ * @param range - the value range
2151
+ * @param min - `scaling.min`
2152
+ * @param max - `scaling.max`
2153
+ * @returns the scaled value
2154
+ */
2155
+ function visScaled(value: number, range: readonly [number, number], min: number, max: number): number {
2156
+ const [lo, hi] = range;
2157
+ const scale = hi === lo ? 0.5 : Math.max(0, (value - lo) / (hi - lo));
2158
+ return min + scale * (max - min);
2159
+ }
2160
+
2161
+ /** vis-network's default edge `scaling.min` and `scaling.max`, the widths `value` is mapped onto. */
2162
+ const VIS_EDGE_SCALING: readonly [number, number] = [1, 15];
2163
+
2164
+ /**
2165
+ * vis.js: the width vis-network draws an edge at once any edge has a `value`. Its `_updateValueRange` then sets every
2166
+ * edge's width: one with a value to the value mapped onto `scaling`, one without to the width it was created with,
2167
+ * which is `options.edges.width` -- an edge's own `width` is dropped (`Edge.baseWidth` is read before the edge's own
2168
+ * options are applied, and nothing updates it).
2169
+ * @param layer - the edge's merged layer
2170
+ * @param defaults - `options.edges`
2171
+ * @param range - the value range over the edges, or null when no edge has a value
2172
+ * @param site - where its values are read
2173
+ * @returns the width, or nothing when no edge has a value (the edge's own width holds)
2174
+ */
2175
+ function visEdgeWidth(
2176
+ layer: VisLayer,
2177
+ defaults: VisLayer,
2178
+ range: readonly [number, number] | null,
2179
+ site: (property: string) => CanonSite,
2180
+ ): { width?: number } {
2181
+ if (range === null) {
2182
+ return {};
986
2183
  }
987
- let k = 0;
988
- for (const [g, layer] of groupLayers) {
989
- const set = paletteRows.get(g) ?? visValues(layer, "node", ruleSite("node"), true);
990
- if (Object.keys(set).length === 0) {
991
- continue;
992
- }
993
- const key = `group.${String(k)}`;
994
- nodeColumns.set(
995
- key,
996
- Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)),
997
- );
998
- const origin = paletteRows.has(g) ? "tool" : "declared";
999
- const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1000
- nodeRules.push({ ...r, facts: { names: [[key, g]] } });
2184
+ const value = finiteValue(layer.value);
2185
+ if (value !== null) {
2186
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_EDGE_SCALING[0];
2187
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_EDGE_SCALING[1];
2188
+ return { width: visScaled(value, range, min, max) };
1001
2189
  }
2190
+ return { width: (defaults.width === undefined ? null : canonNumber(defaults.width, site("width"))) ?? 1 };
2191
+ }
1002
2192
 
1003
- const toolNode = toolDefaults("vis-network")?.node;
1004
- const borderOf = (i: number): StyleValue =>
1005
- node.values.borderColor?.[i] ?? (toolNode?.borderColor?.value as StyleValue | undefined) ?? "#2b7ce9";
1006
- const edge = visEdges(snapshot, visLayer(options.edges, "edge"), borderOf, notes, context);
1007
- const values = { node: node.values, edge: edge.values };
1008
- exactFromBlock(snapshot, src.block, values, VIS_WRITTEN);
1009
- return {
1010
- canonical: true,
1011
- dialect: "vis",
1012
- tool: "vis-network",
1013
- opacityInColor: true,
1014
- units: "px",
1015
- node: { values: values.node, rules: nodeRules, columns: nodeColumns },
1016
- edge: { values: values.edge, rules: edge.rules, columns: edge.columns },
1017
- notes: notes.list(),
1018
- block: src.block,
1019
- readFrom: {
1020
- node: VIS_KEYS.node.filter((x) => snapshot.nodes.has(x)),
1021
- edge: VIS_KEYS.edge.filter((x) => snapshot.edges.has(x)),
1022
- },
1023
- };
2193
+ /**
2194
+ * vis.js: the radius vis-network draws a node of a sized shape at: its `value` mapped linearly from the value range
2195
+ * onto `scaling.min` .. `scaling.max` (the middle when every value is the same), else its `size`, else 25
2196
+ * (vis-network's `setValueRange` and `customScalingFunction`).
2197
+ * @param layer - the node's merged layer
2198
+ * @param range - the value range over the nodes
2199
+ * @param site - where its values are read
2200
+ * @returns the radius
2201
+ */
2202
+ function visRadius(
2203
+ layer: VisLayer,
2204
+ range: readonly [number, number] | null,
2205
+ site: (property: string) => CanonSite,
2206
+ ): number {
2207
+ const value = finiteValue(layer.value);
2208
+ if (value !== null && range !== null) {
2209
+ const min = typeof layer.scalingMin === "number" ? layer.scalingMin : VIS_SCALING[0];
2210
+ const max = typeof layer.scalingMax === "number" ? layer.scalingMax : VIS_SCALING[1];
2211
+ return visScaled(value, range, min, max);
2212
+ }
2213
+ const size = layer.size === undefined ? null : canonNumber(layer.size, site("width"));
2214
+ return size ?? VIS_SIZE;
1024
2215
  }
1025
2216
 
1026
2217
  /**
@@ -1068,12 +2259,49 @@ function visGroupLayers(
1068
2259
  return { groupLayers, paletteRows };
1069
2260
  }
1070
2261
 
2262
+ /**
2263
+ * vis.js: the group layer one node takes. vis-network merges a group's options into a node only for the keys the node
2264
+ * does not set itself, each key whole (vis-network 9.1.9 `Node.updateGroupOptions`: every option of the node's own is
2265
+ * skipped, `font` always), so a node with a `color` of its own takes none of its group's colors, not even a border it
2266
+ * leaves out.
2267
+ * @param groupLayers - each group's whole layer (visGroupLayers)
2268
+ * @param groupDefs - the group definitions the file gives
2269
+ * @returns the layer of a group for a node of the given own keys
2270
+ */
2271
+ function visOwnGroup(
2272
+ groupLayers: ReadonlyMap<string, VisLayer>,
2273
+ groupDefs: Record<string, unknown>,
2274
+ ): (g: string, own: Record<string, unknown>) => VisLayer {
2275
+ const cache = new Map<string, VisLayer>();
2276
+ return (g, own) => {
2277
+ const whole = groupLayers.get(g) ?? {};
2278
+ const def = Object.hasOwn(groupDefs, g) ? groupDefs[g] : null;
2279
+ // a group the file does not define is vis-network's palette color, which a color of the node's own replaces
2280
+ const skip = Object.keys(own).filter(
2281
+ (k) => k !== "font" && (isJsonObject(def) ? Object.hasOwn(def, k) : k === "color"),
2282
+ );
2283
+ if (skip.length === 0) {
2284
+ return whole;
2285
+ }
2286
+ const key = `${g}\u0000${skip.join("\u0000")}`;
2287
+ let layer = cache.get(key);
2288
+ if (layer === undefined) {
2289
+ layer = isJsonObject(def)
2290
+ ? visLayer(Object.fromEntries(Object.entries(def).filter(([k]) => !skip.includes(k))), "node")
2291
+ : {};
2292
+ cache.set(key, layer);
2293
+ }
2294
+ return layer;
2295
+ };
2296
+ }
2297
+
1071
2298
  /**
1072
2299
  * vis.js edges: their own keys over `options.edges`; an edge without a color of its own takes its source node's
1073
2300
  * border color (its target's under `inherit: "to"`).
1074
2301
  * @param snapshot - the graph
1075
2302
  * @param edgeDefaults - `options.edges`
1076
2303
  * @param borderOf - each node's drawn border color
2304
+ * @param geometry - the nodes' positions and boxes, for the edges' routes
1077
2305
  * @param notes - where problems are noted
1078
2306
  * @param context - the work budget
1079
2307
  * @returns the edge values, rules and style columns
@@ -1082,6 +2310,7 @@ function visEdges(
1082
2310
  snapshot: GraphSnapshot,
1083
2311
  edgeDefaults: VisLayer,
1084
2312
  borderOf: (node: number) => StyleValue,
2313
+ geometry: VisGeometry,
1085
2314
  notes: StyleNotes,
1086
2315
  context: StyleReadContext,
1087
2316
  ): { values: Values; rules: StyleRule[]; columns: Map<string, StyleColumn> } {
@@ -1090,9 +2319,17 @@ function visEdges(
1090
2319
  const edgeRecords = elementRecords(snapshot, "edge");
1091
2320
  const inherited: (StyleValue | null)[] = new Array<StyleValue | null>(m).fill(null);
1092
2321
  let inheriting = 0;
2322
+ const range = visValueRange(m, (e) => finiteValue({ ...edgeDefaults, ...edgeRecords(e) }.value));
2323
+ // vis-network sets round caps to draw a dashed line (EdgeBase._drawDashedLine) and never sets them back, so every
2324
+ // band drawn with or after a dashed edge, in the file's order, has round caps, which close gaps narrower than the
2325
+ // band
2326
+ let roundCaps = false;
1093
2327
  for (let e = 0; e < m; e++) {
1094
2328
  const own = edgeRecords(e);
1095
- const merged = { ...edgeDefaults, ...own };
2329
+ // an array of `options.edges.dashes` reaches the edge through bridgeObject as a plain object, no longer an
2330
+ // array, so vis-network draws its default [5, 5] (EdgeBase.setStrokeDashed)
2331
+ const bridged = own.dashes === undefined && Array.isArray(edgeDefaults.dashes) ? { dashes: true } : {};
2332
+ const merged = { ...edgeDefaults, ...own, ...bridged };
1096
2333
  const site = (property: string): CanonSite => ({
1097
2334
  notes,
1098
2335
  target: "edge",
@@ -1100,24 +2337,83 @@ function visEdges(
1100
2337
  element: e,
1101
2338
  budget: context.budget,
1102
2339
  });
1103
- const vals = visValues(merged, "edge", site, true);
2340
+ const [s, t] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
2341
+ const vals: Row = {
2342
+ ...visValues(merged, "edge", site, true),
2343
+ ...visEdgeWidth(merged, edgeDefaults, range, site),
2344
+ ...visRoute(edgeDefaults, own, s, t, geometry),
2345
+ ...visEdgeLabel(merged, range, geometry.at(s), geometry.at(t)),
2346
+ };
2347
+ // a dashed edge sets them before it draws its own band (BezierEdgeBase._bezierCurve draws the band first)
2348
+ roundCaps ||= merged.hidden !== true && ![undefined, false, "false"].includes(merged.dashes as never);
2349
+ const band = roundCaps && isJsonObject(merged.background) ? visBandStyle(merged.background, true) : null;
2350
+ if (band !== null) {
2351
+ vals.outlineStyle = band;
2352
+ }
2353
+ const from = visEdgeInherit(vals, own.inherit ?? edgeDefaults.inherit ?? "from", borderOf, s, t);
1104
2354
  for (const [p, v] of Object.entries(vals)) {
1105
2355
  edge.put(p, e, v);
1106
2356
  }
1107
- const inherit = own.inherit ?? edgeDefaults.inherit ?? "from";
1108
- if (inherit === false || inherit === "false") {
1109
- if (vals.color === undefined) {
1110
- edge.put("color", e, VIS_EDGE_COLOR);
1111
- }
1112
- continue;
1113
- }
1114
- if (inherit === "both") {
1115
- notes.add(STYLE_ISSUE.GRADIENT, "edge", "color", { sample: "inherit both", element: e });
2357
+ if (from !== null) {
2358
+ inherited[e] = from;
2359
+ inheriting++;
1116
2360
  }
1117
- inherited[e] = borderOf(inherit === "to" ? snapshot.edgeTarget(e) : snapshot.edgeSource(e));
1118
- inheriting++;
1119
2361
  }
1120
2362
  context.budget.spend("edge", m);
2363
+ const { rules, columns } = visEdgeRules(edgeDefaults, notes, inherited, inheriting);
2364
+ return { values: edge.values, rules, columns };
2365
+ }
2366
+
2367
+ /**
2368
+ * vis.js: an edge's `inherit`, applied to its values: an edge that inherits draws its node's border color, not a
2369
+ * color `options.edges` gives (that only stops inheriting there), and under "both" a gradient between its nodes'; one
2370
+ * that does not inherit and has no color draws vis-network's default.
2371
+ * @param vals - the edge's values, written in place
2372
+ * @param inherit - its `inherit`
2373
+ * @param borderOf - each node's drawn border color
2374
+ * @param s - its source node
2375
+ * @param t - its target node
2376
+ * @returns the color it inherits, or null for none
2377
+ */
2378
+ function visEdgeInherit(
2379
+ vals: Row,
2380
+ inherit: unknown,
2381
+ borderOf: (node: number) => StyleValue,
2382
+ s: number,
2383
+ t: number,
2384
+ ): StyleValue | null {
2385
+ const inherits = inherit !== false && inherit !== "false";
2386
+ if (inherits) {
2387
+ delete vals.color;
2388
+ }
2389
+ if (inherit === "both") {
2390
+ // vis-network: a linear gradient from the source node's border color to the target's
2391
+ vals.gradientKind = "linear";
2392
+ vals.gradientColors = `${String(borderOf(s))} ${String(borderOf(t))}`;
2393
+ }
2394
+ if (!inherits) {
2395
+ if (vals.color === undefined) {
2396
+ vals.color = VIS_EDGE_COLOR;
2397
+ }
2398
+ return null;
2399
+ }
2400
+ return borderOf(inherit === "to" ? t : s);
2401
+ }
2402
+
2403
+ /**
2404
+ * vis.js: the edge rules: `options.edges` as declared, and the colors edges inherit from their nodes.
2405
+ * @param edgeDefaults - `options.edges`
2406
+ * @param notes - where problems are noted
2407
+ * @param inherited - each edge's inherited color, or null
2408
+ * @param inheriting - how many edges inherit one
2409
+ * @returns the rules and their style columns
2410
+ */
2411
+ function visEdgeRules(
2412
+ edgeDefaults: VisLayer,
2413
+ notes: StyleNotes,
2414
+ inherited: (StyleValue | null)[],
2415
+ inheriting: number,
2416
+ ): { rules: StyleRule[]; columns: Map<string, StyleColumn> } {
1121
2417
  const edgeRules: StyleRule[] = [];
1122
2418
  const edgeColumns = new Map<string, StyleColumn>();
1123
2419
  const declaredEdge = visValues(edgeDefaults, "edge", (property) => ({ notes, target: "edge", property }), true);
@@ -1139,24 +2435,462 @@ function visEdges(
1139
2435
  facts: { elements: inheriting, reason: "SOURCE_NODE_COLOR" },
1140
2436
  });
1141
2437
  }
1142
- return { values: edge.values, rules: edgeRules, columns: edgeColumns };
2438
+ return { rules: edgeRules, columns: edgeColumns };
1143
2439
  }
1144
2440
 
1145
- /** The vis.js keys the reader reads, per target. */
1146
- const VIS_KEYS: Readonly<Record<StyleTarget, readonly string[]>> = {
1147
- node: ["color", "shape", "size", "borderWidth", "opacity", "hidden", "label", "title", "font"],
1148
- edge: ["color", "width", "dashes", "arrows", "hidden", "label", "title", "font"],
1149
- };
2441
+ /** What a vis.js edge's route is drawn from: its nodes' positions and boxes. */
2442
+ interface VisGeometry {
2443
+ /** A node's position as vis-network draws it (`parseInt` of `x` and `y`, y down), or null. */
2444
+ readonly at: (i: number) => readonly [number, number] | null;
2445
+ /** A node's half width, half height, border width and vis-network shape, or null for a node sized by its label. */
2446
+ readonly box: (i: number) => VisBox | null;
2447
+ }
2448
+
2449
+ /** A node's half width, half height, border width and vis-network shape. */
2450
+ type VisBox = readonly [number, number, number, string];
1150
2451
 
1151
2452
  /**
1152
- * Each node's own vis.js keys as a layer.
2453
+ * vis.js: the positions and boxes of the nodes, for the routes of their edges.
1153
2454
  * @param snapshot - the graph
1154
- * @returns the layer of a node
2455
+ * @param nodeValues - the node values read so far (sizes and border widths)
2456
+ * @param shapes - each node's vis-network shape
2457
+ * @returns the geometry
2458
+ */
2459
+ function visGeometry(snapshot: GraphSnapshot, nodeValues: Values, shapes: readonly string[]): VisGeometry {
2460
+ const whole = (v: unknown): number => {
2461
+ if (typeof v === "string") {
2462
+ return Number.parseInt(v, 10);
2463
+ }
2464
+ return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : Number.NaN;
2465
+ };
2466
+ const [xs, ys] = [cells(snapshot, "node", "x"), cells(snapshot, "node", "y")];
2467
+ const num = (v: StyleValue | null | undefined): number | null => (typeof v === "number" && v > 0 ? v : null);
2468
+ return {
2469
+ at: (i) => {
2470
+ const p = [whole(xs?.(i)), whole(ys?.(i))] as const;
2471
+ return Number.isNaN(p[0]) || Number.isNaN(p[1]) ? null : p;
2472
+ },
2473
+ box: (i) => {
2474
+ const [w, h] = [num(nodeValues.width?.[i]), num(nodeValues.height?.[i])];
2475
+ const bw = nodeValues.borderWidth?.[i];
2476
+ return w === null || h === null ? null : [w / 2, h / 2, typeof bw === "number" ? bw : 1, shapes[i]];
2477
+ },
2478
+ };
2479
+ }
2480
+
2481
+ /** vis-network's `smooth` with every layer applied: booleans set `enabled`, objects their own fields. */
2482
+ interface VisSmooth {
2483
+ enabled: boolean;
2484
+ type: string;
2485
+ roundness: number;
2486
+ forceDirection: unknown;
2487
+ }
2488
+
2489
+ /**
2490
+ * vis.js: an edge's `smooth` over `options.edges.smooth` over vis-network's defaults (dynamic, roundness 0.5).
2491
+ * @param layers - the layers, the lowest first
2492
+ * @returns the smooth options
2493
+ */
2494
+ function visSmooth(...layers: unknown[]): VisSmooth {
2495
+ const s: VisSmooth = { enabled: true, type: "dynamic", roundness: 0.5, forceDirection: "none" };
2496
+ for (const l of layers) {
2497
+ if (typeof l === "boolean") {
2498
+ s.enabled = l;
2499
+ } else if (isJsonObject(l)) {
2500
+ s.enabled = typeof l.enabled === "boolean" ? l.enabled : s.enabled;
2501
+ s.type = typeof l.type === "string" ? l.type : s.type;
2502
+ s.roundness = typeof l.roundness === "number" && Number.isFinite(l.roundness) ? l.roundness : s.roundness;
2503
+ s.forceDirection = l.forceDirection ?? s.forceDirection;
2504
+ }
2505
+ }
2506
+ return s;
2507
+ }
2508
+
2509
+ /**
2510
+ * vis.js: an edge label's size under `scaling.label` and its turn along the line.
2511
+ * @param merged - the edge's merged layer
2512
+ * @param range - the edges' value range
2513
+ * @param a - the source node's center, or null
2514
+ * @param b - the target node's center, or null
2515
+ * @returns the values
2516
+ */
2517
+ function visEdgeLabel(
2518
+ merged: VisLayer,
2519
+ range: readonly [number, number] | null,
2520
+ a: VisPoint | null,
2521
+ b: VisPoint | null,
2522
+ ): Row {
2523
+ const row: Row = {};
2524
+ const size = visLabelScaled(merged, range);
2525
+ if (size !== null) {
2526
+ row.labelSize = size;
2527
+ }
2528
+ const turn = visLabelTurn(merged.fontAlign, a, b);
2529
+ if (turn !== null) {
2530
+ row.labelRotation = turn;
2531
+ }
2532
+ return row;
2533
+ }
2534
+
2535
+ /**
2536
+ * vis.js: how far an edge label whose `font.align` is not "horizontal" is turned to run along its edge, kept readable
2537
+ * (vis-network 9.1.9 `Edge._getRotation`), in degrees clockwise on screen.
2538
+ * @param align - `font.align`
2539
+ * @param a - the source node's center, or null
2540
+ * @param b - the target node's center, or null
2541
+ * @returns the turn, or null for a level label
2542
+ */
2543
+ function visLabelTurn(align: unknown, a: VisPoint | null, b: VisPoint | null): number | null {
2544
+ if (typeof align !== "string" || align === "horizontal" || a === null || b === null) {
2545
+ return null;
2546
+ }
2547
+ const [dx, dy] = [a[0] - b[0], a[1] - b[1]];
2548
+ let angle = Math.atan2(dy, dx);
2549
+ if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
2550
+ angle += Math.PI;
2551
+ }
2552
+ const degrees = Math.round(((angle * 180) / Math.PI) * 1e4) / 1e4;
2553
+ return degrees < 0 ? degrees + 360 : degrees;
2554
+ }
2555
+
2556
+ /** A point, y down. */
2557
+ type VisPoint = readonly [number, number];
2558
+
2559
+ /**
2560
+ * vis-network's control point of a static smooth curve (9.1.9 `BezierEdgeStatic._getViaCoordinates`), from the two
2561
+ * node centers.
2562
+ * @param from - the source node's center
2563
+ * @param to - the target node's center
2564
+ * @param smooth - the smooth options
2565
+ * @returns the control point
2566
+ */
2567
+ function visStaticVia(from: VisPoint, to: VisPoint, smooth: VisSmooth): VisPoint {
2568
+ const f = smooth.roundness;
2569
+ const [fx, fy] = from;
2570
+ const [tx, ty] = to;
2571
+ const dx = Math.abs(fx - tx);
2572
+ const dy = Math.abs(fy - ty);
2573
+ // the step a discrete, diagonalCross or continuous curve takes from its source, toward its target in x and in y
2574
+ // (away from it in y when level: `from.y >= to.y` steps up)
2575
+ const step = f * Math.max(dx, dy);
2576
+ const sx = step * (Math.sign(tx - fx) || 1);
2577
+ const sy = step * (Math.sign(ty - fy) || -1);
2578
+ // which way a cross, horizontal or vertical curve's point lies from the target, back toward the source
2579
+ const backX = (1 - f) * dx * (Math.sign(fx - tx) || 1);
2580
+ const backY = (1 - f) * dy * (Math.sign(fy - ty) || 1);
2581
+ switch (smooth.type) {
2582
+ case "discrete":
2583
+ return visDiscreteVia(from, [fx + sx, fy + sy], dx, dy, f);
2584
+ case "diagonalCross":
2585
+ return [fx + sx, fy + sy];
2586
+ case "straightCross":
2587
+ return dx <= dy ? [tx, ty + backY] : [tx + backX, ty];
2588
+ case "horizontal":
2589
+ return [tx + backX, fy];
2590
+ case "vertical":
2591
+ return [fx, ty + backY];
2592
+ case "curvedCW":
2593
+ return visCurvedVia(from, to, f, f * 0.5 + 0.5);
2594
+ case "curvedCCW":
2595
+ return visCurvedVia(from, to, f, -f * 0.5 + 0.5);
2596
+ default:
2597
+ // continuous: the diagonal step, no farther than the target along the longer axis
2598
+ return dx <= dy
2599
+ ? [fx + Math.sign(sx) * Math.min(step, dx), fy + sy]
2600
+ : [fx + sx, fy + Math.sign(sy) * Math.min(step, dy)];
2601
+ }
2602
+ }
2603
+
2604
+ /**
2605
+ * vis-network's discrete curve: the diagonal step, kept at the source's x (or y) when the nodes are nearly in line.
2606
+ * @param from - the source node's center
2607
+ * @param diagonal - the diagonal step's point
2608
+ * @param dx - the distance between the nodes in x
2609
+ * @param dy - the distance in y
2610
+ * @param f - the roundness
2611
+ * @returns the control point
2612
+ */
2613
+ function visDiscreteVia(from: VisPoint, diagonal: VisPoint, dx: number, dy: number, f: number): VisPoint {
2614
+ if (dx <= dy) {
2615
+ return [dx < f * dy ? from[0] : diagonal[0], diagonal[1]];
2616
+ }
2617
+ return [diagonal[0], dy < f * dx ? from[1] : diagonal[1]];
2618
+ }
2619
+
2620
+ /**
2621
+ * vis-network's curvedCW and curvedCCW: a point `(roundness / 2 + 1 / 2)` of the distance from the source, turned
2622
+ * `turn` half turns from the line to the target (vis-network's sin / cos convention, y down).
2623
+ * @param from - the source node's center
2624
+ * @param to - the target node's center
2625
+ * @param f - the roundness
2626
+ * @param turn - the turn, in half turns
2627
+ * @returns the control point
2628
+ */
2629
+ function visCurvedVia(from: VisPoint, to: VisPoint, f: number, turn: number): VisPoint {
2630
+ const ex = to[0] - from[0];
2631
+ const ey = from[1] - to[1];
2632
+ const r = (f * 0.5 + 0.5) * Math.hypot(ex, ey);
2633
+ const angle = (Math.atan2(ey, ex) + turn * Math.PI) % (2 * Math.PI);
2634
+ return [from[0] + r * Math.sin(angle), from[1] + r * Math.cos(angle)];
2635
+ }
2636
+
2637
+ /**
2638
+ * vis-network's two control points of a cubicBezier curve (9.1.9 `CubicBezierEdge._getViaCoordinates`): each
2639
+ * `roundness` of the way across from its own end, horizontally when the nodes are farther apart in x or
2640
+ * `forceDirection` is horizontal (or true), else vertically.
2641
+ * @param from - the source node's center
2642
+ * @param to - the target node's center
2643
+ * @param smooth - the smooth options
2644
+ * @returns the two control points
2645
+ */
2646
+ function visCubicVia(from: VisPoint, to: VisPoint, smooth: VisSmooth): [VisPoint, VisPoint] {
2647
+ const [dx, dy] = [from[0] - to[0], from[1] - to[1]];
2648
+ const r = smooth.roundness;
2649
+ const force = smooth.forceDirection;
2650
+ const across = (Math.abs(dx) > Math.abs(dy) || force === true || force === "horizontal") && force !== "vertical";
2651
+ return across
2652
+ ? [
2653
+ [from[0] - r * dx, from[1]],
2654
+ [to[0] + r * dx, to[1]],
2655
+ ]
2656
+ : [
2657
+ [from[0], from[1] - r * dy],
2658
+ [to[0], to[1] + r * dy],
2659
+ ];
2660
+ }
2661
+
2662
+ /**
2663
+ * A point as the neutral `bends` spell it: `along,across` relative to the line between two centers, y down in, y up
2664
+ * out.
2665
+ * @param a - the source center
2666
+ * @param b - the target center
2667
+ * @param p - the point
2668
+ * @returns `along,across`
2669
+ */
2670
+ function alongAcross(a: VisPoint, b: VisPoint, p: VisPoint): string {
2671
+ const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
2672
+ const [qx, qy] = [p[0] - a[0], p[1] - a[1]];
2673
+ const l2 = dx * dx + dy * dy;
2674
+ return `${fraction((qx * dx + qy * dy) / l2)},${fraction((dy * qx - dx * qy) / l2)}`;
2675
+ }
2676
+
2677
+ /**
2678
+ * A number rounded to four places for a bend, with no negative zero.
2679
+ * @param v - the number
2680
+ * @returns its text
2681
+ */
2682
+ const fraction = (v: number): string => String(Math.round(v * 1e4) / 1e4 || 0);
2683
+
2684
+ /**
2685
+ * vis.js: the route vis-network draws an edge along. A smooth edge is one Bezier curve through its control points
2686
+ * (`bendCurve: "bezier"`): a static type's one point, cubicBezier's two. A dynamic one (vis-network's default) is
2687
+ * drawn through a support node the physics simulation moves; where it never moves (physics off for the network or
2688
+ * the edge) it stays at the middle and the edge is straight. A self-loop is vis-network's circle of
2689
+ * `selfReference.size` (20) centered on its node's outline at `selfReference.angle` (pi / 4).
2690
+ * @param defaults - `options.edges`
2691
+ * @param own - the edge's own keys
2692
+ * @param s - its source node
2693
+ * @param t - its target node
2694
+ * @param g - the nodes' geometry
2695
+ * @returns the route's values, none for a straight edge or a route the file does not decide
2696
+ */
2697
+ function visRoute(defaults: VisLayer, own: VisLayer, s: number, t: number, g: VisGeometry): Row {
2698
+ if (s === t) {
2699
+ return visLoop(defaults, own, g.box(s));
2700
+ }
2701
+ const smooth = visSmooth(defaults.smooth, own.smooth);
2702
+ const [a, b] = [g.at(s), g.at(t)];
2703
+ // ponytail: a dynamic curve where physics runs is wherever the simulation left its support node; drawn straight
2704
+ if (!smooth.enabled || smooth.type === "dynamic" || a === null || b === null || (a[0] === b[0] && a[1] === b[1])) {
2705
+ return {};
2706
+ }
2707
+ const controls = smooth.type === "cubicBezier" ? visCubicVia(a, b, smooth) : [visStaticVia(a, b, smooth)];
2708
+ return { curved: true, bendCurve: "bezier", bends: controls.map((p) => alongAcross(a, b, p)).join(" ") };
2709
+ }
2710
+
2711
+ /**
2712
+ * vis.js: a self-loop, vis-network's circle (9.1.9 `EdgeBase._getCircleData`, `getSelfRefCoordinates`) of radius
2713
+ * `selfReference.size` (or the older `selfReferenceSize`; 20) centered on the node's outline (visToBorder) along
2714
+ * the ray at `selfReference.angle` (pi / 4, counterclockwise from the right). Written as bends at the corners of the
2715
+ * square round the circle, drawn through their midpoints, which run along the circle.
2716
+ * @param defaults - `options.edges`
2717
+ * @param own - the edge's own keys
2718
+ * @param box - the node's half width, half height, border width and shape, or null when unknown
2719
+ * @returns the loop's values, none for a node whose size is not known here
2720
+ */
2721
+ function visLoop(defaults: VisLayer, own: VisLayer, box: VisBox | null): Row {
2722
+ if (box === null) {
2723
+ return {};
2724
+ }
2725
+ const field = (layer: VisLayer, key: string): unknown =>
2726
+ isJsonObject(layer.selfReference) ? layer.selfReference[key] : undefined;
2727
+ const pick = (...vs: unknown[]): number | null =>
2728
+ (vs.find((v) => typeof v === "number" && Number.isFinite(v)) as number | undefined) ?? null;
2729
+ const radius =
2730
+ pick(field(own, "size"), own.selfReferenceSize, field(defaults, "size"), defaults.selfReferenceSize) ?? 20;
2731
+ const angle = pick(field(own, "angle"), field(defaults, "angle")) ?? Math.PI / 4;
2732
+ const [hw, hh] = box;
2733
+ const reach = visToBorder(box, angle);
2734
+ // the circle's center relative to the node's, y up
2735
+ const [cx, cy] = [Math.cos(angle) * reach, Math.sin(angle) * reach];
2736
+ const behind = [field(own, "renderBehindTheNode"), field(defaults, "renderBehindTheNode")].find(
2737
+ (v) => typeof v === "boolean",
2738
+ );
2739
+ if (behind === false) {
2740
+ return visFrontArc(cx, cy, radius, angle, box);
2741
+ }
2742
+ // the node's farthest reach from its center: its radius when round, its corner otherwise
2743
+ const round = box[3] === "dot" || box[3] === "circle" || box[3] === "circularImage";
2744
+ if (reach + (round ? hw : Math.hypot(hw, hh)) < radius) {
2745
+ return visRing(cx, cy, radius, angle, box);
2746
+ }
2747
+ // the corners of the square round the circle, the first the one facing the node and again last, so the curve
2748
+ // through their midpoints runs the whole circle and its ends, out of and back into the node, bulge as it does
2749
+ const corners: string[] = [];
2750
+ for (let k = 0; k <= 4; k++) {
2751
+ const a = angle + Math.PI + ((k % 4) * Math.PI) / 2;
2752
+ const [x, y] = [cx + Math.SQRT2 * radius * Math.cos(a), cy + Math.SQRT2 * radius * Math.sin(a)];
2753
+ corners.push(`${fraction(x / hw)},${fraction(y / hh)}`);
2754
+ }
2755
+ return { curved: true, bendCurve: "midpoints", bends: corners.join(" ") };
2756
+ }
2757
+
2758
+ /** How many points a vis-network self-loop's circle is written through where it does not meet its node. */
2759
+ const VIS_RING_STEPS = 96;
2760
+
2761
+ /**
2762
+ * vis.js: a self-loop whose circle holds its whole node, so meets it nowhere (a small node, or a large
2763
+ * `selfReference.size`): the circle alone, written as points round it from the side facing the node and back to it,
2764
+ * the first and the last the same point, which graphty draws as that closed ring with no line to the node.
2765
+ * @param cx - the circle's center across from the node's, y up
2766
+ * @param cy - and up from it
2767
+ * @param radius - its radius
2768
+ * @param angle - the ray its center is on, counterclockwise from the right
2769
+ * @param box - the node's half width and half height
2770
+ * @returns the loop's values
2771
+ */
2772
+ function visRing(cx: number, cy: number, radius: number, angle: number, box: VisBox): Row {
2773
+ const [hw, hh] = box;
2774
+ const points: string[] = [];
2775
+ for (let k = 0; k <= VIS_RING_STEPS; k++) {
2776
+ const a = angle + Math.PI + (2 * Math.PI * (k % VIS_RING_STEPS)) / VIS_RING_STEPS;
2777
+ points.push(`${fraction((cx + radius * Math.cos(a)) / hw)},${fraction((cy + radius * Math.sin(a)) / hh)}`);
2778
+ }
2779
+ return { curved: false, bends: points.join(" ") };
2780
+ }
2781
+
2782
+ /**
2783
+ * vis.js: a self-loop drawn in front of its node (`selfReference.renderBehindTheNode: false`): only the arc of its
2784
+ * circle between the two points vis-network 9.1.9 searches for (Edge._circle, `_findBorderPositionCircle`: ten
2785
+ * halvings of a "position" between the angle and the angle plus pi, read as a fraction of a turn), clockwise on screen
2786
+ * from the first to the second, written as points along it.
2787
+ * @param cx - the circle's center across from the node's, y up
2788
+ * @param cy - and up from it
2789
+ * @param radius - its radius
2790
+ * @param angle - `selfReference.angle`
2791
+ * @param box - the node's half width, half height, border width and shape
2792
+ * @returns the loop's values
2793
+ */
2794
+ function visFrontArc(cx: number, cy: number, radius: number, angle: number, box: VisBox): Row {
2795
+ // on screen, y down, the node's center at the origin
2796
+ const [x, y] = [cx, -cy];
2797
+ const search = (direction: -1 | 1): [number, number] => {
2798
+ let [low, high] = [angle, angle + Math.PI];
2799
+ let pos: [number, number] = [x, y];
2800
+ for (let iteration = 0; low <= high && iteration < 10; iteration++) {
2801
+ const middle = (low + high) * 0.5;
2802
+ const turn = middle * 2 * Math.PI;
2803
+ pos = [x + radius * Math.cos(turn), y - radius * Math.sin(turn)];
2804
+ const difference = visToBorder(box, Math.atan2(-pos[1], -pos[0])) - Math.hypot(pos[0], pos[1]);
2805
+ if (Math.abs(difference) < 0.05) {
2806
+ break;
2807
+ }
2808
+ if (difference > 0 === direction > 0) {
2809
+ low = middle;
2810
+ } else {
2811
+ high = middle;
2812
+ }
2813
+ }
2814
+ return pos;
2815
+ };
2816
+ const [from, to] = [search(-1), search(1)];
2817
+ const start = Math.atan2(from[1] - y, from[0] - x);
2818
+ let end = Math.atan2(to[1] - y, to[0] - x);
2819
+ if (end < start) {
2820
+ end += 2 * Math.PI;
2821
+ }
2822
+ const [hw, hh] = box;
2823
+ const steps = Math.max(2, Math.ceil(((end - start) / (2 * Math.PI)) * VIS_RING_STEPS));
2824
+ const points: string[] = [];
2825
+ for (let k = 0; k <= steps; k++) {
2826
+ const a = start + ((end - start) * k) / steps;
2827
+ points.push(`${fraction((x + radius * Math.cos(a)) / hw)},${fraction(-(y + radius * Math.sin(a)) / hh)}`);
2828
+ }
2829
+ return { curved: false, bends: points.join(" ") };
2830
+ }
2831
+
2832
+ /**
2833
+ * vis-network 9.1.9's `distanceToBorder` of a node, how far from its center its outline is along a ray: a dot its
2834
+ * size, a circle or circularImage half its width, an ellipse its ellipse, all with no border; every other shape its
2835
+ * box plus its border width (NodeBase._distanceToBorder, Box).
2836
+ * @param box - the node's half width, half height, border width and shape
2837
+ * @param angle - the ray, counterclockwise from the right
2838
+ * @returns the distance
1155
2839
  */
1156
- function nodeRecords(snapshot: GraphSnapshot): (i: number) => VisLayer {
1157
- return elementRecords(snapshot, "node");
2840
+ function visToBorder(box: VisBox, angle: number): number {
2841
+ const [hw, hh, bw, shape] = box;
2842
+ if (shape === "dot" || shape === "circle" || shape === "circularImage") {
2843
+ return hw;
2844
+ }
2845
+ if (shape === "ellipse") {
2846
+ return (hw * hh) / Math.hypot(Math.sin(angle) * hw, Math.cos(angle) * hh);
2847
+ }
2848
+ return Math.min(Math.abs(hw / Math.cos(angle)), Math.abs(hh / Math.sin(angle))) + bw;
1158
2849
  }
1159
2850
 
2851
+ /** The vis.js keys the reader reads, per target. */
2852
+ const VIS_KEYS: Readonly<Record<StyleTarget, readonly string[]>> = {
2853
+ node: [
2854
+ "color",
2855
+ "shape",
2856
+ "size",
2857
+ "value",
2858
+ "scaling",
2859
+ "borderWidth",
2860
+ "opacity",
2861
+ "hidden",
2862
+ "label",
2863
+ "title",
2864
+ "font",
2865
+ "margin",
2866
+ "widthConstraint",
2867
+ "heightConstraint",
2868
+ "image",
2869
+ "brokenImage",
2870
+ "imagePadding",
2871
+ "icon",
2872
+ "shapeProperties",
2873
+ ],
2874
+ edge: [
2875
+ "color",
2876
+ "width",
2877
+ "value",
2878
+ "scaling",
2879
+ "dashes",
2880
+ "arrows",
2881
+ "hidden",
2882
+ "label",
2883
+ "title",
2884
+ "font",
2885
+ "background",
2886
+ "smooth",
2887
+ "selfReference",
2888
+ "selfReferenceSize",
2889
+ "physics",
2890
+ "widthConstraint",
2891
+ ],
2892
+ };
2893
+
1160
2894
  /**
1161
2895
  * Each element's own vis.js keys as a layer, read from the graph's columns.
1162
2896
  * @param snapshot - the graph
@@ -1164,6 +2898,17 @@ function nodeRecords(snapshot: GraphSnapshot): (i: number) => VisLayer {
1164
2898
  * @returns the layer of an element
1165
2899
  */
1166
2900
  function elementRecords(snapshot: GraphSnapshot, target: StyleTarget): (i: number) => VisLayer {
2901
+ const raw = elementRaw(snapshot, target);
2902
+ return (i) => visLayer(raw(i), target);
2903
+ }
2904
+
2905
+ /**
2906
+ * Each element's own vis.js keys as the file gives them, read from the graph's columns.
2907
+ * @param snapshot - the graph
2908
+ * @param target - nodes or edges
2909
+ * @returns the keys of an element
2910
+ */
2911
+ function elementRaw(snapshot: GraphSnapshot, target: StyleTarget): (i: number) => Record<string, unknown> {
1167
2912
  const readers = VIS_KEYS[target]
1168
2913
  .map((name) => [name, cells(snapshot, target, name)] as const)
1169
2914
  .filter((x): x is readonly [string, (i: number) => unknown] => x[1] !== null);
@@ -1175,7 +2920,7 @@ function elementRecords(snapshot: GraphSnapshot, target: StyleTarget): (i: numbe
1175
2920
  record[name] = v;
1176
2921
  }
1177
2922
  }
1178
- return visLayer(record, target);
2923
+ return record;
1179
2924
  };
1180
2925
  }
1181
2926
 
@@ -1283,10 +3028,79 @@ const visSize = (want: (p: string) => StyleValue | null): StyleValue | null => {
1283
3028
  return VIS_SIZED.has(shape) ? oneSize(want) : null;
1284
3029
  };
1285
3030
 
3031
+ /**
3032
+ * The vis.js shape a written node is read back as.
3033
+ * @param want - the block's values of the element
3034
+ * @returns the shape
3035
+ */
3036
+ const visShapeBack = (want: (p: string) => StyleValue | null): string => {
3037
+ const s = want("shape");
3038
+ return typeof s === "string" ? visShape(s)[0] : "ellipse";
3039
+ };
3040
+
3041
+ /**
3042
+ * What a written vis.js node of a shape that changes what its size gives back reads as (visSizedShape, visFitShape):
3043
+ * a hexagon's height and how far below its square its label hangs, a database's label at its top.
3044
+ * @param property - the fact
3045
+ * @returns the reading back of the element's values, null where the shape changes nothing
3046
+ */
3047
+ const visShapeFactBack =
3048
+ (property: "height" | "labelOffsetY") =>
3049
+ (want: (p: string) => StyleValue | null): StyleValue | null => {
3050
+ const shape = visShapeBack(want);
3051
+ const size = visSize(want);
3052
+ if (shape === "hexagon" && typeof size === "number") {
3053
+ return property === "height" ? (Math.sqrt(3) * size) / 2 : ((2 - Math.sqrt(3)) * size) / 4;
3054
+ }
3055
+ return property === "labelOffsetY" && shape === "database" ? VIS_MARGIN : null;
3056
+ };
3057
+
3058
+ /**
3059
+ * What the reader reads back for a fact of a node the writer wrote in a shape vis-network sizes by its label: the
3060
+ * shape's own growth, from the label and font size written.
3061
+ * @param property - the fact
3062
+ * @returns the reading back of the element's values
3063
+ */
3064
+ const visFitBack =
3065
+ (property: string) =>
3066
+ (want: (p: string) => StyleValue | null): StyleValue | null => {
3067
+ const s = want("shape");
3068
+ const shape = typeof s === "string" ? visShape(s)[0] : "ellipse";
3069
+ return visLabelFit({ fontSize: want("labelSize"), label: want("label") }, shape)[property] ?? null;
3070
+ };
3071
+
3072
+ /**
3073
+ * What reading a file graphty wrote in vis.js gives back: VIS_WRITTEN, and for a self-loop the route the reader draws
3074
+ * it along (vis-network's circle, made from vis-network's own defaults and the node's size, never from the block).
3075
+ * @param snapshot - the graph
3076
+ * @param edgeValues - the edge values the reader read
3077
+ * @returns the readings back
3078
+ */
3079
+ function visWritten(snapshot: GraphSnapshot, edgeValues: Values): Written {
3080
+ const loop =
3081
+ (property: string) =>
3082
+ (want: (p: string) => StyleValue | null, i: number): StyleValue | null =>
3083
+ snapshot.edgeSource(i) === snapshot.edgeTarget(i) ? (edgeValues[property]?.[i] ?? null) : want(property);
3084
+ return {
3085
+ node: VIS_WRITTEN.node,
3086
+ edge: { ...VIS_WRITTEN.edge, bends: loop("bends"), bendCurve: loop("bendCurve"), curved: loop("curved") },
3087
+ };
3088
+ }
3089
+
1286
3090
  const VIS_WRITTEN: Written = {
1287
3091
  node: {
1288
- width: visSize,
1289
- height: visSize,
3092
+ width: (want) => visSize(want) ?? visFitBack("width")(want),
3093
+ height: (want) => visShapeFactBack("height")(want) ?? visSize(want) ?? visFitBack("height")(want),
3094
+ labelOffsetY: visShapeFactBack("labelOffsetY"),
3095
+ labelFit: visFitBack("labelFit"),
3096
+ labelPaddingX: visFitBack("labelPaddingX"),
3097
+ labelPaddingY: visFitBack("labelPaddingY"),
3098
+ labelFitSquare: visFitBack("labelFitSquare"),
3099
+ // the writer writes hex colors, whose alpha vis-network's node opacity replaces, the border's too
3100
+ borderOpacity: (want) => {
3101
+ const opacity = want("opacity");
3102
+ return typeof opacity === "number" ? opacity : want("borderOpacity");
3103
+ },
1290
3104
  shape: (want) => {
1291
3105
  const s = want("shape");
1292
3106
  if (typeof s !== "string") {
@@ -1295,6 +3109,17 @@ const VIS_WRITTEN: Written = {
1295
3109
  const site = { notes: new StyleNotes(), target: "node" as const, property: "shape" };
1296
3110
  return canonEnum("shape", visShape(s)[0], "vis", site);
1297
3111
  },
3112
+ labelPosition: (want) => {
3113
+ const s = want("shape");
3114
+ if (typeof s !== "string") {
3115
+ return null;
3116
+ }
3117
+ const shape = visShape(s)[0];
3118
+ if (shape === "database") {
3119
+ return "inside-top";
3120
+ }
3121
+ return VIS_SIZED.has(shape) ? "bottom" : "center";
3122
+ },
1298
3123
  },
1299
3124
  edge: {
1300
3125
  lineStyle: (want) => {
@@ -1302,7 +3127,12 @@ const VIS_WRITTEN: Written = {
1302
3127
  if (s === null) {
1303
3128
  return null;
1304
3129
  }
1305
- return s === "solid" ? "solid" : "dashed";
3130
+ // written as `dashes: true`, vis-network's [5, 5], which a wide edge's round caps close up
3131
+ if (s === "solid") {
3132
+ return "solid";
3133
+ }
3134
+ const width = want("width");
3135
+ return visDashStyle([5, 5], typeof width === "number" ? width : 1, true);
1306
3136
  },
1307
3137
  targetArrow: (want) => arrowBack(want("targetArrow")),
1308
3138
  sourceArrow: (want) => arrowBack(want("sourceArrow")),
@@ -1355,8 +3185,8 @@ export const NATIVE: Readonly<
1355
3185
  edge: cyjsProperties(CYJS_EDGE_PROPERTIES, ["curved", "labelItalic", "visible"]),
1356
3186
  },
1357
3187
  graphology: {
1358
- node: ["fill", "fillOpacity", "width", "height", "label", "visible"],
1359
- edge: ["color", "opacity", "width", "label", "visible"],
3188
+ node: ["fill", "fillOpacity", "width", "height", "label"],
3189
+ edge: ["color", "opacity", "width", "label"],
1360
3190
  },
1361
3191
  vis: {
1362
3192
  node: [
@@ -1928,16 +3758,15 @@ function writeGraphology(
1928
3758
  writeKey(
1929
3759
  table,
1930
3760
  "size",
1931
- target === "node" ? nodeSizes(p("node", "width"), p("node", "height"), approximated) : p("edge", "width"),
3761
+ target === "node"
3762
+ ? nodeSizes(p("node", "width"), p("node", "height"), approximated).map((d) =>
3763
+ d === null ? null : d / 2,
3764
+ )
3765
+ : // sigma draws an edge half as wide as its size (readSigmaRow)
3766
+ p("edge", "width").map((w) => (typeof w === "number" ? w * 2 : w)),
1932
3767
  replaced,
1933
3768
  );
1934
3769
  writeKey(table, "label", p(target, "label"), replaced);
1935
- writeKey(
1936
- table,
1937
- "hidden",
1938
- p(target, "visible").map((v) => (v === false ? true : null)),
1939
- replaced,
1940
- );
1941
3770
  }
1942
3771
  }
1943
3772
 
@@ -1948,6 +3777,24 @@ function writeGraphology(
1948
3777
  * @param approximated - where approximations are counted
1949
3778
  * @param replaced - where replaced data values are counted
1950
3779
  */
3780
+ /**
3781
+ * vis.js: a scaling range of each written size for the elements with a `value`, which vis-network would otherwise
3782
+ * map onto `scaling` instead of drawing the size written (a node's radius, an edge's width).
3783
+ * @param table - the node or edge table being written
3784
+ * @param sizes - each element's written size, or null
3785
+ * @param replaced - the data values replaced so far
3786
+ */
3787
+ function keepScaled(table: Table, sizes: readonly (StyleValue | null)[], replaced: Replaced): void {
3788
+ const value = table.get("value");
3789
+ if (value === null) {
3790
+ return;
3791
+ }
3792
+ const scaling = sizes.map((v, i) =>
3793
+ typeof v !== "number" || typeof plainCell(value.value(i)) !== "number" ? null : { min: v, max: v },
3794
+ );
3795
+ writeKey(table, "scaling", scaling, replaced);
3796
+ }
3797
+
1951
3798
  function writeVis(
1952
3799
  out: ReturnType<GraphSnapshot["withColumns"]>,
1953
3800
  input: ExporterStyleInput,
@@ -2007,7 +3854,9 @@ function writeVis(
2007
3854
  );
2008
3855
  const sizes = nodeSizes(p("node", "width"), p("node", "height"), approximated);
2009
3856
  const shapes = p("node", "shape").map((s, i) => {
2010
- const [shape, exact] = typeof s === "string" ? visShape(s) : [null, true];
3857
+ const [written, exact] = typeof s === "string" ? visShape(s) : [null, true];
3858
+ // an ellipse with no size of its own is vis-network's label-sized `ellipse`, one with a size its `dot`
3859
+ const shape = written === "dot" && s === "ellipse" && sizes[i] === null ? "ellipse" : written;
2011
3860
  if (!exact) {
2012
3861
  approximated.add("node", "shape", s);
2013
3862
  }
@@ -2017,7 +3866,11 @@ function writeVis(
2017
3866
  return shape;
2018
3867
  });
2019
3868
  writeKey(out.nodes, "shape", shapes, replaced);
2020
- writeKey(out.nodes, "size", sizes, replaced);
3869
+ // vis-network draws `size` as a radius
3870
+ const radii = sizes.map((d) => (d === null ? null : d / 2));
3871
+ writeKey(out.nodes, "size", radii, replaced);
3872
+ // a node's `value` would scale its size again on reading: a scaling range of its own radius keeps it
3873
+ keepScaled(out.nodes, radii, replaced);
2021
3874
  writeKey(out.nodes, "borderWidth", p("node", "borderWidth"), replaced);
2022
3875
  writeKey(out.nodes, "opacity", p("node", "opacity"), replaced);
2023
3876
  label(out.nodes, "node");
@@ -2039,7 +3892,10 @@ function writeVis(
2039
3892
  }),
2040
3893
  replaced,
2041
3894
  );
2042
- writeKey(out.edges, "width", p("edge", "width"), replaced);
3895
+ const widths = p("edge", "width");
3896
+ writeKey(out.edges, "width", widths, replaced);
3897
+ // an edge's `value` would scale its width again on reading: a scaling range of its own width keeps it
3898
+ keepScaled(out.edges, widths, replaced);
2043
3899
  writeKey(
2044
3900
  out.edges,
2045
3901
  "dashes",