@graphty/graph-io 0.3.48 → 0.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (570) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-Bzn3L-je.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-Bzn3L-je.js.map} +1 -1
  3. package/dist/chunks/bends-BGXjv_U4.js +129 -0
  4. package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
  5. package/dist/chunks/{block-Cu22ewuS.js → block-iptrlMBb.js} +906 -164
  6. package/dist/chunks/block-iptrlMBb.js.map +1 -0
  7. package/dist/chunks/choose-CczZJaX_.js +717 -0
  8. package/dist/chunks/choose-CczZJaX_.js.map +1 -0
  9. package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/{context-cgmmOxZ2.js → context-C3p0RQxR.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-C3p0RQxR.js.map} +1 -1
  15. package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
  16. package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
  17. package/dist/chunks/{credentials-BiQsnMTb.js → credentials-DXHx7nwR.js} +16 -14
  18. package/dist/chunks/credentials-DXHx7nwR.js.map +1 -0
  19. package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
  20. package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-DLA5kINx.js} +7 -2
  24. package/dist/chunks/descriptor-DLA5kINx.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-BwHQkZle.js} +1660 -133
  26. package/dist/chunks/emit-BwHQkZle.js.map +1 -0
  27. package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
  28. package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
  29. package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
  30. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  31. package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  32. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  33. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-D2CETOMK.js} +1222 -156
  34. package/dist/chunks/exporter-D2CETOMK.js.map +1 -0
  35. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-D9yMtocA.js} +7 -7
  36. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-D9yMtocA.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-_jrBTnos.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-_jrBTnos.js.map} +1 -1
  39. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-nDfxIK5D.js} +9 -8
  40. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-nDfxIK5D.js.map} +1 -1
  41. package/dist/chunks/format-BXiAb-1h.js +408 -0
  42. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  43. package/dist/chunks/{generic-CP6YD34u.js → generic-ymWCETAJ.js} +387 -31
  44. package/dist/chunks/generic-ymWCETAJ.js.map +1 -0
  45. package/dist/chunks/{importer-e3huMZbm.js → importer-B92x1xY5.js} +14 -10
  46. package/dist/chunks/importer-B92x1xY5.js.map +1 -0
  47. package/dist/chunks/{importer-B87_fmVx.js → importer-CT9mJ1nn.js} +326 -84
  48. package/dist/chunks/importer-CT9mJ1nn.js.map +1 -0
  49. package/dist/chunks/{importer-CB_zV5_L.js → importer-DaLTjbds.js} +116 -1102
  50. package/dist/chunks/importer-DaLTjbds.js.map +1 -0
  51. package/dist/chunks/{importer-DPZDN40-.js → importer-GREKMdIf.js} +25 -17
  52. package/dist/chunks/importer-GREKMdIf.js.map +1 -0
  53. package/dist/chunks/{importer-Bji2MeWr.js → importer-PqaaKKnY.js} +13 -10
  54. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-PqaaKKnY.js.map} +1 -1
  55. package/dist/chunks/{importer-Cyy6UseH.js → importer-Vn5QvmmJ.js} +66 -13
  56. package/dist/chunks/importer-Vn5QvmmJ.js.map +1 -0
  57. package/dist/chunks/{importer-CVT-JyDE.js → importer-vRq4h3j5.js} +1469 -356
  58. package/dist/chunks/importer-vRq4h3j5.js.map +1 -0
  59. package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
  60. package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
  61. package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
  62. package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  63. package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-Clf4QIyl.js} +282 -74
  64. package/dist/chunks/lexicon-Clf4QIyl.js.map +1 -0
  65. package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
  66. package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  67. package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
  68. package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
  69. package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
  70. package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
  71. package/dist/chunks/round-trip-Zm3_ivgq.js +79 -0
  72. package/dist/chunks/round-trip-Zm3_ivgq.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-EokFic1V.js} +5 -5
  74. package/dist/chunks/sigv4-EokFic1V.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CCs214_3.js} +1237 -138
  76. package/dist/chunks/styles-CCs214_3.js.map +1 -0
  77. package/dist/chunks/styles-Cm62yyQX.js +2812 -0
  78. package/dist/chunks/styles-Cm62yyQX.js.map +1 -0
  79. package/dist/chunks/styles-DPQQIig4.js +1928 -0
  80. package/dist/chunks/styles-DPQQIig4.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-UMFdNqze.js} +503 -172
  82. package/dist/chunks/styles-yed-UMFdNqze.js.map +1 -0
  83. package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
  84. package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
  85. package/dist/csv.js +7 -7
  86. package/dist/cx.js +2 -2
  87. package/dist/cx2.js +2 -2
  88. package/dist/cys.js +1 -1
  89. package/dist/dot.js +5 -4
  90. package/dist/dot.js.map +1 -1
  91. package/dist/gexf.js +42 -12
  92. package/dist/gexf.js.map +1 -1
  93. package/dist/gml.js +847 -193
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +232 -220
  96. package/dist/graph-io.js.map +1 -1
  97. package/dist/graphml.js +3 -3
  98. package/dist/json.js +3 -2
  99. package/dist/neo4j.js +6 -6
  100. package/dist/obo.js +1 -1
  101. package/dist/pajek.js +1 -1
  102. package/dist/positions.js +45 -3
  103. package/dist/positions.js.map +1 -1
  104. package/dist/sources-cypher.js +138 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +16 -6
  108. package/dist/sources-rest.js.map +1 -1
  109. package/dist/sources-sparql.d.ts +1 -0
  110. package/dist/sources-sparql.js +405 -0
  111. package/dist/sources-sparql.js.map +1 -0
  112. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  113. package/dist/src/formats/cx/importer.js +5 -1
  114. package/dist/src/formats/cx/importer.js.map +1 -1
  115. package/dist/src/formats/cx/styles.d.ts +24 -1
  116. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  117. package/dist/src/formats/cx/styles.js +117 -11
  118. package/dist/src/formats/cx/styles.js.map +1 -1
  119. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  120. package/dist/src/formats/cx2/importer.js +1 -0
  121. package/dist/src/formats/cx2/importer.js.map +1 -1
  122. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  123. package/dist/src/formats/cx2/styles.js +3 -1
  124. package/dist/src/formats/cx2/styles.js.map +1 -1
  125. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cys/importer.js +1 -0
  127. package/dist/src/formats/cys/importer.js.map +1 -1
  128. package/dist/src/formats/cys/styles.d.ts +2 -0
  129. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cys/styles.js +15 -7
  131. package/dist/src/formats/cys/styles.js.map +1 -1
  132. package/dist/src/formats/dot/drawing.d.ts +98 -0
  133. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  134. package/dist/src/formats/dot/drawing.js +734 -0
  135. package/dist/src/formats/dot/drawing.js.map +1 -0
  136. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  137. package/dist/src/formats/dot/importer.js +108 -31
  138. package/dist/src/formats/dot/importer.js.map +1 -1
  139. package/dist/src/formats/dot/label-places.d.ts +34 -0
  140. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  141. package/dist/src/formats/dot/label-places.js +179 -0
  142. package/dist/src/formats/dot/label-places.js.map +1 -0
  143. package/dist/src/formats/dot/record.d.ts +50 -0
  144. package/dist/src/formats/dot/record.d.ts.map +1 -0
  145. package/dist/src/formats/dot/record.js +275 -0
  146. package/dist/src/formats/dot/record.js.map +1 -0
  147. package/dist/src/formats/dot/styles.d.ts +62 -2
  148. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  149. package/dist/src/formats/dot/styles.js +1454 -119
  150. package/dist/src/formats/dot/styles.js.map +1 -1
  151. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  152. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  153. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  154. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  155. package/dist/src/formats/gexf/importer.d.ts +7 -0
  156. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  157. package/dist/src/formats/gexf/importer.js +41 -5
  158. package/dist/src/formats/gexf/importer.js.map +1 -1
  159. package/dist/src/formats/gexf/preview.d.ts +77 -0
  160. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  161. package/dist/src/formats/gexf/preview.js +191 -0
  162. package/dist/src/formats/gexf/preview.js.map +1 -0
  163. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  164. package/dist/src/formats/gexf/schema.js +2 -1
  165. package/dist/src/formats/gexf/schema.js.map +1 -1
  166. package/dist/src/formats/gexf/styles.d.ts +58 -31
  167. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  168. package/dist/src/formats/gexf/styles.js +848 -225
  169. package/dist/src/formats/gexf/styles.js.map +1 -1
  170. package/dist/src/formats/gml/importer.d.ts +5 -2
  171. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  172. package/dist/src/formats/gml/importer.js +258 -71
  173. package/dist/src/formats/gml/importer.js.map +1 -1
  174. package/dist/src/formats/gml/index.d.ts +1 -1
  175. package/dist/src/formats/gml/index.d.ts.map +1 -1
  176. package/dist/src/formats/gml/index.js +1 -1
  177. package/dist/src/formats/gml/index.js.map +1 -1
  178. package/dist/src/formats/gml/positions.d.ts +7 -0
  179. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  180. package/dist/src/formats/gml/positions.js +29 -7
  181. package/dist/src/formats/gml/positions.js.map +1 -1
  182. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  183. package/dist/src/formats/gml/styles.js +753 -101
  184. package/dist/src/formats/gml/styles.js.map +1 -1
  185. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  186. package/dist/src/formats/graphml/styles-plain.js +6 -10
  187. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  188. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  189. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  190. package/dist/src/formats/graphml/styles-yed.js +692 -161
  191. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  192. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  193. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  194. package/dist/src/formats/graphml/styles-yfiles3.js +429 -64
  195. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  196. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  197. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/yfiles.js +16 -6
  199. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  200. package/dist/src/formats/json/dialect.d.ts +3 -2
  201. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  202. package/dist/src/formats/json/dialect.js +39 -5
  203. package/dist/src/formats/json/dialect.js.map +1 -1
  204. package/dist/src/formats/json/importer.d.ts +13 -2
  205. package/dist/src/formats/json/importer.d.ts.map +1 -1
  206. package/dist/src/formats/json/importer.js +42 -6
  207. package/dist/src/formats/json/importer.js.map +1 -1
  208. package/dist/src/formats/json/index.d.ts +1 -1
  209. package/dist/src/formats/json/index.d.ts.map +1 -1
  210. package/dist/src/formats/json/index.js +1 -1
  211. package/dist/src/formats/json/index.js.map +1 -1
  212. package/dist/src/formats/json/positions.d.ts +5 -2
  213. package/dist/src/formats/json/positions.d.ts.map +1 -1
  214. package/dist/src/formats/json/positions.js +20 -4
  215. package/dist/src/formats/json/positions.js.map +1 -1
  216. package/dist/src/formats/json/styles.d.ts +12 -1
  217. package/dist/src/formats/json/styles.d.ts.map +1 -1
  218. package/dist/src/formats/json/styles.js +1755 -136
  219. package/dist/src/formats/json/styles.js.map +1 -1
  220. package/dist/src/formats/pajek/draw.d.ts +96 -0
  221. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  222. package/dist/src/formats/pajek/draw.js +372 -0
  223. package/dist/src/formats/pajek/draw.js.map +1 -0
  224. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  225. package/dist/src/formats/pajek/exporter.js +70 -51
  226. package/dist/src/formats/pajek/exporter.js.map +1 -1
  227. package/dist/src/formats/pajek/importer.d.ts +9 -0
  228. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  229. package/dist/src/formats/pajek/importer.js +233 -102
  230. package/dist/src/formats/pajek/importer.js.map +1 -1
  231. package/dist/src/formats/pajek/index.d.ts +1 -0
  232. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  233. package/dist/src/formats/pajek/index.js.map +1 -1
  234. package/dist/src/formats/pajek/styles.d.ts +15 -7
  235. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  236. package/dist/src/formats/pajek/styles.js +1278 -114
  237. package/dist/src/formats/pajek/styles.js.map +1 -1
  238. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  239. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  240. package/dist/src/formats/pajek/syntax.js +26 -2
  241. package/dist/src/formats/pajek/syntax.js.map +1 -1
  242. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  243. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  244. package/dist/src/formats/xgmml/emit.js +65 -5
  245. package/dist/src/formats/xgmml/emit.js.map +1 -1
  246. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  247. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  248. package/dist/src/formats/xgmml/styles.js +277 -60
  249. package/dist/src/formats/xgmml/styles.js.map +1 -1
  250. package/dist/src/index.d.ts +4 -3
  251. package/dist/src/index.d.ts.map +1 -1
  252. package/dist/src/index.js +4 -3
  253. package/dist/src/index.js.map +1 -1
  254. package/dist/src/positions/convert.d.ts.map +1 -1
  255. package/dist/src/positions/convert.js +11 -1
  256. package/dist/src/positions/convert.js.map +1 -1
  257. package/dist/src/positions/extract.d.ts.map +1 -1
  258. package/dist/src/positions/extract.js +53 -1
  259. package/dist/src/positions/extract.js.map +1 -1
  260. package/dist/src/positions/index.d.ts +1 -1
  261. package/dist/src/positions/index.d.ts.map +1 -1
  262. package/dist/src/positions/types.d.ts +59 -0
  263. package/dist/src/positions/types.d.ts.map +1 -1
  264. package/dist/src/sources/builtins.d.ts +1 -1
  265. package/dist/src/sources/builtins.d.ts.map +1 -1
  266. package/dist/src/sources/builtins.js +8 -2
  267. package/dist/src/sources/builtins.js.map +1 -1
  268. package/dist/src/sources/context.d.ts.map +1 -1
  269. package/dist/src/sources/context.js +1 -0
  270. package/dist/src/sources/context.js.map +1 -1
  271. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  272. package/dist/src/sources/cypher/descriptor.js +5 -0
  273. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  274. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  275. package/dist/src/sources/cypher/index.js +124 -18
  276. package/dist/src/sources/cypher/index.js.map +1 -1
  277. package/dist/src/sources/cypher/params.d.ts +1 -1
  278. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  279. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  280. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  281. package/dist/src/sources/cypher/query-builder.js +31 -0
  282. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  283. package/dist/src/sources/ids.d.ts +5 -0
  284. package/dist/src/sources/ids.d.ts.map +1 -1
  285. package/dist/src/sources/ids.js +5 -0
  286. package/dist/src/sources/ids.js.map +1 -1
  287. package/dist/src/sources/params.d.ts +3 -0
  288. package/dist/src/sources/params.d.ts.map +1 -1
  289. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  290. package/dist/src/sources/rest/descriptor.js +5 -0
  291. package/dist/src/sources/rest/descriptor.js.map +1 -1
  292. package/dist/src/sources/rest/source.d.ts.map +1 -1
  293. package/dist/src/sources/rest/source.js +15 -0
  294. package/dist/src/sources/rest/source.js.map +1 -1
  295. package/dist/src/sources/sigv4.d.ts +5 -3
  296. package/dist/src/sources/sigv4.d.ts.map +1 -1
  297. package/dist/src/sources/sigv4.js +6 -4
  298. package/dist/src/sources/sigv4.js.map +1 -1
  299. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  300. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  301. package/dist/src/sources/sparql/descriptor.js +64 -0
  302. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  303. package/dist/src/sources/sparql/index.d.ts +28 -0
  304. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  305. package/dist/src/sources/sparql/index.js +314 -0
  306. package/dist/src/sources/sparql/index.js.map +1 -0
  307. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  308. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  309. package/dist/src/sources/sparql/ntriples.js +180 -0
  310. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  311. package/dist/src/sources/sparql/params.d.ts +27 -0
  312. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  313. package/dist/src/sources/sparql/params.js +5 -0
  314. package/dist/src/sources/sparql/params.js.map +1 -0
  315. package/dist/src/sources/sparql/query.d.ts +72 -0
  316. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  317. package/dist/src/sources/sparql/query.js +113 -0
  318. package/dist/src/sources/sparql/query.js.map +1 -0
  319. package/dist/src/sources/types.d.ts +31 -0
  320. package/dist/src/sources/types.d.ts.map +1 -1
  321. package/dist/src/styles/annotations.d.ts +61 -5
  322. package/dist/src/styles/annotations.d.ts.map +1 -1
  323. package/dist/src/styles/annotations.js +130 -22
  324. package/dist/src/styles/annotations.js.map +1 -1
  325. package/dist/src/styles/canon/bends.d.ts +46 -0
  326. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  327. package/dist/src/styles/canon/bends.js +123 -0
  328. package/dist/src/styles/canon/bends.js.map +1 -1
  329. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  330. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  331. package/dist/src/styles/canon/color-tables.js +10 -4
  332. package/dist/src/styles/canon/color-tables.js.map +1 -1
  333. package/dist/src/styles/canon/color.d.ts +3 -1
  334. package/dist/src/styles/canon/color.d.ts.map +1 -1
  335. package/dist/src/styles/canon/color.js +103 -3
  336. package/dist/src/styles/canon/color.js.map +1 -1
  337. package/dist/src/styles/canon/enums.d.ts +2 -2
  338. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  339. package/dist/src/styles/canon/enums.js +81 -7
  340. package/dist/src/styles/canon/enums.js.map +1 -1
  341. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  342. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  343. package/dist/src/styles/canon/graphviz.js +57 -9
  344. package/dist/src/styles/canon/graphviz.js.map +1 -1
  345. package/dist/src/styles/canon/number.d.ts +8 -0
  346. package/dist/src/styles/canon/number.d.ts.map +1 -1
  347. package/dist/src/styles/canon/number.js +18 -0
  348. package/dist/src/styles/canon/number.js.map +1 -1
  349. package/dist/src/styles/canon/text.d.ts +29 -2
  350. package/dist/src/styles/canon/text.d.ts.map +1 -1
  351. package/dist/src/styles/canon/text.js +156 -16
  352. package/dist/src/styles/canon/text.js.map +1 -1
  353. package/dist/src/styles/codes.d.ts +2 -0
  354. package/dist/src/styles/codes.d.ts.map +1 -1
  355. package/dist/src/styles/codes.js +2 -0
  356. package/dist/src/styles/codes.js.map +1 -1
  357. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  358. package/dist/src/styles/cyjs/cascade.js +1015 -79
  359. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  360. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  361. package/dist/src/styles/cyjs/defaults-table.js +3 -0
  362. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  363. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  364. package/dist/src/styles/cyjs/emit.js +202 -9
  365. package/dist/src/styles/cyjs/emit.js.map +1 -1
  366. package/dist/src/styles/cyjs/face.d.ts +20 -0
  367. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  368. package/dist/src/styles/cyjs/face.js +332 -0
  369. package/dist/src/styles/cyjs/face.js.map +1 -0
  370. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  371. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  372. package/dist/src/styles/cyjs/mappers.js +461 -26
  373. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  374. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  375. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  376. package/dist/src/styles/cyjs/routes.js +360 -0
  377. package/dist/src/styles/cyjs/routes.js.map +1 -0
  378. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  379. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  380. package/dist/src/styles/cytoscape/charts.js +560 -0
  381. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  382. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  383. package/dist/src/styles/cytoscape/emit.js +156 -48
  384. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  385. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  386. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  387. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  388. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  389. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  390. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  391. package/dist/src/styles/cytoscape/mappings.js +92 -0
  392. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  393. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  394. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  395. package/dist/src/styles/cytoscape/resolve.js +623 -17
  396. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  397. package/dist/src/styles/defaults.d.ts +46 -26
  398. package/dist/src/styles/defaults.d.ts.map +1 -1
  399. package/dist/src/styles/defaults.js +308 -71
  400. package/dist/src/styles/defaults.js.map +1 -1
  401. package/dist/src/styles/index.d.ts +4 -3
  402. package/dist/src/styles/index.d.ts.map +1 -1
  403. package/dist/src/styles/index.js +3 -2
  404. package/dist/src/styles/index.js.map +1 -1
  405. package/dist/src/styles/internal.d.ts +1 -0
  406. package/dist/src/styles/internal.d.ts.map +1 -1
  407. package/dist/src/styles/internal.js +1 -0
  408. package/dist/src/styles/internal.js.map +1 -1
  409. package/dist/src/styles/io/generic.d.ts.map +1 -1
  410. package/dist/src/styles/io/generic.js +56 -16
  411. package/dist/src/styles/io/generic.js.map +1 -1
  412. package/dist/src/styles/io/round-trip.d.ts +56 -0
  413. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  414. package/dist/src/styles/io/round-trip.js +138 -0
  415. package/dist/src/styles/io/round-trip.js.map +1 -0
  416. package/dist/src/styles/io/writer.d.ts.map +1 -1
  417. package/dist/src/styles/io/writer.js +6 -1
  418. package/dist/src/styles/io/writer.js.map +1 -1
  419. package/dist/src/styles/notes.d.ts.map +1 -1
  420. package/dist/src/styles/notes.js +1 -0
  421. package/dist/src/styles/notes.js.map +1 -1
  422. package/dist/src/styles/plan/features.d.ts +1 -1
  423. package/dist/src/styles/plan/features.d.ts.map +1 -1
  424. package/dist/src/styles/plan/features.js +22 -2
  425. package/dist/src/styles/plan/features.js.map +1 -1
  426. package/dist/src/styles/plan/index.d.ts +1 -1
  427. package/dist/src/styles/plan/index.d.ts.map +1 -1
  428. package/dist/src/styles/plan/index.js +1 -1
  429. package/dist/src/styles/plan/index.js.map +1 -1
  430. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  431. package/dist/src/styles/plan/plan.js +61 -4
  432. package/dist/src/styles/plan/plan.js.map +1 -1
  433. package/dist/src/styles/plan/resolve.d.ts +11 -0
  434. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  435. package/dist/src/styles/plan/resolve.js +40 -4
  436. package/dist/src/styles/plan/resolve.js.map +1 -1
  437. package/dist/src/styles/slots.d.ts +14 -0
  438. package/dist/src/styles/slots.d.ts.map +1 -0
  439. package/dist/src/styles/slots.js +20 -0
  440. package/dist/src/styles/slots.js.map +1 -0
  441. package/dist/src/styles/types.d.ts +622 -6
  442. package/dist/src/styles/types.d.ts.map +1 -1
  443. package/dist/src/styles/vocabulary.d.ts +3 -2
  444. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  445. package/dist/src/styles/vocabulary.js +203 -0
  446. package/dist/src/styles/vocabulary.js.map +1 -1
  447. package/dist/styles.js +30 -239
  448. package/dist/styles.js.map +1 -1
  449. package/dist/xgmml.js +3 -3
  450. package/package.json +27 -7
  451. package/src/formats/cx/importer.ts +6 -1
  452. package/src/formats/cx/styles.ts +143 -9
  453. package/src/formats/cx2/importer.ts +1 -0
  454. package/src/formats/cx2/styles.ts +3 -1
  455. package/src/formats/cys/importer.ts +1 -0
  456. package/src/formats/cys/styles.ts +27 -5
  457. package/src/formats/dot/drawing.ts +799 -0
  458. package/src/formats/dot/importer.ts +120 -35
  459. package/src/formats/dot/label-places.ts +228 -0
  460. package/src/formats/dot/record.ts +336 -0
  461. package/src/formats/dot/styles.ts +1654 -153
  462. package/src/formats/gexf/dialog-metrics.ts +61 -0
  463. package/src/formats/gexf/importer.ts +50 -5
  464. package/src/formats/gexf/preview.ts +217 -0
  465. package/src/formats/gexf/schema.ts +2 -1
  466. package/src/formats/gexf/styles.ts +1099 -245
  467. package/src/formats/gml/importer.ts +291 -76
  468. package/src/formats/gml/index.ts +1 -1
  469. package/src/formats/gml/positions.ts +29 -7
  470. package/src/formats/gml/styles.ts +851 -109
  471. package/src/formats/graphml/styles-plain.ts +6 -15
  472. package/src/formats/graphml/styles-yed.ts +762 -173
  473. package/src/formats/graphml/styles-yfiles3.ts +485 -63
  474. package/src/formats/graphml/yfiles.ts +19 -6
  475. package/src/formats/json/dialect.ts +41 -5
  476. package/src/formats/json/importer.ts +59 -7
  477. package/src/formats/json/index.ts +1 -0
  478. package/src/formats/json/positions.ts +22 -5
  479. package/src/formats/json/styles.ts +2041 -185
  480. package/src/formats/pajek/draw.ts +458 -0
  481. package/src/formats/pajek/exporter.ts +81 -49
  482. package/src/formats/pajek/importer.ts +278 -103
  483. package/src/formats/pajek/index.ts +1 -0
  484. package/src/formats/pajek/styles.ts +1381 -133
  485. package/src/formats/pajek/syntax.ts +27 -2
  486. package/src/formats/xgmml/emit.ts +73 -5
  487. package/src/formats/xgmml/styles.ts +330 -71
  488. package/src/index.ts +5 -1
  489. package/src/positions/convert.ts +12 -1
  490. package/src/positions/extract.ts +55 -1
  491. package/src/positions/index.ts +1 -0
  492. package/src/positions/types.ts +60 -0
  493. package/src/sources/builtins.ts +8 -1
  494. package/src/sources/context.ts +1 -0
  495. package/src/sources/cypher/descriptor.ts +7 -1
  496. package/src/sources/cypher/index.ts +148 -18
  497. package/src/sources/cypher/params.ts +1 -1
  498. package/src/sources/cypher/query-builder.ts +33 -0
  499. package/src/sources/ids.ts +6 -0
  500. package/src/sources/params.ts +3 -0
  501. package/src/sources/rest/descriptor.ts +5 -0
  502. package/src/sources/rest/source.ts +16 -0
  503. package/src/sources/sigv4.ts +6 -4
  504. package/src/sources/sparql/descriptor.ts +67 -0
  505. package/src/sources/sparql/index.ts +356 -0
  506. package/src/sources/sparql/ntriples.ts +219 -0
  507. package/src/sources/sparql/params.ts +28 -0
  508. package/src/sources/sparql/query.ts +162 -0
  509. package/src/sources/types.ts +35 -0
  510. package/src/styles/annotations.ts +201 -27
  511. package/src/styles/canon/bends.ts +137 -0
  512. package/src/styles/canon/color-tables.ts +10 -4
  513. package/src/styles/canon/color.ts +105 -3
  514. package/src/styles/canon/enums.ts +98 -8
  515. package/src/styles/canon/graphviz.ts +71 -10
  516. package/src/styles/canon/number.ts +19 -0
  517. package/src/styles/canon/text.ts +187 -16
  518. package/src/styles/codes.ts +2 -0
  519. package/src/styles/cyjs/cascade.ts +1160 -84
  520. package/src/styles/cyjs/defaults-table.ts +3 -0
  521. package/src/styles/cyjs/emit.ts +233 -13
  522. package/src/styles/cyjs/face.ts +372 -0
  523. package/src/styles/cyjs/mappers.ts +492 -27
  524. package/src/styles/cyjs/routes.ts +432 -0
  525. package/src/styles/cytoscape/charts.ts +684 -0
  526. package/src/styles/cytoscape/emit.ts +170 -48
  527. package/src/styles/cytoscape/lexicon.ts +431 -78
  528. package/src/styles/cytoscape/mappings.ts +108 -0
  529. package/src/styles/cytoscape/resolve.ts +741 -17
  530. package/src/styles/defaults.ts +417 -108
  531. package/src/styles/index.ts +5 -0
  532. package/src/styles/internal.ts +1 -0
  533. package/src/styles/io/generic.ts +79 -23
  534. package/src/styles/io/round-trip.ts +202 -0
  535. package/src/styles/io/writer.ts +6 -1
  536. package/src/styles/notes.ts +1 -0
  537. package/src/styles/plan/features.ts +23 -2
  538. package/src/styles/plan/index.ts +1 -1
  539. package/src/styles/plan/plan.ts +69 -5
  540. package/src/styles/plan/resolve.ts +47 -4
  541. package/src/styles/slots.ts +25 -0
  542. package/src/styles/types.ts +633 -5
  543. package/src/styles/vocabulary.ts +211 -2
  544. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  545. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  546. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  547. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  548. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  549. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  550. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  551. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  552. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  553. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  554. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  555. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  556. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  557. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  558. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  559. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  560. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  561. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  562. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  563. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  564. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  565. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  566. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  567. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  568. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  569. package/dist/chunks/writer-DvHTl36J.js +0 -449
  570. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -10,11 +10,13 @@ import { canonEnum } from "../../styles/canon/enums.js";
10
10
  import { canonNumber } from "../../styles/canon/number.js";
11
11
  import { canonText } from "../../styles/canon/text.js";
12
12
  import { StyleApproximations } from "../../styles/io/approximations.js";
13
- import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
13
+ import { STYLE_BLOCK_NAME } from "../../styles/io/block.js";
14
+ import { takeFromBlock } from "../../styles/io/round-trip.js";
14
15
  import { StyleNotes } from "../../styles/notes.js";
15
- import { styleFingerprint } from "../../styles/plan/features.js";
16
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
17
- import { NODE_COLUMNS, VIZ_NAMESPACE } from "./schema.js";
16
+ import { plainCell } from "../../styles/plan/resolve.js";
17
+ import { DIALOG_LINE_HEIGHT, dialogWidth } from "./dialog-metrics.js";
18
+ import { dependantColor, drawnLabels, edgeThickness, GephiPreview } from "./preview.js";
19
+ import { NODE_COLUMNS, TEMPORAL_COLUMNS, temporalTableName, VIZ_NAMESPACE } from "./schema.js";
18
20
  /** The column of a node's `label` attribute. */
19
21
  const LABEL_COLUMN = NODE_COLUMNS.label;
20
22
  /**
@@ -37,15 +39,18 @@ export const GEXF_STYLE_CAPABILITIES = Object.freeze({
37
39
  const GEPHI_NODE_FILL = "#000000";
38
40
  /** The style column marking directed edges, which Gephi draws with an arrow at the target. */
39
41
  const DIRECTED_KEY = "directed";
40
- /** The style column holding the mixed color of each edge without a color of its own. */
41
- const MIXED_COLOR_KEY = "mixed-color";
42
- /** The style columns of the border Gephi draws on each node without one of its own. */
43
- const BORDER_COLOR_KEY = "border-color";
44
- const BORDER_WIDTH_KEY = "border-width";
45
- /** The style column of the label size Gephi draws on each node, scaled with the node. */
46
- const LABEL_SIZE_KEY = "label-size";
47
- /** The style column of the width Gephi draws an edge with from its weight. */
48
- const WEIGHT_WIDTH_KEY = "weight-width";
42
+ /** The style column marking the nodes whose labels Gephi hides where they overlap (node.label.avoidOverlap). */
43
+ const AVOID_OVERLAP_KEY = "label-avoid-overlap";
44
+ /** The style column marking the edges Gephi does not draw (dropped, or merged into a parallel edge). */
45
+ const HIDDEN_KEY = "gephi-hidden";
46
+ /** The style column holding the mix of its end nodes' colors of each edge without a color of its own. */
47
+ const MIXED_COLOR_KEY = "end-colors";
48
+ /** The width property and the style column of the width Gephi draws an edge with from its weight. */
49
+ const WEIGHT_WIDTH = ["width", "weight-width"];
50
+ /** Gephi's node width when a node has no `viz:size` (graphstore NodeImpl size 10, a radius). */
51
+ const GEPHI_NODE_WIDTH = 20;
52
+ /** How much thicker than its weight Gephi draws a self-loop. */
53
+ const SELF_LOOP_THICKNESS = 1.3;
49
54
  /** GEXF's node shapes, by the neutral shape each writes exactly. */
50
55
  const NODE_SHAPES = new Map([
51
56
  ["ellipse", "disc"],
@@ -88,26 +93,39 @@ const VIZ_COLUMNS = {
88
93
  edge: ["color", "thickness", "shape"],
89
94
  };
90
95
  /**
91
- * A `viz:color` `hex` value as channels: r, g and b 0..255 and an alpha 0..1. Reads `#rgb`, `#rgba`, `#rrggbb` and
92
- * `#rrggbbaa` (alpha last), with or without the `#`.
96
+ * A `viz:color` `hex` value as channels: r, g and b 0..255 and an alpha 0..1, read the way Gephi reads it: Java's
97
+ * `Color.decode`, which parses the text as one integer (`Integer.decode`: `#` or `0x` hexadecimal, a leading `0`
98
+ * octal, otherwise decimal; a leading sign allowed) and keeps its low 24 bits, opaque. So `#rrggbb` is the color it
99
+ * names, but `#f80` is `#000f80`, `#4daf4a80` is `#af4a80` and `984ea3` is not a color at all (Gephi draws that node
100
+ * as if it had no color: `gexf-specimen-node-fills-borders`, `color-hex-short`, `color-hex-alpha`, `color-hex-nohash`).
93
101
  * @param hex - the attribute's value
94
- * @returns `[r, g, b, a]`, or null when it is not a hex color
102
+ * @returns `[r, g, b, 1]`, or null when Java cannot read it as an int
95
103
  */
96
104
  export function canonVizColor(hex) {
97
- const text = hex.trim();
98
- if (!/^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text)) {
105
+ const m = /^([+-]?)(?:(0x|0X|#)([0-9a-fA-F]+)|(0[0-7]+)|(\d+))$/.exec(hex.trim());
106
+ if (m === null) {
99
107
  return null;
100
108
  }
101
- const c = canonColor(text.startsWith("#") ? text : `#${text}`, "css", {
102
- notes: new StyleNotes(),
103
- target: "node",
104
- property: "fill",
105
- });
106
- if (c === null) {
109
+ const [, sign, , hexDigits, octal, decimal] = m;
110
+ let v;
111
+ if (hexDigits !== undefined) {
112
+ v = Number.parseInt(hexDigits, 16);
113
+ }
114
+ else if (octal !== undefined) {
115
+ v = Number.parseInt(octal, 8);
116
+ }
117
+ else {
118
+ v = Number.parseInt(decimal, 10);
119
+ }
120
+ if (sign === "-") {
121
+ v = -v;
122
+ }
123
+ // Integer.decode accepts -2^31 .. 2^31 - 1 only
124
+ if (v < -2147483648 || v > 2147483647) {
107
125
  return null;
108
126
  }
109
- const n = Number.parseInt(c.color.slice(1), 16);
110
- return [(n >> 16) & 255, (n >> 8) & 255, n & 255, c.alpha];
127
+ const n = v & 0xffffff;
128
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1];
111
129
  }
112
130
  /**
113
131
  * A viz column of the snapshot, or null when the column is absent or not viz-derived.
@@ -207,17 +225,23 @@ function readVizColor(viz, color, i) {
207
225
  }
208
226
  }
209
227
  /**
210
- * Reads one element's viz size (a node's width and height) or thickness (an edge's width).
228
+ * Reads one element's viz size (a node's width and height) or thickness (an edge's width). A node's
229
+ * size is a radius, as Gephi and sigma draw it (Gephi's preview: a circle of radius 0.92 x size plus
230
+ * a 0.16 x size outline, an outer diameter of 2 x size), so its width and height are twice it.
211
231
  * @param viz - the target's reading
212
232
  * @param size - the viz size or thickness column
213
233
  * @param i - the element
214
234
  */
215
235
  function readVizSize(viz, size, i) {
216
236
  const v = canonNumber(plainCell(viz.table.value(size, i)), viz.site(i, "width"));
217
- if (v !== null) {
218
- viz.column("width")[i] = v;
237
+ // Gephi draws a node of viz:size 0 at its default size 10 (gexf-specimen-node-sizes-opacity, size-0)
238
+ if (v !== null && !(v === 0 && viz.target === "node")) {
219
239
  if (viz.target === "node") {
220
- viz.column("height")[i] = v;
240
+ viz.column("width")[i] = 2 * v;
241
+ viz.column("height")[i] = 2 * v;
242
+ }
243
+ else {
244
+ viz.column("width")[i] = v;
221
245
  }
222
246
  }
223
247
  }
@@ -238,176 +262,730 @@ function readVizShape(viz, shape, i) {
238
262
  return typeof raw === "string" && raw.toLowerCase() === "image";
239
263
  }
240
264
  /**
241
- * Whether each edge is directed.
265
+ * The edges as Gephi's importer and preview draw them (measured with the Gephi Toolkit 0.11.3:
266
+ * `gexf-specimen-edges-lines`, `-p13`, `-p15`, `gexf-specimen-arrows`, `-p6`, `-p7`):
267
+ * - an edge whose own type is against the graph's `defaultedgetype` is dropped (`undirected` in a `directed`
268
+ * graph, `directed` in an `undirected` one); a `mutual` graph, or one with no `defaultedgetype`, keeps every
269
+ * edge, an untyped or `mutual` one directed;
270
+ * - edges of one kind between the same ends (either way round for an undirected edge) are merged into the first,
271
+ * whose weight is their sum;
272
+ * - an edge's weight is its `weight` (1 when it has none), or, when an edge attribute named weight is dynamic, its
273
+ * value over the earliest interval (0 when it has none);
274
+ * - when the smallest weight drawn is 0 or less, every weight is raised by |smallest| + 1.
275
+ * An undirected edge of a directed snapshot is two halves here; both take what its first half gets.
242
276
  * @param snapshot - the graph
243
- * @returns one flag per edge
277
+ * @param gexf - whether the file is a GEXF; another format keeps its own direction, with nothing merged or dropped
278
+ * @returns per edge, what Gephi draws
244
279
  */
245
- function directedEdges(snapshot) {
246
- const out = new Array(snapshot.edgeCount).fill(snapshot.directed);
247
- const col = snapshot.directed ? snapshot.edges.byRole("directed") : null;
248
- if (col !== null) {
249
- for (let e = 0; e < snapshot.edgeCount; e++) {
250
- out[e] = plainCell(col.value(e)) !== false;
280
+ function gephiEdges(snapshot, gexf) {
281
+ const n = snapshot.edgeCount;
282
+ const first = firstHalf(snapshot);
283
+ // another format Gephi draws: its own direction, nothing merged or dropped
284
+ if (!gexf) {
285
+ return otherFormatEdges(snapshot);
286
+ }
287
+ const header = gexfDefaultEdgeType(snapshot);
288
+ const dirCol = snapshot.edges.byRole("directed");
289
+ const mutualCol = snapshot.edges.byRole("mutual");
290
+ const kindCol = snapshot.edges.byRole("kind");
291
+ const weightOf = gephiWeight(snapshot);
292
+ const out = {
293
+ directed: new Array(n).fill(false),
294
+ hidden: new Array(n).fill(false),
295
+ weight: new Array(n).fill(1),
296
+ };
297
+ const leaders = new Map();
298
+ const drawnLeaders = [];
299
+ const ownDirection = gexfOwnDirection(header, dirCol, mutualCol);
300
+ for (let e = 0; e < n; e++) {
301
+ const f = first(e);
302
+ if (f !== e) {
303
+ continue;
304
+ }
305
+ // the edge's own type: graph-io records it only when the file mixes directions
306
+ const { ownUndirected, mismatched } = ownDirection(f);
307
+ if (mismatched) {
308
+ out.hidden[f] = true;
309
+ continue;
310
+ }
311
+ // Gephi draws no arrowhead on a self-loop
312
+ const s = snapshot.edgeSource(f);
313
+ const t = snapshot.edgeTarget(f);
314
+ const directed = header !== "undirected" && s !== t && !ownUndirected;
315
+ out.directed[f] = directed;
316
+ const kind = kindCol?.isSet(f) === true ? String(kindCol.value(f)) : "";
317
+ const key = `${kind}\u0000${gephiEndsKey(s, t, directed)}`;
318
+ const leader = leaders.get(key);
319
+ if (leader === undefined) {
320
+ leaders.set(key, f);
321
+ drawnLeaders.push(f);
322
+ out.weight[f] = weightOf(f);
323
+ }
324
+ else {
325
+ out.weight[leader] += weightOf(f);
326
+ out.hidden[f] = true;
251
327
  }
252
328
  }
329
+ // a loop, not Math.min(...): a spread of every edge overflows the call stack on a large graph
330
+ const smallest = drawnLeaders.reduce((m, f) => Math.min(m, out.weight[f]), Infinity);
331
+ const shift = smallest <= 0 ? 1 - smallest : 0;
332
+ for (let e = 0; e < n; e++) {
333
+ const f = first(e);
334
+ out.directed[e] = out.directed[f];
335
+ out.hidden[e] = out.hidden[f];
336
+ out.weight[e] = out.weight[f] + shift;
337
+ }
253
338
  return out;
254
339
  }
255
340
  /**
256
- * The rules Gephi's defaults make per element, as Gephi's Preview draws them (`DefaultPreset.java`; sources in
257
- * `styles/defaults.ts`): an arrow at the target of every directed edge; the mean of its two nodes' colors on every
258
- * edge without a color of its own (Preview's "mixed" edge color); the edge's weight as the thickness of every edge
259
- * without a thickness of its own; a border darker than its fill and scaled with its size on every node without a
260
- * border of its own; and each node's label, in a size scaled with the node (12 for a size-10 node) on every node
261
- * without a label size of its own (Gephi draws GEXF, its GraphML export and its GML the same way).
262
- *
263
- * Labels are a choice, not Gephi's default: Gephi hides them in both its drawings (`DefaultPreset.java` line 106,
264
- * `VizConfig.java` line 113), while sigma.js and gexf-js, which most published GEXF files are viewed with, show
265
- * them. graph-io shows them: a label is what the file states about a node, and hiding it is a view setting Gephi
266
- * offers as a switch, like the application's own; an application that wants Gephi's look hides them itself.
267
- *
268
- * An edge's own color is kept, though Preview's "mixed" mode draws over it: the file states it, and Gephi's
269
- * Overview, Preview's "original" mode, sigma.js and gexf-js all draw it.
341
+ * The ends of an edge as the key Gephi merges parallel edges by: in order for a directed edge, either way round
342
+ * for an undirected one.
343
+ * @param s - the source node index
344
+ * @param t - the target node index
345
+ * @param directed - whether the edge is drawn directed
346
+ * @returns the key
347
+ */
348
+ function gephiEndsKey(s, t, directed) {
349
+ return directed ? `${String(s)}>${String(t)}` : `${String(Math.min(s, t))}-${String(Math.max(s, t))}`;
350
+ }
351
+ /**
352
+ * A GEXF edge's own type, read from the directed and mutual columns, or from the header when the file did not
353
+ * mix directions (no directed column); an edge whose own type disagrees with the header's is one Gephi never loads.
354
+ * @param header - the file's default edge type
355
+ * @param dirCol - the directed column, or null
356
+ * @param mutualCol - the mutual column, or null
357
+ * @returns per edge index, whether its own type is undirected and whether it disagrees with the header
358
+ */
359
+ function gexfOwnDirection(header, dirCol, mutualCol) {
360
+ const verdict = (ownUndirected, ownDirected) => ({
361
+ ownUndirected,
362
+ mismatched: (header === "directed" && ownUndirected) || (header === "undirected" && ownDirected),
363
+ });
364
+ return (f) => {
365
+ if (dirCol === null) {
366
+ return verdict(header === "undirected", header !== "undirected");
367
+ }
368
+ const mutual = mutualCol?.value(f) === true;
369
+ return verdict(dirCol.value(f) === false && !mutual, dirCol.value(f) === true && !mutual);
370
+ };
371
+ }
372
+ /**
373
+ * The edges of a format other than GEXF as Gephi draws them: each in its own direction, as thick as its weight (1
374
+ * for a weight of 0 or below, so the edge stays visible).
375
+ * @param snapshot - the graph
376
+ * @returns per edge, what Gephi draws
377
+ */
378
+ function otherFormatEdges(snapshot) {
379
+ const first = firstHalf(snapshot);
380
+ const weightOf = gephiWeight(snapshot);
381
+ const directed = new Array(snapshot.edgeCount).fill(snapshot.directed);
382
+ const col = snapshot.directed ? snapshot.edges.byRole("directed") : null;
383
+ const weight = [];
384
+ for (let e = 0; e < snapshot.edgeCount; e++) {
385
+ if (col !== null) {
386
+ directed[e] = plainCell(col.value(e)) !== false;
387
+ }
388
+ const w = weightOf(first(e));
389
+ weight.push(w > 0 ? w : 1);
390
+ }
391
+ return { directed, hidden: directed.map(() => false), weight };
392
+ }
393
+ /**
394
+ * The graph's `defaultedgetype` as written, lower case, or null when it wrote none.
395
+ * @param snapshot - the graph
396
+ * @returns the keyword
397
+ */
398
+ function gexfDefaultEdgeType(snapshot) {
399
+ const written = snapshot.meta.extra.gexf?.defaultedgetype;
400
+ return typeof written === "string" ? written.trim().toLowerCase() : null;
401
+ }
402
+ /**
403
+ * Each edge's weight as Gephi reads it: an edge attribute whose id or title is `weight` and is dynamic is the weight,
404
+ * at its value over the earliest interval (0 for an edge without one: its static `weight` is not read); otherwise
405
+ * the edge's `weight`, 1 for an edge with none.
406
+ * @param snapshot - the graph
407
+ * @returns the weight of an edge
408
+ */
409
+ function gephiWeight(snapshot) {
410
+ const isWeight = (v) => typeof v === "string" && v.toLowerCase() === "weight";
411
+ const dynamic = snapshot.edges
412
+ .names()
413
+ .map((name) => snapshot.edges.get(name))
414
+ .find((c) => c !== null && c.meta.dynamic && [c.meta.name, c.meta.origin?.id, c.meta.origin?.title].some(isWeight));
415
+ const table = dynamic === undefined || dynamic === null
416
+ ? undefined
417
+ : snapshot.extensions.get(temporalTableName("edge", dynamic.meta.name));
418
+ if (table !== undefined) {
419
+ const element = table.get(TEMPORAL_COLUMNS.element);
420
+ const start = table.get(TEMPORAL_COLUMNS.start);
421
+ const value = table.get(TEMPORAL_COLUMNS.value);
422
+ const earliest = new Map();
423
+ for (let row = 0; element !== null && value !== null && row < table.rowCount; row++) {
424
+ const e = element.value(row);
425
+ const at = start?.isSet(row) === true ? start.value(row) : -Infinity;
426
+ const v = Number(value.value(row));
427
+ const seen = earliest.get(e);
428
+ if (Number.isFinite(v) && (seen === undefined || at < seen.start)) {
429
+ earliest.set(e, { start: at, value: v });
430
+ }
431
+ }
432
+ return (e) => earliest.get(e)?.value ?? 0;
433
+ }
434
+ // the snapshot's own weights (the role column exists only when some edge has none), as their shortest decimal
435
+ const weights = explicitWeights(snapshot);
436
+ return (e) => {
437
+ const w = Number(weights.text(e) ?? 1);
438
+ return Number.isFinite(w) ? w : 1;
439
+ };
440
+ }
441
+ /**
442
+ * A node color's channels as Gephi holds them: r, g, b and alpha, 0..255.
443
+ * @param fill - the node's fill, or null for Gephi's default
444
+ * @param opacity - its opacity, or null for opaque
445
+ * @returns the channels
446
+ */
447
+ function nodeChannels(fill, opacity) {
448
+ const n = Number.parseInt((typeof fill === "string" ? fill : GEPHI_NODE_FILL).slice(1), 16);
449
+ const a = typeof opacity === "number" ? Math.round(opacity * 255) : 255;
450
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255, a];
451
+ }
452
+ /**
453
+ * `#rrggbb` of three channels.
454
+ * @param c - r, g and b, 0..255
455
+ * @returns the color
456
+ */
457
+ function hexOf(c) {
458
+ return `#${c
459
+ .slice(0, 3)
460
+ .map((v) => v.toString(16).padStart(2, "0"))
461
+ .join("")}`;
462
+ }
463
+ /**
464
+ * A passthrough rule of a tool default that is itself per element, and its column, when any element takes it.
465
+ * @param out - the target's rules and columns, changed in place
466
+ * @param target - nodes or edges
467
+ * @param pair - the style property and its style column, which is also the rule's id after `<target>.tool.`
468
+ * @param column - the default per element, null where the element has its own value
469
+ * @param reason - why the values are per element
470
+ */
471
+ function perElementDefault(out, target, pair, column, reason) {
472
+ const [property, key] = pair;
473
+ const elements = column.filter((v) => v !== null).length;
474
+ if (elements === 0) {
475
+ return;
476
+ }
477
+ out.columns.set(key, column);
478
+ out.rules.push({
479
+ id: `${target}.tool.${key}`,
480
+ target,
481
+ role: "passthrough",
482
+ origin: "tool",
483
+ where: { has: { style: key } },
484
+ map: { [property]: { kind: "passthrough", by: { style: key } } },
485
+ facts: { elements, reason },
486
+ });
487
+ }
488
+ /**
489
+ * A tool rule that sets the same values on every element.
490
+ * @param out - the target's rules, changed in place
491
+ * @param target - nodes or edges
492
+ * @param id - the rule's id after `<target>.tool.`
493
+ * @param set - the values
494
+ */
495
+ function everyElement(out, target, id, set) {
496
+ if (Object.keys(set).length > 0) {
497
+ out.rules.push({
498
+ id: `${target}.tool.${id}`,
499
+ target,
500
+ role: "group",
501
+ origin: "tool",
502
+ where: { all: true },
503
+ set,
504
+ });
505
+ }
506
+ }
507
+ /**
508
+ * The rules Gephi's preview defaults (DefaultPreset) make per element, as its SVG export draws every GEXF and GraphML
509
+ * file: each node outlined in a darker shade of its fill, 0.08 of its width wide, with its label 0.3 of its width
510
+ * tall; each edge in the mix of its two end nodes' colors and alphas, as thick as its weight, with an arrow at the
511
+ * target of a directed edge. An element's own value is kept (a file graphty wrote states every one). A GEXF drawing
512
+ * also merges and drops edges as Gephi's importer does (gephiEdges()) and follows the Preview settings it was drawn
513
+ * with.
270
514
  * @param snapshot - the graph
271
- * @param node - the node values read from the file
272
- * @param edge - the edge values read from the file
515
+ * @param nodes - each node's fill, opacity and width
516
+ * @param edges - each edge's own color and width
273
517
  * @param budget - the work budget
518
+ * @param gexf - the GEXF drawing, or undefined for another format Gephi draws
274
519
  * @returns the node and edge rules and the style columns they read
275
520
  */
276
- export function gephiRules(snapshot, node, edge, budget) {
277
- return { node: gephiNodeRules(snapshot, node, budget), edge: gephiEdgeRules(snapshot, node, edge, budget) };
521
+ export function gephiRules(snapshot, nodes, edges, budget, gexf) {
522
+ const preview = gexf?.preview ?? new GephiPreview();
523
+ const node = { rules: [], columns: new Map() };
524
+ const edge = { rules: [], columns: new Map() };
525
+ budget.spend("node", snapshot.nodeCount);
526
+ budget.spend("edge", snapshot.edgeCount);
527
+ const channels = Array.from({ length: snapshot.nodeCount }, (_, i) => nodeChannels(nodes.fill?.[i], nodes.fillOpacity?.[i]));
528
+ const labelSizes = nodeRules(node, snapshot, channels, nodes, preview);
529
+ edgeRules(edge, snapshot, channels, nodes, edges, gexf, preview);
530
+ nodeLabelRules(node, channels, labelSizes, preview, gexf?.overlapSettled === true);
531
+ labelBox(node, channels, nodes, preview);
532
+ return { node, edge };
278
533
  }
279
- /** Preview's "darker" border: each channel of the fill times this (`DependantColor.java` line 55). */
280
- const GEPHI_BORDER_DARKEN = 0.498;
281
- /** Preview's border width as a share of the node's drawn width (`SizeUtils.java` lines 11, 37). */
282
- const GEPHI_BORDER_SHARE = 0.08;
283
- /** Gephi's node size when a node has none (`AbstractProcessor.java` lines 195-198). */
284
- const GEPHI_NODE_SIZE = 10;
285
534
  /**
286
- * Preview's label size per unit of node size: Arial 12 (`DefaultPreset.java` line 93) times the node drawn twice its
287
- * size wide times 0.05 (`NodeLabelRenderer.java` lines 165-173, `NODE_LABEL_PROPORTIONAL_SIZE` true), so 12 for a
288
- * size-10 node.
535
+ * A node's width, twice its Gephi size.
536
+ * @param nodes - the node values
537
+ * @param i - the node
538
+ * @returns the width
289
539
  */
290
- const GEPHI_LABEL_PER_SIZE = 1.2;
540
+ function nodeWidth(nodes, i) {
541
+ const w = nodes.width?.[i];
542
+ return typeof w === "number" ? w : GEPHI_NODE_WIDTH;
543
+ }
291
544
  /**
292
- * The node rules of Gephi's defaults: the border Preview draws, the label and its size scaled with the node.
545
+ * The node rules: opacity, border and label size.
546
+ * @param node - the node rules, changed in place
293
547
  * @param snapshot - the graph
294
- * @param node - the node values read from the file
295
- * @param budget - the work budget
296
- * @returns the rules and their columns
297
- */
298
- function gephiNodeRules(snapshot, node, budget) {
299
- const rules = [];
300
- const columns = new Map();
301
- const n = snapshot.nodeCount;
302
- budget.spend("node", n);
303
- // Preview draws a ring of the fill, each channel times 0.498 (rounded), 0.08 of the node's width wide
304
- const color = withoutOwn(node.borderColor, n, (i) => darker(node.fill?.[i] ?? GEPHI_NODE_FILL));
305
- const size = (i) => {
306
- const w = node.width?.[i];
307
- return typeof w === "number" ? w : GEPHI_NODE_SIZE;
308
- };
309
- const width = withoutOwn(node.borderWidth, n, (i) => round6(size(i) * GEPHI_BORDER_SHARE));
310
- // Preview scales each label with its node (NODE_LABEL_PROPORTIONAL_SIZE)
311
- const labelSize = withoutOwn(node.labelSize, n, (i) => round6(size(i) * GEPHI_LABEL_PER_SIZE));
312
- for (const [property, key, values, reason] of [
313
- ["borderColor", BORDER_COLOR_KEY, color, "FILL_BORDER"],
314
- ["borderWidth", BORDER_WIDTH_KEY, width, "FILL_BORDER"],
315
- ["labelSize", LABEL_SIZE_KEY, labelSize, "NODE_SIZE_LABEL"],
316
- ]) {
317
- const elements = values.filter((v) => v !== null).length;
318
- if (elements > 0) {
319
- columns.set(key, values);
320
- rules.push({
321
- id: `node.tool.${key}`,
322
- target: "node",
323
- role: "passthrough",
324
- origin: "tool",
325
- where: { has: { style: key } },
326
- map: { [property]: { kind: "passthrough", by: { style: key } } },
327
- facts: { elements, reason },
328
- });
329
- }
330
- }
331
- const label = snapshot.nodes.names().find((name) => snapshot.nodes.get(name)?.meta.role === "label");
332
- if (label !== undefined && node.label === undefined) {
333
- rules.push({
334
- id: "node.tool.label",
548
+ * @param channels - each node's color channels
549
+ * @param nodes - the node values
550
+ * @param preview - the settings
551
+ * @returns each node's label size
552
+ */
553
+ function nodeRules(node, snapshot, channels, nodes, preview) {
554
+ const nodeIndices = Array.from({ length: snapshot.nodeCount }, (_, i) => i);
555
+ // a node's alpha is drawn only under node.per.node.opacity, times node.opacity, on its fill and its border alike:
556
+ // a node not opaque is a fill of radius 0.84 s and a separate ring out to s, both at that alpha
557
+ const perNode = preview.on("node.per.node.opacity");
558
+ const alpha = channels.map((c) => ((perNode ? c[3] : 255) / 255) * (preview.num("node.opacity") / 100));
559
+ const translucent = alpha.map((a) => (a < 1 ? a : null));
560
+ perElementDefault(node, "node", ["fillOpacity", "preview-opacity"], translucent, "NODE_OPACITY");
561
+ perElementDefault(node, "node", ["borderOpacity", "preview-border-opacity"], translucent, "NODE_OPACITY");
562
+ // NodeRenderer: the border is DependantColor DARKER by default, half of each channel (+-1 from Gephi's float math)
563
+ const borderMode = preview.text("node.border.color");
564
+ perElementDefault(node, "node", ["borderColor", "border-shade"], channels.map((c) => hexOf(dependantColor(borderMode, c) ?? c.map((v) => Math.floor(v / 2)))), "NODE_FILL_SHADE");
565
+ // A node of size s is a circle of radius 0.92 s with a 0.16 s border: 0.08 of its 2 s width, or node.border.width
566
+ // under node.border.fixed. Its label is the label font's size in proportion to the node's, round(size / 20 * s)
567
+ // (NODE_LABEL_PROPORTIONAL_SIZE; Arial 12 makes round(0.6 s)), never under 1.
568
+ const fixed = preview.on("node.border.fixed") ? preview.num("node.border.width") : null;
569
+ perElementDefault(node, "node", ["borderWidth", "size-border"], nodeIndices.map((i) => {
570
+ // a fixed border is no wider than the node's size (radius): a size-3 node takes a border of 4 as 3
571
+ return fixed === null ? 0.08 * nodeWidth(nodes, i) : Math.min(fixed, nodeWidth(nodes, i) / 2);
572
+ }), "NODE_SIZE");
573
+ const font = preview.font("node");
574
+ const labelSizes = nodeIndices.map((i) => preview.on("node.label.proportinalSize")
575
+ ? Math.max(1, Math.round((font.size / 40) * nodeWidth(nodes, i)))
576
+ : font.size);
577
+ perElementDefault(node, "node", ["labelSize", "size-label"], labelSizes, "NODE_SIZE");
578
+ return labelSizes;
579
+ }
580
+ /**
581
+ * The node label rules the Preview settings make beyond the size: color, outline, font and overlap avoidance.
582
+ * @param node - the node rules, changed in place
583
+ * @param channels - each node's color channels
584
+ * @param sizes - each node's label size
585
+ * @param preview - the settings
586
+ * @param settled - whether the labels overlap avoidance hides are already left out
587
+ */
588
+ function nodeLabelRules(node, channels, sizes, preview, settled) {
589
+ const color = preview.text("node.label.color");
590
+ if (color !== "#000000" && color !== "original") {
591
+ perElementDefault(node, "node", ["labelColor", "preview-label-color"], channels.map((c) => hexOf(dependantColor(color, c) ?? [0, 0, 0])), "NODE_LABEL_COLOR");
592
+ }
593
+ labelOutline(node, "node", channels, sizes, preview);
594
+ const font = preview.font("node");
595
+ everyElement(node, "node", "preview-label", fontValues(font, preview.on("node.label.customFont")));
596
+ // ponytail: where a node has no stored position Gephi's own grid (gephiOverlapHidden()) cannot be run; avoiding
597
+ // overlap, larger first, is that test without the grid's rounding. A rule only while it is on: graphty-element
598
+ // has no value that turns a lower rule's overlap avoidance off again.
599
+ if (preview.on("node.label.avoidOverlap") && !settled) {
600
+ node.columns.set(AVOID_OVERLAP_KEY, sizes.map(() => true));
601
+ node.rules.push({
602
+ id: "node.tool.label-avoid-overlap",
335
603
  target: "node",
336
- role: "passthrough",
604
+ role: "group",
337
605
  origin: "tool",
338
- where: { has: { data: label } },
339
- map: { label: { kind: "passthrough", by: { data: label } } },
340
- facts: { reason: "NODE_NAME" },
606
+ where: { has: { style: AVOID_OVERLAP_KEY } },
607
+ set: { labelAvoidOverlap: true },
341
608
  });
342
609
  }
343
- return { rules, columns };
344
610
  }
345
611
  /**
346
- * The edge rules of Gephi's defaults: the target arrow of a directed edge, the mixed color and the weight's width.
612
+ * Gephi's alpha for a Preview opacity in percent: Java's `Color` keeps it as one of 256 steps, cut down to a whole
613
+ * step (`labels-p10`'s 30 is drawn 76 / 255, not 77).
614
+ * @param percent - the opacity, 0 to 100
615
+ * @returns the alpha, 0 to 1
616
+ */
617
+ function gephiAlpha(percent) {
618
+ // ponytail: 1e-9 keeps a step that floating point lands a hair under (0.4 x 255) on its step
619
+ return Math.floor((Math.min(Math.max(percent, 0), 100) / 100) * 255 + 1e-9) / 255;
620
+ }
621
+ /**
622
+ * The label box of `node.label.box`: Gephi strokes an unfilled box round the label, 0.08 of the node's size wide, in
623
+ * the color `node.label.box.color` derives from the node's and at `node.label.box.opacity` (`gexf-specimen-labels-p11`,
624
+ * `-p12`, `-p13`, `-p20`, `-p21`). The box is the text's own bounds -- its advance width and its font's ascent plus
625
+ * descent, with no leading -- grown by the stroke's width, so the stroke's inner edge lies on the text's bounds
626
+ * (`labels-p11`: a 15 px "Label" is 40.47 x 17.46 inside a 2.0 stroke): no padding, a line height of 1.
627
+ * @param node - the node rules, changed in place
628
+ * @param channels - each node's color channels
629
+ * @param nodes - the node values
630
+ * @param preview - the settings
631
+ */
632
+ function labelBox(node, channels, nodes, preview) {
633
+ if (!preview.on("node.label.box")) {
634
+ return;
635
+ }
636
+ const mode = preview.text("node.label.box.color");
637
+ perElementDefault(node, "node", ["labelBorderColor", "preview-box-color"], channels.map((c) => hexOf(dependantColor(mode, c) ?? c)), "LABEL_BOX");
638
+ perElementDefault(node, "node", ["labelBorderWidth", "preview-box-width"], channels.map((_, i) => 0.04 * nodeWidth(nodes, i)), "LABEL_BOX");
639
+ everyElement(node, "node", "preview-box", {
640
+ labelBorderOpacity: gephiAlpha(preview.num("node.label.box.opacity")),
641
+ labelPadding: 0,
642
+ labelLineHeight: 1,
643
+ });
644
+ }
645
+ /**
646
+ * The label font values a custom font sets.
647
+ * @param font - the font
648
+ * @param custom - whether the font is custom
649
+ * @returns the values
650
+ */
651
+ function fontValues(font, custom) {
652
+ if (!custom) {
653
+ return {};
654
+ }
655
+ return {
656
+ labelFont: font.family,
657
+ ...(font.bold ? { labelWeight: "bold" } : {}),
658
+ ...(font.italic ? { labelItalic: true } : {}),
659
+ };
660
+ }
661
+ /**
662
+ * The label outline of `<target>.label.outline.size` and `.color`: a stroke size / 32 of the label's size wide, round
663
+ * each letter, in the color its mode derives from the element's color and at `.outline.opacity` (Gephi's default 80;
664
+ * `gexf-specimen-labels-p9`, `-p10`, `-p17` .. `-p19`, `edge-labels-p5`, `-p6`).
665
+ * @param out - the rules, changed in place
666
+ * @param target - nodes or edges
667
+ * @param colors - each element's color channels
668
+ * @param sizes - each element's label size
669
+ * @param preview - the settings
670
+ */
671
+ function labelOutline(out, target, colors, sizes, preview) {
672
+ const size = preview.num(`${target}.label.outline.size`);
673
+ if (size <= 0) {
674
+ return;
675
+ }
676
+ const mode = preview.text(`${target}.label.outline.color`);
677
+ perElementDefault(out, target, ["labelOutlineWidth", "preview-outline-width"], sizes.map((s) => (size / 32) * s), "LABEL_OUTLINE");
678
+ perElementDefault(out, target, ["labelOutlineColor", "preview-outline-color"], colors.map((c) => hexOf(dependantColor(mode, c) ?? [255, 255, 255])), "LABEL_OUTLINE");
679
+ everyElement(out, target, "preview-outline", {
680
+ labelOutlineOpacity: gephiAlpha(preview.num(`${target}.label.outline.opacity`)),
681
+ });
682
+ }
683
+ /**
684
+ * The node labels Gephi's label overlap avoidance hides (`node.label.avoidOverlap`, Gephi 0.11
685
+ * NodeLabelRenderer.preProcess): largest font first, each label's box -- its Dialog advance width by its line height,
686
+ * centered on its node -- marks the cells of a `node.label.overlapGridSize` grid over the nodes' bounds, and a label
687
+ * any of whose cells an earlier one marked is hidden. A label's font size is 12 x 0.05 x its node's size (proportional
688
+ * size) or the font's, measured in the float size of the last label of the same whole size (Gephi's font cache),
689
+ * its labels taken largest node first.
690
+ * Decided where every node has a stored position; elsewhere graphty-element lays the graph out, so null.
347
691
  * @param snapshot - the graph
348
- * @param node - the node values read from the file
349
- * @param edge - the edge values read from the file
350
- * @param budget - the work budget
351
- * @returns the rules and their columns
352
- */
353
- function gephiEdgeRules(snapshot, node, edge, budget) {
354
- const rules = [];
355
- const columns = new Map();
356
- const directed = directedEdges(snapshot);
357
- if (directed.some(Boolean)) {
358
- columns.set(DIRECTED_KEY, directed.map((d) => (d ? true : null)));
359
- rules.push({
692
+ * @param labels - each node's label as drawn
693
+ * @param widths - each node's width (twice its size), or undefined for Gephi's default
694
+ * @param preview - the settings
695
+ * @returns whether each label is hidden, or null
696
+ */
697
+ function gephiOverlapHidden(snapshot, labels, widths, preview) {
698
+ const centers = gephiCenters(snapshot);
699
+ if (centers.includes(null)) {
700
+ return null;
701
+ }
702
+ const f = Math.fround;
703
+ const grid = preview.num("node.label.overlapGridSize");
704
+ const proportional = preview.on("node.label.proportinalSize");
705
+ const base = f(preview.font("node").size * preview.num("node.label.scale"));
706
+ const nodeSizes = centers.map((_, i) => {
707
+ const w = widths?.[i];
708
+ return f((typeof w === "number" ? w : GEPHI_NODE_WIDTH) / 2);
709
+ });
710
+ const sizes = nodeSizes.map((nodeSize) => proportional ? f(f(base * nodeSize) * f(0.05)) : f(base * f(1 + f(nodeSize * f(0.00001)))));
711
+ // Gephi's label items come largest node first (file order among equals; measured on six corpus files)
712
+ const items = sizes.map((_, i) => i).sort((a, b) => nodeSizes[b] - nodeSizes[a]);
713
+ // Font.deriveFont(size).getSize() rounds; the cache keeps the last font of each whole size
714
+ const cached = new Map();
715
+ items.forEach((i) => cached.set(Math.round(sizes[i]), sizes[i]));
716
+ // NodeItem Y is the file's y turned over
717
+ const at = centers.map((c) => [c?.[0] ?? 0, -(c?.[1] ?? 0)]);
718
+ // loops, not Math.min(...): a spread of every node overflows the call stack on a large graph
719
+ let [loX, loY, hiX, hiY] = [Infinity, Infinity, -Infinity, -Infinity];
720
+ for (const [x, y] of at) {
721
+ [loX, loY, hiX, hiY] = [Math.min(loX, x), Math.min(loY, y), Math.max(hiX, x), Math.max(hiY, y)];
722
+ }
723
+ const [minX, minY] = [loX - grid, loY - grid];
724
+ const cols = Math.ceil((hiX + grid - minX) / grid);
725
+ const rows = Math.ceil((hiY + grid - minY) / grid);
726
+ const taken = new Set();
727
+ const hidden = labels.map(() => false);
728
+ const order = [...items].sort((a, b) => sizes[b] - sizes[a]);
729
+ for (const i of order) {
730
+ const label = labels[i];
731
+ if (typeof label !== "string" || label.trim() === "") {
732
+ continue;
733
+ }
734
+ const size = cached.get(Math.round(sizes[i])) ?? sizes[i];
735
+ const half = [(dialogWidth(label) * size) / 2, (DIALOG_LINE_HEIGHT * size) / 2];
736
+ const cells = gridCells({ min: [minX, minY], size: grid, cols, rows }, [at[i][0] - half[0], at[i][1] - half[1]], [at[i][0] + half[0], at[i][1] + half[1]]);
737
+ if (cells.some((k) => taken.has(k))) {
738
+ hidden[i] = true;
739
+ }
740
+ else {
741
+ cells.forEach((k) => taken.add(k));
742
+ }
743
+ }
744
+ return hidden;
745
+ }
746
+ /**
747
+ * The cells of a grid a box covers, as Gephi's label grid counts them (each corner's cell truncated, kept on the grid).
748
+ * @param grid - the grid: its lowest corner, cell size and cell counts
749
+ * @param grid.min - its lowest x and y
750
+ * @param grid.size - its cell size
751
+ * @param grid.cols - its columns
752
+ * @param grid.rows - its rows
753
+ * @param lo - the box's lowest x and y
754
+ * @param hi - the box's highest x and y
755
+ * @returns the cell indices, row by row
756
+ */
757
+ function gridCells(grid, lo, hi) {
758
+ const cell = (v, axis) => Math.min((axis === 0 ? grid.cols : grid.rows) - 1, Math.max(0, Math.trunc((v - grid.min[axis]) / grid.size)));
759
+ const cells = [];
760
+ for (let r = cell(lo[1], 1); r <= cell(hi[1], 1); r++) {
761
+ for (let c = cell(lo[0], 0); c <= cell(hi[0], 0); c++) {
762
+ cells.push(r * grid.cols + c);
763
+ }
764
+ }
765
+ return cells;
766
+ }
767
+ /** Gephi's `edge.arc-curveness`: a curved edge is an arc of radius its nodes' distance over it (code only, 1.2). */
768
+ const GEPHI_ARC_CURVENESS = 1.2;
769
+ /**
770
+ * Each node's stored center, its viz:position; null without one (graphty-element lays such a node out, where Gephi
771
+ * would put it at (0, 0)).
772
+ * @param snapshot - the graph
773
+ * @returns x and y by node
774
+ */
775
+ function gephiCenters(snapshot) {
776
+ const position = snapshot.nodes.byRole("position");
777
+ return Array.from({ length: snapshot.nodeCount }, (_, i) => {
778
+ const v = position?.isSet(i) === true ? position.value(i) : null;
779
+ return v === null ? null : [v[0] || 0, v[1] || 0];
780
+ });
781
+ }
782
+ /**
783
+ * The curved edges Gephi draws no line for: its arc (radius distance / 1.2) is cut back at each end by a chord as long
784
+ * as that end's node radius (its size), plus `edge.radius`, plus at the target of a directed edge its arrowhead
785
+ * (`arrow.size` x its thickness); where the two cuts meet or pass each other, or a cut is longer than the arc, the
786
+ * nodes swallow the edge and no line is drawn (Gephi 0.11 EdgeRenderer.CurvedEdgeRenderer.Helper,
787
+ * computeTruncateAngle; gexf-cns-wos: 24 edges between overlapping nodes). Its arrowhead is still drawn, unless its
788
+ * nodes are at one place (ArrowRenderer).
789
+ * @param snapshot - the graph
790
+ * @param nodes - the node values (their widths)
791
+ * @param directed - whether each edge is drawn directed
792
+ * @param thickness - each edge's drawn thickness
793
+ * @param preview - the settings
794
+ * @returns for each edge, "line" when only its line is swallowed, "all" when its nodes are at one place, else null
795
+ */
796
+ function swallowedEdges(snapshot, nodes, directed, thickness, preview) {
797
+ const centers = gephiCenters(snapshot);
798
+ const edgeRadius = preview.num("edge.radius");
799
+ const arrowSize = preview.num("arrow.size");
800
+ const arc = 2 * Math.asin(Math.min(1, GEPHI_ARC_CURVENESS / 2));
801
+ return Array.from({ length: snapshot.edgeCount }, (_, e) => {
802
+ const [a, b] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
803
+ const [pa, pb] = [centers[a], centers[b]];
804
+ if (a === b || pa === null || pb === null) {
805
+ return null;
806
+ }
807
+ const d = Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
808
+ if (d === 0) {
809
+ return "all";
810
+ }
811
+ const r = d / GEPHI_ARC_CURVENESS;
812
+ // the angle a cut of chord length c takes off the arc; past the arc's length, more than the whole arc
813
+ const cut = (c) => c >= r * arc ? Infinity : 2 * Math.atan2(c / 2, Math.sqrt(r * r - (c / 2) * (c / 2)));
814
+ const source = edgeRadius + nodeWidth(nodes, a) / 2;
815
+ const target = edgeRadius + nodeWidth(nodes, b) / 2 + (directed[e] ? arrowSize * thickness[e] : 0);
816
+ return cut(source) + cut(target) >= arc ? "line" : null;
817
+ });
818
+ }
819
+ /** How many points a Gephi self-loop's circle is drawn through. */
820
+ const SELF_LOOP_STEPS = 96;
821
+ /**
822
+ * Gephi's self-loops as bends round their node: a circle up and to the right of it, centered `r` right of and `r`
823
+ * above the node's center, of radius `r` = half the node's size (a radius) plus a third of the loop's stroke, which is
824
+ * 1.3 times the edge's weight (gephi PreviewPlugin EdgeRenderer, SelfLoopEdgeRenderer; measured on the GEXF specimen
825
+ * sheets and gexf-graphology-rio: r = 0.5435 x the drawn radius + 0.33 x the stroke). The circle runs from the side
826
+ * facing the node all the way round; its part inside the node is not drawn.
827
+ * @param snapshot - the graph
828
+ * @param thickness - each edge's drawn thickness
829
+ * @param hidden - whether each edge is not drawn
830
+ * @returns each loop's bends, as fractions of its node's half size; null for any other edge
831
+ */
832
+ function selfLoopBends(snapshot, thickness, hidden) {
833
+ const sizes = vizColumn(snapshot, "node", "size");
834
+ const sizeOf = (n) => {
835
+ const v = sizes === null ? null : Number(plainCell(snapshot.nodes.value(sizes, n)));
836
+ return v !== null && Number.isFinite(v) && v > 0 ? v : GEPHI_NODE_WIDTH / 2;
837
+ };
838
+ const fraction = (v) => String(Math.round(v * 1e4) / 1e4);
839
+ return Array.from({ length: snapshot.edgeCount }, (_, e) => {
840
+ const n = snapshot.edgeSource(e);
841
+ if (n !== snapshot.edgeTarget(e) || hidden[e]) {
842
+ return null;
843
+ }
844
+ const size = sizeOf(n);
845
+ const r = (0.5 * size + 0.33 * SELF_LOOP_THICKNESS * thickness[e]) / size;
846
+ const points = [];
847
+ for (let k = 0; k <= SELF_LOOP_STEPS; k++) {
848
+ // from the point facing the node's center (225 degrees), clockwise all the way round
849
+ const a = (5 * Math.PI) / 4 - (2 * Math.PI * k) / SELF_LOOP_STEPS;
850
+ points.push(`${fraction(r + r * Math.cos(a))},${fraction(r + r * Math.sin(a))}`);
851
+ }
852
+ return points.join(" ");
853
+ });
854
+ }
855
+ /**
856
+ * The edge rules: arrows, thickness, the edges not drawn, color and opacity, curves and labels.
857
+ * @param edge - the edge rules, changed in place
858
+ * @param snapshot - the graph
859
+ * @param channels - each node's color channels
860
+ * @param nodes - the node values (their widths)
861
+ * @param edges - each edge's own color and width
862
+ * @param gexf - the GEXF drawing, or undefined
863
+ * @param preview - the settings
864
+ */
865
+ function edgeRules(edge, snapshot, channels, nodes, edges, gexf, preview) {
866
+ const drawn = gephiEdges(snapshot, gexf !== undefined);
867
+ const arrowSize = preview.num("arrow.size");
868
+ if (drawn.directed.some(Boolean) && arrowSize > 0) {
869
+ edge.columns.set(DIRECTED_KEY, drawn.directed.map((d) => (d ? true : null)));
870
+ // arrow.size scales the arrowhead from its default 3 (gexf-specimen-arrows-p2); 0 draws none (-p3)
871
+ edge.rules.push({
360
872
  id: "edge.tool.arrow",
361
873
  target: "edge",
362
874
  role: "group",
363
875
  origin: "tool",
364
876
  where: { has: { style: DIRECTED_KEY } },
365
- set: { targetArrow: "triangle" },
877
+ set: { targetArrow: "triangle", ...(arrowSize === 3 ? {} : { arrowSize: arrowSize / 3 }) },
366
878
  });
367
879
  }
368
- // Preview draws an edge in the mean of its two nodes' colors. graphty-element cannot bind an edge's color to its
369
- // nodes' drawn colors yet, so the color is frozen here; replace this column with that binding once the element
370
- // has it (tracking issue "graphty-element: bind an edge's color to its source node's drawn color").
371
- const fill = (i) => node.fill?.[i] ?? GEPHI_NODE_FILL;
372
- const mixed = withoutOwn(edge.color, snapshot.edgeCount, (e) => mix(fill(snapshot.edgeSource(e)), fill(snapshot.edgeTarget(e))));
373
- const uncolored = mixed.filter((v) => v !== null).length;
374
- budget.spend("edge", snapshot.edgeCount);
375
- // Gephi draws an edge as thick as its weight (1 for an edge with none); a width of its own is the file's
376
- // The weights come from the snapshot's own weights (the role column exists only when some edge has none), read
377
- // as the shortest decimal of their dtype. A weight of 0 or below is drawn 1 wide, so the edge stays visible.
378
- // An undirected edge in a directed graph is one edge in the file and two halves here: both take the first's.
379
- const first = firstHalf(snapshot);
380
- const weights = explicitWeights(snapshot);
381
- const weightWidth = withoutOwn(edge.width, snapshot.edgeCount, (e) => {
382
- const w = Number(weights.text(first(e)) ?? 1);
383
- return Number.isFinite(w) && w > 0 ? w : 1;
880
+ // Gephi draws an edge as thick as its weight (gephiEdges(): merged and shifted); a width of its own is the file's
881
+ const hidden = preview.on("edge.show") ? drawn.hidden : drawn.hidden.map(() => true);
882
+ const thickness = edgeThickness(drawn.weight, hidden.map((h) => !h), preview);
883
+ perElementDefault(edge, "edge", WEIGHT_WIDTH,
884
+ // Gephi's self-loop is 1.3 times as thick (measured: weight 1 draws 1.3, 3.25 draws 4.225)
885
+ withoutOwn(edges.width, snapshot.edgeCount, (e) => snapshot.edgeSource(e) === snapshot.edgeTarget(e) ? SELF_LOOP_THICKNESS * thickness[e] : thickness[e]), "EDGE_WEIGHT");
886
+ perElementDefault(edge, "edge", ["bends", "self-loop"], selfLoopBends(snapshot, thickness, hidden), "NODE_SIZE");
887
+ // a curved edge its nodes (and its arrowhead) swallow draws no line; its arrowhead is still drawn
888
+ const swallowed = preview.on("edge.curved")
889
+ ? swallowedEdges(snapshot, nodes, drawn.directed, thickness, preview)
890
+ : hidden.map(() => null);
891
+ // the edges Gephi does not draw: one of another direction than the graph's, one merged into a parallel edge, or a
892
+ // swallowed one with no arrowhead to keep
893
+ hidden.forEach((h, e) => {
894
+ const arrow = drawn.directed[e] && arrowSize > 0 && swallowed[e] === "line";
895
+ hidden[e] = h || (swallowed[e] !== null && !arrow);
384
896
  });
385
- const unsized = weightWidth.filter((v) => v !== null).length;
386
- if (unsized > 0) {
387
- columns.set(WEIGHT_WIDTH_KEY, weightWidth);
388
- rules.push({
389
- id: "edge.tool.weight-width",
897
+ if (hidden.some(Boolean)) {
898
+ edge.columns.set(HIDDEN_KEY, hidden.map((h) => (h ? true : null)));
899
+ edge.rules.push({
900
+ id: "edge.tool.gephi-hidden",
390
901
  target: "edge",
391
- role: "passthrough",
902
+ role: "group",
392
903
  origin: "tool",
393
- where: { has: { style: WEIGHT_WIDTH_KEY } },
394
- map: { width: { kind: "passthrough", by: { style: WEIGHT_WIDTH_KEY } } },
395
- facts: { elements: unsized, reason: "EDGE_WEIGHT" },
904
+ where: { has: { style: HIDDEN_KEY } },
905
+ set: { visible: false },
396
906
  });
397
907
  }
398
- if (uncolored > 0) {
399
- columns.set(MIXED_COLOR_KEY, mixed);
400
- rules.push({
401
- id: "edge.tool.mixed-color",
402
- target: "edge",
403
- role: "passthrough",
404
- origin: "tool",
405
- where: { has: { style: MIXED_COLOR_KEY } },
406
- map: { color: { kind: "passthrough", by: { style: MIXED_COLOR_KEY } } },
407
- facts: { elements: uncolored, reason: "END_NODES_COLOR" },
408
- });
908
+ const colors = edgeColors(snapshot, channels, gexf, preview);
909
+ perElementDefault(edge, "edge", ["color", MIXED_COLOR_KEY], withoutOwn(edges.color, snapshot.edgeCount, (e) => hexOf(colors[e])), "END_NODE_COLORS");
910
+ perElementDefault(edge, "edge", ["opacity", "end-opacity"], withoutOwn(edges.color, snapshot.edgeCount, (e) => colors[e][3] / 255).map((v, e) => {
911
+ // a swallowed edge Gephi draws only the arrowhead of draws no line
912
+ if (swallowed[e] !== null && !hidden[e]) {
913
+ return 0;
914
+ }
915
+ return v === 1 ? null : v;
916
+ }), "END_NODE_COLORS");
917
+ everyElement(edge, "edge", "preview-curve", preview.on("edge.curved") ? {} : { curved: false });
918
+ // Gephi's `edge.radius` leaves that much empty between each node and its edges' ends, arrowheads too
919
+ const nodeGap = preview.num("edge.radius");
920
+ everyElement(edge, "edge", "preview-radius", nodeGap > 0 ? { nodeGap } : {});
921
+ edgeLabelRules(edge, colors, gexf, preview);
922
+ }
923
+ /**
924
+ * Each edge's color and alpha (0..255) under `edge.color` and `edge.opacity`: MIXED (the default) is each channel,
925
+ * alpha too, the truncated mean of the two end nodes'; `source` and `target` are that node's; `original` is the edge's
926
+ * own viz color, black without one; `#rrggbb` is that color, opaque (`gexf-specimen-edges-lines-p3` .. `-p7`).
927
+ * graphty-element cannot bind an edge's color to its nodes' drawn colors, so the color is frozen here.
928
+ * @param snapshot - the graph
929
+ * @param channels - each node's color channels
930
+ * @param gexf - the GEXF drawing, or undefined
931
+ * @param preview - the settings
932
+ * @returns each edge's r, g, b and alpha
933
+ */
934
+ function edgeColors(snapshot, channels, gexf, preview) {
935
+ const mode = preview.text("edge.color");
936
+ const factor = preview.num("edge.opacity") / 100;
937
+ const own = (e) => {
938
+ const c = gexf?.original?.color?.[e];
939
+ const a = gexf?.original?.opacity?.[e];
940
+ return nodeChannels(typeof c === "string" ? c : GEPHI_NODE_FILL, typeof a === "number" ? a : null);
941
+ };
942
+ const custom = dependantColor(mode, [0, 0, 0]);
943
+ return Array.from({ length: snapshot.edgeCount }, (_, e) => {
944
+ const s = channels[snapshot.edgeSource(e)];
945
+ const t = channels[snapshot.edgeTarget(e)];
946
+ let c;
947
+ if (mode === "source") {
948
+ c = [...s];
949
+ }
950
+ else if (mode === "target") {
951
+ c = [...t];
952
+ }
953
+ else if (mode === "original") {
954
+ c = own(e);
955
+ }
956
+ else if (mode.startsWith("#") && custom !== null) {
957
+ c = [...custom, 255];
958
+ }
959
+ else {
960
+ c = s.map((v, k) => Math.floor((v + t[k]) / 2));
961
+ }
962
+ return factor === 1 ? c : [c[0], c[1], c[2], c[3] * factor];
963
+ });
964
+ }
965
+ /**
966
+ * The edge label rules of `edge.label.show`: the font's size (not in proportion to anything), color, outline and
967
+ * font (`gexf-specimen-edge-labels-p2` .. `-p15`).
968
+ * @param edge - the edge rules, changed in place
969
+ * @param colors - each edge's color and alpha
970
+ * @param gexf - the GEXF drawing, or undefined
971
+ * @param preview - the settings
972
+ */
973
+ function edgeLabelRules(edge, colors, gexf, preview) {
974
+ if (gexf?.edgeLabels === undefined || !preview.on("edge.label.show")) {
975
+ return;
409
976
  }
410
- return { rules, columns };
977
+ const font = preview.font("edge");
978
+ const color = preview.text("edge.label.color");
979
+ if (color !== "original") {
980
+ perElementDefault(edge, "edge", ["labelColor", "preview-label-color"], colors.map((c) => hexOf(dependantColor(color, c) ?? [0, 0, 0])), "EDGE_LABEL_COLOR");
981
+ }
982
+ labelOutline(edge, "edge", colors, colors.map(() => font.size), preview);
983
+ // Gephi centers an edge label on its line's middle (gexf-specimen-edge-labels-p2: 0.04 of the font off it)
984
+ everyElement(edge, "edge", "preview-label", {
985
+ labelSize: font.size,
986
+ labelPosition: "center",
987
+ ...fontValues(font, preview.on("edge.label.customFont")),
988
+ });
411
989
  }
412
990
  /**
413
991
  * Preview's "mixed" edge color: the mean of two colors, each channel rounded down (`EdgeColor.java` lines 89-93).
@@ -424,22 +1002,6 @@ function gephiMix(a, b) {
424
1002
  }
425
1003
  return `#${out.toString(16).padStart(6, "0")}`;
426
1004
  }
427
- /**
428
- * Preview's "darker" border color: each channel times 0.498, rounded (`ColorUtils.java` lines 22-34).
429
- * @param c - the fill, `#rrggbb`
430
- * @returns the border color
431
- */
432
- function gephiDarker(c) {
433
- const x = Number.parseInt(c.slice(1), 16);
434
- let out = 0;
435
- for (const shift of [16, 8, 0]) {
436
- out |= Math.min(255, Math.round(((x >> shift) & 255) * GEPHI_BORDER_DARKEN)) << shift;
437
- }
438
- return `#${out.toString(16).padStart(6, "0")}`;
439
- }
440
- const mix = (a, b) => gephiMix(String(a), String(b));
441
- const darker = (c) => gephiDarker(String(c));
442
- const round6 = (x) => Math.round(x * 1e6) / 1e6;
443
1005
  /**
444
1006
  * The edge whose source Gephi draws for each edge: an undirected edge in a directed graph is one edge in the file
445
1007
  * and two halves here, and the second half takes the first half's.
@@ -467,10 +1029,10 @@ function withoutOwn(own, count, value) {
467
1029
  return out;
468
1030
  }
469
1031
  /**
470
- * The styles of a GEXF file: each element's viz values as Gephi draws them, with Gephi Preview's defaults beneath
471
- * (black size-10 discs with a darker border, curved edges, an arrow at the target of every directed edge, the mixed
472
- * color of its nodes on every edge without a color of its own) and each node's label (see gephiRules()); plus a
473
- * `graphty.styles` block the file holds in `<meta>`.
1032
+ * The styles of a GEXF file as Gephi's preview draws it: each node's label, fill and size, Gephi's defaults beneath
1033
+ * (black size-10 discs, curved edges) and its per-element defaults (gephiRules()); the viz values Gephi does not draw
1034
+ * are left out (withoutUndrawn()); plus a `graphty.styles` block the file holds in `<meta>`, whose file keeps every
1035
+ * value.
474
1036
  * @param snapshot - the graph the import built
475
1037
  * @param context - the work budget and the block text the import kept
476
1038
  * @returns the styles
@@ -479,18 +1041,42 @@ export function gexfStyles(snapshot, context) {
479
1041
  const notes = new StyleNotes();
480
1042
  const node = vizValues(snapshot, "node", notes, context);
481
1043
  const edge = vizValues(snapshot, "edge", notes, context);
482
- const block = typeof context.kept === "string" ? context.kept : null;
483
- if (block !== null) {
1044
+ const { block, preview } = keptOf(context.kept);
1045
+ const nodeOpacity = node.fillOpacity;
1046
+ const original = { color: edge.color, opacity: edge.opacity };
1047
+ if (block === null) {
1048
+ withoutUndrawn(node, edge);
1049
+ }
1050
+ else {
484
1051
  exactFromBlock(snapshot, block, node, edge);
485
1052
  }
486
- const gephi = gephiRules(snapshot, node, edge, context.budget);
1053
+ const labels = drawnLabels(node.label, preview, "node", block !== null);
1054
+ const overlapHidden = block === null && labels !== undefined && preview.on("node.label.avoidOverlap")
1055
+ ? gephiOverlapHidden(snapshot, labels, node.width, preview)
1056
+ : null;
1057
+ overlapHidden?.forEach((hide, i) => {
1058
+ if (hide && labels !== undefined) {
1059
+ labels[i] = null;
1060
+ }
1061
+ });
1062
+ if (labels === undefined) {
1063
+ delete node.label;
1064
+ }
1065
+ else {
1066
+ node.label = labels;
1067
+ }
1068
+ const edgeLabels = drawnLabels(edgeLabelValues(snapshot), preview, "edge", block !== null);
1069
+ if (edgeLabels !== undefined) {
1070
+ edge.label = edgeLabels;
1071
+ }
1072
+ const rules = gephiRules(snapshot, { fill: node.fill, fillOpacity: nodeOpacity, width: node.width }, { color: edge.color, width: edge.width }, context.budget, { preview, original, edgeLabels, overlapSettled: overlapHidden !== null });
487
1073
  return {
488
1074
  canonical: true,
489
1075
  dialect: "gexf",
490
1076
  tool: "gephi",
491
1077
  units: "relative",
492
- node: { values: node, ...gephi.node },
493
- edge: { values: edge, ...gephi.edge },
1078
+ node: { values: node, ...rules.node },
1079
+ edge: { values: edge, ...rules.edge },
494
1080
  notes: notes.list(),
495
1081
  opacityInColor: true,
496
1082
  block,
@@ -500,6 +1086,55 @@ export function gexfStyles(snapshot, context) {
500
1086
  },
501
1087
  };
502
1088
  }
1089
+ /**
1090
+ * What the import kept for the styles: the `graphty.styles` block, and the Preview settings of the `gephiPreview`
1091
+ * option.
1092
+ * @param kept - the kept value: the block's text, or `{ block, gephiPreview }`
1093
+ * @returns the block (null without one) and the Preview settings
1094
+ */
1095
+ function keptOf(kept) {
1096
+ if (typeof kept === "string") {
1097
+ return { block: kept, preview: new GephiPreview() };
1098
+ }
1099
+ if (typeof kept === "object" && kept !== null) {
1100
+ const { block, gephiPreview } = kept;
1101
+ return { block: typeof block === "string" ? block : null, preview: new GephiPreview(gephiPreview) };
1102
+ }
1103
+ return { block: null, preview: new GephiPreview() };
1104
+ }
1105
+ /**
1106
+ * Each edge's `label` attribute.
1107
+ * @param snapshot - the graph
1108
+ * @returns the labels, or undefined when no edge has one
1109
+ */
1110
+ function edgeLabelValues(snapshot) {
1111
+ const col = snapshot.edges.byRole("label");
1112
+ if (col === null) {
1113
+ return undefined;
1114
+ }
1115
+ return Array.from({ length: snapshot.edgeCount }, (_, e) => {
1116
+ const v = plainCell(col.value(e));
1117
+ return typeof v === "string" && v.length > 0 ? v : null;
1118
+ });
1119
+ }
1120
+ /**
1121
+ * Leaves out the viz values Gephi does not draw, for a file graphty did not write: its preview draws every node an
1122
+ * opaque disc (a node's alpha only tints its edges) and every edge a solid line in its end nodes' mixed color, as
1123
+ * thick as its weight (graphstore keeps no edge thickness or shape, and EdgeColor MIXED ignores an edge's color).
1124
+ * Every `viz:shape` is drawn a disc, an `image` shape too: Gephi Toolkit 0.11.3 draws no picture
1125
+ * (`gexf-specimen-images-charts`: a data: URI and a web URL both drawn as the node's colored disc).
1126
+ * @param node - the node values, changed in place
1127
+ * @param edge - the edge values, changed in place
1128
+ */
1129
+ function withoutUndrawn(node, edge) {
1130
+ delete node.shape;
1131
+ delete node.image;
1132
+ delete node.fillOpacity;
1133
+ delete edge.color;
1134
+ delete edge.opacity;
1135
+ delete edge.width;
1136
+ delete edge.lineStyle;
1137
+ }
503
1138
  /**
504
1139
  * For a file graphty wrote: where a viz value is the nearest GEXF value of what the `graphty.styles` block gives (a
505
1140
  * hexagon written as a disc, a width and height written as one size), the block's exact value is the truth, so a
@@ -511,44 +1146,32 @@ export function gexfStyles(snapshot, context) {
511
1146
  * @param edge - the edge values, changed in place
512
1147
  */
513
1148
  function exactFromBlock(snapshot, text, node, edge) {
514
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
515
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "gexf") : null;
516
- if (ex === null) {
517
- return;
518
- }
519
- const exact = (values, target, property, written) => {
520
- const native = values[property];
521
- if (native === undefined) {
522
- return;
523
- }
524
- const want = resolveStyles(ex, target, property, snapshot);
525
- for (let i = 0; i < native.length; i++) {
526
- const w = want[i];
527
- // a size is written from the width and the height together: one the block leaves unset reads back too
528
- const writable = w !== null || property === "width" || property === "height";
529
- if (writable && native[i] !== w && native[i] !== null && native[i] === written(w, i)) {
530
- native[i] = w;
531
- }
532
- }
533
- };
534
- const width = resolveStyles(ex, "node", "width", snapshot);
535
- const height = resolveStyles(ex, "node", "height", snapshot);
536
- const size = (i) => {
537
- const w = width[i];
538
- const h = height[i];
539
- return typeof w === "number" && typeof h === "number" ? Math.max(w, h) : (w ?? h);
540
- };
541
- const read = new Map([
1149
+ const shapeRead = new Map([
542
1150
  ["disc", "ellipse"],
543
1151
  ["square", "rectangle"],
544
1152
  ]);
545
- exact(node, "node", "shape", (v) => {
546
- const written = gexfNodeShape(String(v)).value;
547
- return read.get(written) ?? written;
548
- });
549
- exact(edge, "edge", "lineStyle", (v) => gexfEdgeShape(String(v)).value);
550
- exact(node, "node", "width", (_v, i) => size(i));
551
- exact(node, "node", "height", (_v, i) => size(i));
1153
+ const size = (want) => {
1154
+ const w = want("width");
1155
+ const h = want("height");
1156
+ return typeof w === "number" && typeof h === "number" ? Math.max(w, h) : (w ?? h);
1157
+ };
1158
+ // what the viz element written for an element's block values reads back as
1159
+ const written = {
1160
+ shape: (want) => {
1161
+ const { value } = gexfNodeShape(String(want("shape")));
1162
+ return shapeRead.get(value) ?? value;
1163
+ },
1164
+ width: size,
1165
+ height: size,
1166
+ lineStyle: (want) => gexfEdgeShape(String(want("lineStyle"))).value,
1167
+ };
1168
+ takeFromBlock(snapshot, text, "gexf", (target) => ({
1169
+ values: target === "node" ? node : edge,
1170
+ properties: target === "node" ? ["shape", "width", "height"] : ["lineStyle"],
1171
+ readBack: (_i, want) => (property) => written[property](want),
1172
+ // a size is written from the width and the height together: one the block leaves unset reads back too
1173
+ take: (property, wanted, native) => native !== null && (wanted !== null || property === "width" || property === "height"),
1174
+ }));
552
1175
  }
553
1176
  /**
554
1177
  * The dtype a tagged column is written with: numbers, booleans or text (a mixed column as text).
@@ -594,7 +1217,7 @@ export function gexfStyledExport(snapshot, input) {
594
1217
  const size = nodeSizes[i];
595
1218
  const s = nodeShapes[i];
596
1219
  return (colorElement(fill[i], fillOpacity[i], indent) +
597
- (size === null ? "" : `${indent}<viz:size value="${String(size)}"/>\n`) +
1220
+ (size === null ? "" : `${indent}<viz:size value="${String(size / 2)}"/>\n`) +
598
1221
  shapeElement(s, s === "image" ? String(image[i]) : null, indent));
599
1222
  },
600
1223
  edge(e, indent) {