@graphty/graph-io 0.3.48 → 0.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{approximations-C2riR4NT.js → approximations-BRXsll0T.js} +2 -2
- package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-BRXsll0T.js.map} +1 -1
- package/dist/chunks/bends-BGXjv_U4.js +129 -0
- package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
- package/dist/chunks/{block-Cu22ewuS.js → block-yl0f_qLO.js} +931 -164
- package/dist/chunks/block-yl0f_qLO.js.map +1 -0
- package/dist/chunks/choose-CczZJaX_.js +717 -0
- package/dist/chunks/choose-CczZJaX_.js.map +1 -0
- package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
- package/dist/chunks/codes-BE2xir2k.js.map +1 -0
- package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
- package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
- package/dist/chunks/{context-cgmmOxZ2.js → context-DizmppHy.js} +5 -4
- package/dist/chunks/{context-cgmmOxZ2.js.map → context-DizmppHy.js.map} +1 -1
- package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
- package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
- package/dist/chunks/{credentials-BiQsnMTb.js → credentials-CmvJXYeH.js} +67 -16
- package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
- package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
- package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
- package/dist/chunks/descriptor-B28U25x4.js +60 -0
- package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
- package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-Dhez_Cew.js} +11 -2
- package/dist/chunks/descriptor-Dhez_Cew.js.map +1 -0
- package/dist/chunks/{emit-CD8xnY8s.js → emit-7Jkpg0yd.js} +1788 -133
- package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
- package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
- package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
- package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
- package/dist/chunks/export-Cha4dRFz.js.map +1 -0
- package/dist/chunks/{exporter-ChjL5fmP.js → exporter-1Ajud15J.js} +9 -8
- package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-1Ajud15J.js.map} +1 -1
- package/dist/chunks/{exporter-BIdaoIgh.js → exporter-B6yvL3wy.js} +7 -7
- package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-B6yvL3wy.js.map} +1 -1
- package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
- package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
- package/dist/chunks/{exporter-DtVU80Fa.js → exporter-CA5q8ROP.js} +9 -8
- package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-CA5q8ROP.js.map} +1 -1
- package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-CQQZFJ2v.js} +1284 -156
- package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
- package/dist/chunks/format-BXiAb-1h.js +408 -0
- package/dist/chunks/format-BXiAb-1h.js.map +1 -0
- package/dist/chunks/{generic-CP6YD34u.js → generic-Dc21MAdg.js} +387 -31
- package/dist/chunks/generic-Dc21MAdg.js.map +1 -0
- package/dist/chunks/{importer-B87_fmVx.js → importer-C6NVpCXp.js} +327 -84
- package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
- package/dist/chunks/{importer-DPZDN40-.js → importer-CKFe4q54.js} +25 -17
- package/dist/chunks/importer-CKFe4q54.js.map +1 -0
- package/dist/chunks/{importer-Bji2MeWr.js → importer-CKbUbsJA.js} +13 -10
- package/dist/chunks/{importer-Bji2MeWr.js.map → importer-CKbUbsJA.js.map} +1 -1
- package/dist/chunks/{importer-CB_zV5_L.js → importer-ClCsdgTJ.js} +208 -1104
- package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
- package/dist/chunks/{importer-e3huMZbm.js → importer-CyVZTrf0.js} +14 -10
- package/dist/chunks/importer-CyVZTrf0.js.map +1 -0
- package/dist/chunks/{importer-CVT-JyDE.js → importer-DgG4v1Ms.js} +1537 -355
- package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
- package/dist/chunks/{importer-Cyy6UseH.js → importer-asFUrIsj.js} +66 -13
- package/dist/chunks/importer-asFUrIsj.js.map +1 -0
- package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
- package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
- package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
- package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
- package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-IVRq2Uoo.js} +282 -74
- package/dist/chunks/lexicon-IVRq2Uoo.js.map +1 -0
- package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
- package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
- package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
- package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
- package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
- package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
- package/dist/chunks/round-trip-Cy3_SHRZ.js +79 -0
- package/dist/chunks/round-trip-Cy3_SHRZ.js.map +1 -0
- package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-BXwVbza3.js} +5 -5
- package/dist/chunks/sigv4-BXwVbza3.js.map +1 -0
- package/dist/chunks/{styles-CaABMoO1.js → styles-CAVmBLgp.js} +1237 -138
- package/dist/chunks/styles-CAVmBLgp.js.map +1 -0
- package/dist/chunks/styles-CPczw1Jz.js +1928 -0
- package/dist/chunks/styles-CPczw1Jz.js.map +1 -0
- package/dist/chunks/styles-boK6Ln54.js +2847 -0
- package/dist/chunks/styles-boK6Ln54.js.map +1 -0
- package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-XeFBQ6VS.js} +506 -175
- package/dist/chunks/styles-yed-XeFBQ6VS.js.map +1 -0
- package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
- package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
- package/dist/csv.js +7 -7
- package/dist/cx.js +2 -2
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +8 -4
- package/dist/dot.js.map +1 -1
- package/dist/gexf.js +42 -12
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +848 -194
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +230 -193
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +3 -3
- package/dist/json.js +3 -2
- package/dist/neo4j.js +6 -6
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/positions.js +45 -3
- package/dist/positions.js.map +1 -1
- package/dist/sources-cypher.js +141 -23
- package/dist/sources-cypher.js.map +1 -1
- package/dist/sources-ndex.js +2 -2
- package/dist/sources-rest.js +32 -6
- package/dist/sources-rest.js.map +1 -1
- package/dist/sources-sparql.d.ts +1 -0
- package/dist/sources-sparql.js +439 -0
- package/dist/sources-sparql.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +5 -1
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx/styles.d.ts +24 -1
- package/dist/src/formats/cx/styles.d.ts.map +1 -1
- package/dist/src/formats/cx/styles.js +117 -11
- package/dist/src/formats/cx/styles.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +1 -0
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cx2/styles.d.ts.map +1 -1
- package/dist/src/formats/cx2/styles.js +3 -1
- package/dist/src/formats/cx2/styles.js.map +1 -1
- package/dist/src/formats/cys/importer.d.ts.map +1 -1
- package/dist/src/formats/cys/importer.js +1 -0
- package/dist/src/formats/cys/importer.js.map +1 -1
- package/dist/src/formats/cys/styles.d.ts +2 -0
- package/dist/src/formats/cys/styles.d.ts.map +1 -1
- package/dist/src/formats/cys/styles.js +15 -7
- package/dist/src/formats/cys/styles.js.map +1 -1
- package/dist/src/formats/dot/drawing.d.ts +98 -0
- package/dist/src/formats/dot/drawing.d.ts.map +1 -0
- package/dist/src/formats/dot/drawing.js +734 -0
- package/dist/src/formats/dot/drawing.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +34 -2
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +6 -1
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +170 -33
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +3 -1
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +2 -1
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/label-places.d.ts +41 -0
- package/dist/src/formats/dot/label-places.d.ts.map +1 -0
- package/dist/src/formats/dot/label-places.js +185 -0
- package/dist/src/formats/dot/label-places.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +13 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +13 -0
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/record.d.ts +50 -0
- package/dist/src/formats/dot/record.d.ts.map +1 -0
- package/dist/src/formats/dot/record.js +275 -0
- package/dist/src/formats/dot/record.js.map +1 -0
- package/dist/src/formats/dot/styles.d.ts +72 -2
- package/dist/src/formats/dot/styles.d.ts.map +1 -1
- package/dist/src/formats/dot/styles.js +1500 -119
- package/dist/src/formats/dot/styles.js.map +1 -1
- package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
- package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
- package/dist/src/formats/gexf/dialog-metrics.js +59 -0
- package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +7 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +41 -5
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/preview.d.ts +77 -0
- package/dist/src/formats/gexf/preview.d.ts.map +1 -0
- package/dist/src/formats/gexf/preview.js +191 -0
- package/dist/src/formats/gexf/preview.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -1
- package/dist/src/formats/gexf/schema.js +2 -1
- package/dist/src/formats/gexf/schema.js.map +1 -1
- package/dist/src/formats/gexf/styles.d.ts +58 -31
- package/dist/src/formats/gexf/styles.d.ts.map +1 -1
- package/dist/src/formats/gexf/styles.js +848 -225
- package/dist/src/formats/gexf/styles.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +5 -2
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +258 -71
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +1 -1
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +1 -1
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/gml/positions.d.ts +7 -0
- package/dist/src/formats/gml/positions.d.ts.map +1 -1
- package/dist/src/formats/gml/positions.js +29 -7
- package/dist/src/formats/gml/positions.js.map +1 -1
- package/dist/src/formats/gml/styles.d.ts.map +1 -1
- package/dist/src/formats/gml/styles.js +754 -102
- package/dist/src/formats/gml/styles.js.map +1 -1
- package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-plain.js +6 -10
- package/dist/src/formats/graphml/styles-plain.js.map +1 -1
- package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
- package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-yed.js +695 -164
- package/dist/src/formats/graphml/styles-yed.js.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.js +431 -64
- package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts +3 -3
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +16 -6
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +3 -2
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +39 -5
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +13 -2
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +42 -6
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +1 -1
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +1 -1
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/formats/json/positions.d.ts +5 -2
- package/dist/src/formats/json/positions.d.ts.map +1 -1
- package/dist/src/formats/json/positions.js +20 -4
- package/dist/src/formats/json/positions.js.map +1 -1
- package/dist/src/formats/json/styles.d.ts +12 -1
- package/dist/src/formats/json/styles.d.ts.map +1 -1
- package/dist/src/formats/json/styles.js +1843 -136
- package/dist/src/formats/json/styles.js.map +1 -1
- package/dist/src/formats/pajek/draw.d.ts +104 -0
- package/dist/src/formats/pajek/draw.d.ts.map +1 -0
- package/dist/src/formats/pajek/draw.js +382 -0
- package/dist/src/formats/pajek/draw.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +70 -51
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +9 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +233 -102
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/index.d.ts +1 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -1
- package/dist/src/formats/pajek/index.js.map +1 -1
- package/dist/src/formats/pajek/styles.d.ts +15 -7
- package/dist/src/formats/pajek/styles.d.ts.map +1 -1
- package/dist/src/formats/pajek/styles.js +1395 -114
- package/dist/src/formats/pajek/styles.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +10 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +26 -2
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +1 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +65 -5
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/styles.d.ts +23 -3
- package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
- package/dist/src/formats/xgmml/styles.js +277 -60
- package/dist/src/formats/xgmml/styles.js.map +1 -1
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/positions/convert.d.ts.map +1 -1
- package/dist/src/positions/convert.js +11 -1
- package/dist/src/positions/convert.js.map +1 -1
- package/dist/src/positions/extract.d.ts.map +1 -1
- package/dist/src/positions/extract.js +53 -1
- package/dist/src/positions/extract.js.map +1 -1
- package/dist/src/positions/index.d.ts +1 -1
- package/dist/src/positions/index.d.ts.map +1 -1
- package/dist/src/positions/types.d.ts +59 -0
- package/dist/src/positions/types.d.ts.map +1 -1
- package/dist/src/sources/builtins.d.ts +1 -1
- package/dist/src/sources/builtins.d.ts.map +1 -1
- package/dist/src/sources/builtins.js +8 -2
- package/dist/src/sources/builtins.js.map +1 -1
- package/dist/src/sources/context.d.ts.map +1 -1
- package/dist/src/sources/context.js +1 -0
- package/dist/src/sources/context.js.map +1 -1
- package/dist/src/sources/credentials.d.ts +50 -1
- package/dist/src/sources/credentials.d.ts.map +1 -1
- package/dist/src/sources/credentials.js +69 -0
- package/dist/src/sources/credentials.js.map +1 -1
- package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
- package/dist/src/sources/cypher/descriptor.js +5 -0
- package/dist/src/sources/cypher/descriptor.js.map +1 -1
- package/dist/src/sources/cypher/index.d.ts.map +1 -1
- package/dist/src/sources/cypher/index.js +125 -18
- package/dist/src/sources/cypher/index.js.map +1 -1
- package/dist/src/sources/cypher/params.d.ts +1 -1
- package/dist/src/sources/cypher/params.d.ts.map +1 -1
- package/dist/src/sources/cypher/query-builder.d.ts +17 -0
- package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
- package/dist/src/sources/cypher/query-builder.js +31 -0
- package/dist/src/sources/cypher/query-builder.js.map +1 -1
- package/dist/src/sources/ids.d.ts +5 -0
- package/dist/src/sources/ids.d.ts.map +1 -1
- package/dist/src/sources/ids.js +5 -0
- package/dist/src/sources/ids.js.map +1 -1
- package/dist/src/sources/params.d.ts +3 -0
- package/dist/src/sources/params.d.ts.map +1 -1
- package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
- package/dist/src/sources/rest/descriptor.js +9 -0
- package/dist/src/sources/rest/descriptor.js.map +1 -1
- package/dist/src/sources/rest/openalex.d.ts +4 -2
- package/dist/src/sources/rest/openalex.d.ts.map +1 -1
- package/dist/src/sources/rest/openalex.js +4 -2
- package/dist/src/sources/rest/openalex.js.map +1 -1
- package/dist/src/sources/rest/source.d.ts.map +1 -1
- package/dist/src/sources/rest/source.js +38 -0
- package/dist/src/sources/rest/source.js.map +1 -1
- package/dist/src/sources/run.d.ts +2 -0
- package/dist/src/sources/run.d.ts.map +1 -1
- package/dist/src/sources/run.js +30 -3
- package/dist/src/sources/run.js.map +1 -1
- package/dist/src/sources/sigv4.d.ts +5 -3
- package/dist/src/sources/sigv4.d.ts.map +1 -1
- package/dist/src/sources/sigv4.js +6 -4
- package/dist/src/sources/sigv4.js.map +1 -1
- package/dist/src/sources/sparql/descriptor.d.ts +10 -0
- package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
- package/dist/src/sources/sparql/descriptor.js +64 -0
- package/dist/src/sources/sparql/descriptor.js.map +1 -0
- package/dist/src/sources/sparql/index.d.ts +28 -0
- package/dist/src/sources/sparql/index.d.ts.map +1 -0
- package/dist/src/sources/sparql/index.js +327 -0
- package/dist/src/sources/sparql/index.js.map +1 -0
- package/dist/src/sources/sparql/ntriples.d.ts +69 -0
- package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
- package/dist/src/sources/sparql/ntriples.js +180 -0
- package/dist/src/sources/sparql/ntriples.js.map +1 -0
- package/dist/src/sources/sparql/params.d.ts +27 -0
- package/dist/src/sources/sparql/params.d.ts.map +1 -0
- package/dist/src/sources/sparql/params.js +5 -0
- package/dist/src/sources/sparql/params.js.map +1 -0
- package/dist/src/sources/sparql/query.d.ts +73 -0
- package/dist/src/sources/sparql/query.d.ts.map +1 -0
- package/dist/src/sources/sparql/query.js +164 -0
- package/dist/src/sources/sparql/query.js.map +1 -0
- package/dist/src/sources/types.d.ts +60 -0
- package/dist/src/sources/types.d.ts.map +1 -1
- package/dist/src/styles/annotations.d.ts +61 -5
- package/dist/src/styles/annotations.d.ts.map +1 -1
- package/dist/src/styles/annotations.js +130 -22
- package/dist/src/styles/annotations.js.map +1 -1
- package/dist/src/styles/canon/bends.d.ts +46 -0
- package/dist/src/styles/canon/bends.d.ts.map +1 -1
- package/dist/src/styles/canon/bends.js +123 -0
- package/dist/src/styles/canon/bends.js.map +1 -1
- package/dist/src/styles/canon/color-tables.d.ts +6 -1
- package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
- package/dist/src/styles/canon/color-tables.js +10 -4
- package/dist/src/styles/canon/color-tables.js.map +1 -1
- package/dist/src/styles/canon/color.d.ts +3 -1
- package/dist/src/styles/canon/color.d.ts.map +1 -1
- package/dist/src/styles/canon/color.js +103 -3
- package/dist/src/styles/canon/color.js.map +1 -1
- package/dist/src/styles/canon/enums.d.ts +2 -2
- package/dist/src/styles/canon/enums.d.ts.map +1 -1
- package/dist/src/styles/canon/enums.js +89 -7
- package/dist/src/styles/canon/enums.js.map +1 -1
- package/dist/src/styles/canon/graphviz.d.ts +12 -3
- package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
- package/dist/src/styles/canon/graphviz.js +57 -9
- package/dist/src/styles/canon/graphviz.js.map +1 -1
- package/dist/src/styles/canon/number.d.ts +8 -0
- package/dist/src/styles/canon/number.d.ts.map +1 -1
- package/dist/src/styles/canon/number.js +18 -0
- package/dist/src/styles/canon/number.js.map +1 -1
- package/dist/src/styles/canon/text.d.ts +29 -2
- package/dist/src/styles/canon/text.d.ts.map +1 -1
- package/dist/src/styles/canon/text.js +156 -16
- package/dist/src/styles/canon/text.js.map +1 -1
- package/dist/src/styles/codes.d.ts +2 -0
- package/dist/src/styles/codes.d.ts.map +1 -1
- package/dist/src/styles/codes.js +2 -0
- package/dist/src/styles/codes.js.map +1 -1
- package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
- package/dist/src/styles/cyjs/cascade.js +1139 -79
- package/dist/src/styles/cyjs/cascade.js.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.js +4 -0
- package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
- package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
- package/dist/src/styles/cyjs/emit.js +232 -10
- package/dist/src/styles/cyjs/emit.js.map +1 -1
- package/dist/src/styles/cyjs/face.d.ts +20 -0
- package/dist/src/styles/cyjs/face.d.ts.map +1 -0
- package/dist/src/styles/cyjs/face.js +332 -0
- package/dist/src/styles/cyjs/face.js.map +1 -0
- package/dist/src/styles/cyjs/mappers.d.ts +44 -1
- package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
- package/dist/src/styles/cyjs/mappers.js +475 -26
- package/dist/src/styles/cyjs/mappers.js.map +1 -1
- package/dist/src/styles/cyjs/routes.d.ts +53 -0
- package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
- package/dist/src/styles/cyjs/routes.js +360 -0
- package/dist/src/styles/cyjs/routes.js.map +1 -0
- package/dist/src/styles/cytoscape/charts.d.ts +23 -0
- package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
- package/dist/src/styles/cytoscape/charts.js +560 -0
- package/dist/src/styles/cytoscape/charts.js.map +1 -0
- package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/emit.js +156 -48
- package/dist/src/styles/cytoscape/emit.js.map +1 -1
- package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
- package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/lexicon.js +387 -77
- package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
- package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
- package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/mappings.js +92 -0
- package/dist/src/styles/cytoscape/mappings.js.map +1 -1
- package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
- package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
- package/dist/src/styles/cytoscape/resolve.js +623 -17
- package/dist/src/styles/cytoscape/resolve.js.map +1 -1
- package/dist/src/styles/defaults.d.ts +46 -26
- package/dist/src/styles/defaults.d.ts.map +1 -1
- package/dist/src/styles/defaults.js +308 -71
- package/dist/src/styles/defaults.js.map +1 -1
- package/dist/src/styles/index.d.ts +4 -3
- package/dist/src/styles/index.d.ts.map +1 -1
- package/dist/src/styles/index.js +3 -2
- package/dist/src/styles/index.js.map +1 -1
- package/dist/src/styles/internal.d.ts +1 -0
- package/dist/src/styles/internal.d.ts.map +1 -1
- package/dist/src/styles/internal.js +1 -0
- package/dist/src/styles/internal.js.map +1 -1
- package/dist/src/styles/io/generic.d.ts.map +1 -1
- package/dist/src/styles/io/generic.js +56 -16
- package/dist/src/styles/io/generic.js.map +1 -1
- package/dist/src/styles/io/round-trip.d.ts +56 -0
- package/dist/src/styles/io/round-trip.d.ts.map +1 -0
- package/dist/src/styles/io/round-trip.js +138 -0
- package/dist/src/styles/io/round-trip.js.map +1 -0
- package/dist/src/styles/io/writer.d.ts.map +1 -1
- package/dist/src/styles/io/writer.js +6 -1
- package/dist/src/styles/io/writer.js.map +1 -1
- package/dist/src/styles/notes.d.ts.map +1 -1
- package/dist/src/styles/notes.js +1 -0
- package/dist/src/styles/notes.js.map +1 -1
- package/dist/src/styles/plan/features.d.ts +1 -1
- package/dist/src/styles/plan/features.d.ts.map +1 -1
- package/dist/src/styles/plan/features.js +22 -2
- package/dist/src/styles/plan/features.js.map +1 -1
- package/dist/src/styles/plan/index.d.ts +1 -1
- package/dist/src/styles/plan/index.d.ts.map +1 -1
- package/dist/src/styles/plan/index.js +1 -1
- package/dist/src/styles/plan/index.js.map +1 -1
- package/dist/src/styles/plan/plan.d.ts.map +1 -1
- package/dist/src/styles/plan/plan.js +61 -4
- package/dist/src/styles/plan/plan.js.map +1 -1
- package/dist/src/styles/plan/resolve.d.ts +11 -0
- package/dist/src/styles/plan/resolve.d.ts.map +1 -1
- package/dist/src/styles/plan/resolve.js +40 -4
- package/dist/src/styles/plan/resolve.js.map +1 -1
- package/dist/src/styles/slots.d.ts +14 -0
- package/dist/src/styles/slots.d.ts.map +1 -0
- package/dist/src/styles/slots.js +20 -0
- package/dist/src/styles/slots.js.map +1 -0
- package/dist/src/styles/types.d.ts +687 -6
- package/dist/src/styles/types.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.d.ts +3 -2
- package/dist/src/styles/vocabulary.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.js +224 -0
- package/dist/src/styles/vocabulary.js.map +1 -1
- package/dist/styles.js +30 -239
- package/dist/styles.js.map +1 -1
- package/dist/xgmml.js +3 -3
- package/package.json +27 -7
- package/src/formats/cx/importer.ts +6 -1
- package/src/formats/cx/styles.ts +143 -9
- package/src/formats/cx2/importer.ts +1 -0
- package/src/formats/cx2/styles.ts +3 -1
- package/src/formats/cys/importer.ts +1 -0
- package/src/formats/cys/styles.ts +27 -5
- package/src/formats/dot/drawing.ts +799 -0
- package/src/formats/dot/exporter.ts +37 -1
- package/src/formats/dot/importer.ts +192 -36
- package/src/formats/dot/index.ts +3 -1
- package/src/formats/dot/label-places.ts +241 -0
- package/src/formats/dot/names.ts +15 -0
- package/src/formats/dot/record.ts +336 -0
- package/src/formats/dot/styles.ts +1702 -149
- package/src/formats/gexf/dialog-metrics.ts +61 -0
- package/src/formats/gexf/importer.ts +50 -5
- package/src/formats/gexf/preview.ts +217 -0
- package/src/formats/gexf/schema.ts +2 -1
- package/src/formats/gexf/styles.ts +1099 -245
- package/src/formats/gml/importer.ts +291 -76
- package/src/formats/gml/index.ts +1 -1
- package/src/formats/gml/positions.ts +29 -7
- package/src/formats/gml/styles.ts +852 -110
- package/src/formats/graphml/styles-plain.ts +6 -15
- package/src/formats/graphml/styles-yed.ts +765 -176
- package/src/formats/graphml/styles-yfiles3.ts +487 -63
- package/src/formats/graphml/yfiles.ts +19 -6
- package/src/formats/json/dialect.ts +41 -5
- package/src/formats/json/importer.ts +59 -7
- package/src/formats/json/index.ts +1 -0
- package/src/formats/json/positions.ts +22 -5
- package/src/formats/json/styles.ts +2123 -171
- package/src/formats/pajek/draw.ts +469 -0
- package/src/formats/pajek/exporter.ts +81 -49
- package/src/formats/pajek/importer.ts +278 -103
- package/src/formats/pajek/index.ts +1 -0
- package/src/formats/pajek/styles.ts +1506 -133
- package/src/formats/pajek/syntax.ts +27 -2
- package/src/formats/xgmml/emit.ts +73 -5
- package/src/formats/xgmml/styles.ts +330 -71
- package/src/index.ts +12 -1
- package/src/positions/convert.ts +12 -1
- package/src/positions/extract.ts +55 -1
- package/src/positions/index.ts +1 -0
- package/src/positions/types.ts +60 -0
- package/src/sources/builtins.ts +8 -1
- package/src/sources/context.ts +1 -0
- package/src/sources/credentials.ts +90 -1
- package/src/sources/cypher/descriptor.ts +7 -1
- package/src/sources/cypher/index.ts +149 -18
- package/src/sources/cypher/params.ts +1 -1
- package/src/sources/cypher/query-builder.ts +33 -0
- package/src/sources/ids.ts +6 -0
- package/src/sources/params.ts +3 -0
- package/src/sources/rest/descriptor.ts +9 -0
- package/src/sources/rest/openalex.ts +4 -2
- package/src/sources/rest/source.ts +41 -0
- package/src/sources/run.ts +35 -3
- package/src/sources/sigv4.ts +6 -4
- package/src/sources/sparql/descriptor.ts +67 -0
- package/src/sources/sparql/index.ts +371 -0
- package/src/sources/sparql/ntriples.ts +219 -0
- package/src/sources/sparql/params.ts +28 -0
- package/src/sources/sparql/query.ts +216 -0
- package/src/sources/types.ts +68 -0
- package/src/styles/annotations.ts +201 -27
- package/src/styles/canon/bends.ts +137 -0
- package/src/styles/canon/color-tables.ts +10 -4
- package/src/styles/canon/color.ts +105 -3
- package/src/styles/canon/enums.ts +107 -8
- package/src/styles/canon/graphviz.ts +71 -10
- package/src/styles/canon/number.ts +19 -0
- package/src/styles/canon/text.ts +187 -16
- package/src/styles/codes.ts +2 -0
- package/src/styles/cyjs/cascade.ts +1303 -84
- package/src/styles/cyjs/defaults-table.ts +4 -0
- package/src/styles/cyjs/emit.ts +268 -14
- package/src/styles/cyjs/face.ts +372 -0
- package/src/styles/cyjs/mappers.ts +506 -27
- package/src/styles/cyjs/routes.ts +432 -0
- package/src/styles/cytoscape/charts.ts +684 -0
- package/src/styles/cytoscape/emit.ts +170 -48
- package/src/styles/cytoscape/lexicon.ts +431 -78
- package/src/styles/cytoscape/mappings.ts +108 -0
- package/src/styles/cytoscape/resolve.ts +741 -17
- package/src/styles/defaults.ts +417 -108
- package/src/styles/index.ts +5 -0
- package/src/styles/internal.ts +1 -0
- package/src/styles/io/generic.ts +79 -23
- package/src/styles/io/round-trip.ts +202 -0
- package/src/styles/io/writer.ts +6 -1
- package/src/styles/notes.ts +1 -0
- package/src/styles/plan/features.ts +23 -2
- package/src/styles/plan/index.ts +1 -1
- package/src/styles/plan/plan.ts +69 -5
- package/src/styles/plan/resolve.ts +47 -4
- package/src/styles/slots.ts +25 -0
- package/src/styles/types.ts +698 -5
- package/src/styles/vocabulary.ts +232 -2
- package/dist/chunks/block-Cu22ewuS.js.map +0 -1
- package/dist/chunks/choose-Cdx6Enl2.js +0 -57
- package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
- package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
- package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
- package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
- package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
- package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
- package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
- package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
- package/dist/chunks/format-BMS-1Nxb.js +0 -64
- package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
- package/dist/chunks/generic-CP6YD34u.js.map +0 -1
- package/dist/chunks/importer-B87_fmVx.js.map +0 -1
- package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
- package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
- package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
- package/dist/chunks/importer-DPZDN40-.js.map +0 -1
- package/dist/chunks/importer-e3huMZbm.js.map +0 -1
- package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
- package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
- package/dist/chunks/styles-CaABMoO1.js.map +0 -1
- package/dist/chunks/styles-P_gmFAOi.js +0 -770
- package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
- package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
- package/dist/chunks/writer-DvHTl36J.js +0 -449
- package/dist/chunks/writer-DvHTl36J.js.map +0 -1
|
@@ -14,11 +14,11 @@ import { canonNumber } from "../../styles/canon/number.js";
|
|
|
14
14
|
import type { CanonSite } from "../../styles/canon/site.js";
|
|
15
15
|
import { canonText } from "../../styles/canon/text.js";
|
|
16
16
|
import { StyleApproximations } from "../../styles/io/approximations.js";
|
|
17
|
-
import {
|
|
17
|
+
import { STYLE_BLOCK_NAME } from "../../styles/io/block.js";
|
|
18
|
+
import { type ElementValues, takeFromBlock } from "../../styles/io/round-trip.js";
|
|
18
19
|
import type { ExporterStyleInput } from "../../styles/io/writer.js";
|
|
19
20
|
import { StyleNotes } from "../../styles/notes.js";
|
|
20
|
-
import {
|
|
21
|
-
import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
|
|
21
|
+
import { plainCell } from "../../styles/plan/resolve.js";
|
|
22
22
|
import type {
|
|
23
23
|
ImporterStyles,
|
|
24
24
|
StyleColumn,
|
|
@@ -29,7 +29,9 @@ import type {
|
|
|
29
29
|
StyleWriteCapabilities,
|
|
30
30
|
} from "../../styles/types.js";
|
|
31
31
|
import type { LossNote } from "../../types.js";
|
|
32
|
-
import {
|
|
32
|
+
import { DIALOG_LINE_HEIGHT, dialogWidth } from "./dialog-metrics.js";
|
|
33
|
+
import { dependantColor, drawnLabels, edgeThickness, type GephiFont, GephiPreview } from "./preview.js";
|
|
34
|
+
import { NODE_COLUMNS, TEMPORAL_COLUMNS, temporalTableName, VIZ_NAMESPACE } from "./schema.js";
|
|
33
35
|
|
|
34
36
|
/** The column of a node's `label` attribute. */
|
|
35
37
|
const LABEL_COLUMN = NODE_COLUMNS.label;
|
|
@@ -58,15 +60,20 @@ const GEPHI_NODE_FILL = "#000000";
|
|
|
58
60
|
/** The style column marking directed edges, which Gephi draws with an arrow at the target. */
|
|
59
61
|
const DIRECTED_KEY = "directed";
|
|
60
62
|
|
|
61
|
-
/** The style column
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
/** The style column marking the nodes whose labels Gephi hides where they overlap (node.label.avoidOverlap). */
|
|
64
|
+
const AVOID_OVERLAP_KEY = "label-avoid-overlap";
|
|
65
|
+
|
|
66
|
+
/** The style column marking the edges Gephi does not draw (dropped, or merged into a parallel edge). */
|
|
67
|
+
const HIDDEN_KEY = "gephi-hidden";
|
|
68
|
+
|
|
69
|
+
/** The style column holding the mix of its end nodes' colors of each edge without a color of its own. */
|
|
70
|
+
const MIXED_COLOR_KEY = "end-colors";
|
|
71
|
+
/** The width property and the style column of the width Gephi draws an edge with from its weight. */
|
|
72
|
+
const WEIGHT_WIDTH = ["width", "weight-width"] as const;
|
|
73
|
+
/** Gephi's node width when a node has no `viz:size` (graphstore NodeImpl size 10, a radius). */
|
|
74
|
+
const GEPHI_NODE_WIDTH = 20;
|
|
75
|
+
/** How much thicker than its weight Gephi draws a self-loop. */
|
|
76
|
+
const SELF_LOOP_THICKNESS = 1.3;
|
|
70
77
|
|
|
71
78
|
/** GEXF's node shapes, by the neutral shape each writes exactly. */
|
|
72
79
|
const NODE_SHAPES: ReadonlyMap<string, string> = new Map([
|
|
@@ -116,26 +123,37 @@ const VIZ_COLUMNS: Readonly<Record<StyleTarget, readonly string[]>> = {
|
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
/**
|
|
119
|
-
* A `viz:color` `hex` value as channels: r, g and b 0..255 and an alpha 0..1
|
|
120
|
-
*
|
|
126
|
+
* 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
|
|
127
|
+
* `Color.decode`, which parses the text as one integer (`Integer.decode`: `#` or `0x` hexadecimal, a leading `0`
|
|
128
|
+
* octal, otherwise decimal; a leading sign allowed) and keeps its low 24 bits, opaque. So `#rrggbb` is the color it
|
|
129
|
+
* names, but `#f80` is `#000f80`, `#4daf4a80` is `#af4a80` and `984ea3` is not a color at all (Gephi draws that node
|
|
130
|
+
* as if it had no color: `gexf-specimen-node-fills-borders`, `color-hex-short`, `color-hex-alpha`, `color-hex-nohash`).
|
|
121
131
|
* @param hex - the attribute's value
|
|
122
|
-
* @returns `[r, g, b,
|
|
132
|
+
* @returns `[r, g, b, 1]`, or null when Java cannot read it as an int
|
|
123
133
|
*/
|
|
124
134
|
export function canonVizColor(hex: string): [number, number, number, number] | null {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
135
|
+
const m = /^([+-]?)(?:(0x|0X|#)([0-9a-fA-F]+)|(0[0-7]+)|(\d+))$/.exec(hex.trim());
|
|
136
|
+
if (m === null) {
|
|
127
137
|
return null;
|
|
128
138
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
|
|
139
|
+
const [, sign, , hexDigits, octal, decimal] = m;
|
|
140
|
+
let v: number;
|
|
141
|
+
if (hexDigits !== undefined) {
|
|
142
|
+
v = Number.parseInt(hexDigits, 16);
|
|
143
|
+
} else if (octal !== undefined) {
|
|
144
|
+
v = Number.parseInt(octal, 8);
|
|
145
|
+
} else {
|
|
146
|
+
v = Number.parseInt(decimal, 10);
|
|
147
|
+
}
|
|
148
|
+
if (sign === "-") {
|
|
149
|
+
v = -v;
|
|
150
|
+
}
|
|
151
|
+
// Integer.decode accepts -2^31 .. 2^31 - 1 only
|
|
152
|
+
if (v < -2147483648 || v > 2147483647) {
|
|
135
153
|
return null;
|
|
136
154
|
}
|
|
137
|
-
const n =
|
|
138
|
-
return [(n >> 16) & 255, (n >> 8) & 255, n & 255,
|
|
155
|
+
const n = v & 0xffffff;
|
|
156
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1];
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
/**
|
|
@@ -256,17 +274,22 @@ function readVizColor(viz: VizRead, color: string, i: number): void {
|
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
/**
|
|
259
|
-
* Reads one element's viz size (a node's width and height) or thickness (an edge's width).
|
|
277
|
+
* Reads one element's viz size (a node's width and height) or thickness (an edge's width). A node's
|
|
278
|
+
* size is a radius, as Gephi and sigma draw it (Gephi's preview: a circle of radius 0.92 x size plus
|
|
279
|
+
* a 0.16 x size outline, an outer diameter of 2 x size), so its width and height are twice it.
|
|
260
280
|
* @param viz - the target's reading
|
|
261
281
|
* @param size - the viz size or thickness column
|
|
262
282
|
* @param i - the element
|
|
263
283
|
*/
|
|
264
284
|
function readVizSize(viz: VizRead, size: string, i: number): void {
|
|
265
285
|
const v = canonNumber(plainCell(viz.table.value(size, i)), viz.site(i, "width"));
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
// Gephi draws a node of viz:size 0 at its default size 10 (gexf-specimen-node-sizes-opacity, size-0)
|
|
287
|
+
if (v !== null && !(v === 0 && viz.target === "node")) {
|
|
268
288
|
if (viz.target === "node") {
|
|
269
|
-
viz.column("
|
|
289
|
+
viz.column("width")[i] = 2 * v;
|
|
290
|
+
viz.column("height")[i] = 2 * v;
|
|
291
|
+
} else {
|
|
292
|
+
viz.column("width")[i] = v;
|
|
270
293
|
}
|
|
271
294
|
}
|
|
272
295
|
}
|
|
@@ -288,206 +311,987 @@ function readVizShape(viz: VizRead, shape: string, i: number): boolean {
|
|
|
288
311
|
return typeof raw === "string" && raw.toLowerCase() === "image";
|
|
289
312
|
}
|
|
290
313
|
|
|
314
|
+
/** What Gephi draws of each edge (gephiEdges()). */
|
|
315
|
+
interface GephiDrawnEdges {
|
|
316
|
+
/** Whether Gephi draws the edge directed, with an arrow at its target. */
|
|
317
|
+
readonly directed: boolean[];
|
|
318
|
+
/** Whether Gephi does not draw the edge at all. */
|
|
319
|
+
readonly hidden: boolean[];
|
|
320
|
+
/** The weight Gephi draws the edge with: merged, then shifted. */
|
|
321
|
+
readonly weight: number[];
|
|
322
|
+
}
|
|
323
|
+
|
|
291
324
|
/**
|
|
292
|
-
*
|
|
325
|
+
* The edges as Gephi's importer and preview draw them (measured with the Gephi Toolkit 0.11.3:
|
|
326
|
+
* `gexf-specimen-edges-lines`, `-p13`, `-p15`, `gexf-specimen-arrows`, `-p6`, `-p7`):
|
|
327
|
+
* - an edge whose own type is against the graph's `defaultedgetype` is dropped (`undirected` in a `directed`
|
|
328
|
+
* graph, `directed` in an `undirected` one); a `mutual` graph, or one with no `defaultedgetype`, keeps every
|
|
329
|
+
* edge, an untyped or `mutual` one directed;
|
|
330
|
+
* - edges of one kind between the same ends (either way round for an undirected edge) are merged into the first,
|
|
331
|
+
* whose weight is their sum;
|
|
332
|
+
* - an edge's weight is its `weight` (1 when it has none), or, when an edge attribute named weight is dynamic, its
|
|
333
|
+
* value over the earliest interval (0 when it has none);
|
|
334
|
+
* - when the smallest weight drawn is 0 or less, every weight is raised by |smallest| + 1.
|
|
335
|
+
* An undirected edge of a directed snapshot is two halves here; both take what its first half gets.
|
|
293
336
|
* @param snapshot - the graph
|
|
294
|
-
* @
|
|
337
|
+
* @param gexf - whether the file is a GEXF; another format keeps its own direction, with nothing merged or dropped
|
|
338
|
+
* @returns per edge, what Gephi draws
|
|
295
339
|
*/
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
340
|
+
function gephiEdges(snapshot: GraphSnapshot, gexf: boolean): GephiDrawnEdges {
|
|
341
|
+
const n = snapshot.edgeCount;
|
|
342
|
+
const first = firstHalf(snapshot);
|
|
343
|
+
// another format Gephi draws: its own direction, nothing merged or dropped
|
|
344
|
+
if (!gexf) {
|
|
345
|
+
return otherFormatEdges(snapshot);
|
|
346
|
+
}
|
|
347
|
+
const header = gexfDefaultEdgeType(snapshot);
|
|
348
|
+
const dirCol = snapshot.edges.byRole("directed");
|
|
349
|
+
const mutualCol = snapshot.edges.byRole("mutual");
|
|
350
|
+
const kindCol = snapshot.edges.byRole("kind");
|
|
351
|
+
const weightOf = gephiWeight(snapshot);
|
|
352
|
+
const out: GephiDrawnEdges = {
|
|
353
|
+
directed: new Array<boolean>(n).fill(false),
|
|
354
|
+
hidden: new Array<boolean>(n).fill(false),
|
|
355
|
+
weight: new Array<number>(n).fill(1),
|
|
356
|
+
};
|
|
357
|
+
const leaders = new Map<string, number>();
|
|
358
|
+
const drawnLeaders: number[] = [];
|
|
359
|
+
const ownDirection = gexfOwnDirection(header, dirCol, mutualCol);
|
|
360
|
+
for (let e = 0; e < n; e++) {
|
|
361
|
+
const f = first(e);
|
|
362
|
+
if (f !== e) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// the edge's own type: graph-io records it only when the file mixes directions
|
|
366
|
+
const { ownUndirected, mismatched } = ownDirection(f);
|
|
367
|
+
if (mismatched) {
|
|
368
|
+
out.hidden[f] = true;
|
|
369
|
+
continue;
|
|
302
370
|
}
|
|
371
|
+
// Gephi draws no arrowhead on a self-loop
|
|
372
|
+
const s = snapshot.edgeSource(f);
|
|
373
|
+
const t = snapshot.edgeTarget(f);
|
|
374
|
+
const directed = header !== "undirected" && s !== t && !ownUndirected;
|
|
375
|
+
out.directed[f] = directed;
|
|
376
|
+
const kind = kindCol?.isSet(f) === true ? String(kindCol.value(f)) : "";
|
|
377
|
+
const key = `${kind}\u0000${gephiEndsKey(s, t, directed)}`;
|
|
378
|
+
const leader = leaders.get(key);
|
|
379
|
+
if (leader === undefined) {
|
|
380
|
+
leaders.set(key, f);
|
|
381
|
+
drawnLeaders.push(f);
|
|
382
|
+
out.weight[f] = weightOf(f);
|
|
383
|
+
} else {
|
|
384
|
+
out.weight[leader] += weightOf(f);
|
|
385
|
+
out.hidden[f] = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// a loop, not Math.min(...): a spread of every edge overflows the call stack on a large graph
|
|
389
|
+
const smallest = drawnLeaders.reduce((m, f) => Math.min(m, out.weight[f]), Infinity);
|
|
390
|
+
const shift = smallest <= 0 ? 1 - smallest : 0;
|
|
391
|
+
for (let e = 0; e < n; e++) {
|
|
392
|
+
const f = first(e);
|
|
393
|
+
out.directed[e] = out.directed[f];
|
|
394
|
+
out.hidden[e] = out.hidden[f];
|
|
395
|
+
out.weight[e] = out.weight[f] + shift;
|
|
303
396
|
}
|
|
304
397
|
return out;
|
|
305
398
|
}
|
|
306
399
|
|
|
307
400
|
/**
|
|
308
|
-
* The
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
*
|
|
321
|
-
*
|
|
401
|
+
* The ends of an edge as the key Gephi merges parallel edges by: in order for a directed edge, either way round
|
|
402
|
+
* for an undirected one.
|
|
403
|
+
* @param s - the source node index
|
|
404
|
+
* @param t - the target node index
|
|
405
|
+
* @param directed - whether the edge is drawn directed
|
|
406
|
+
* @returns the key
|
|
407
|
+
*/
|
|
408
|
+
function gephiEndsKey(s: number, t: number, directed: boolean): string {
|
|
409
|
+
return directed ? `${String(s)}>${String(t)}` : `${String(Math.min(s, t))}-${String(Math.max(s, t))}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A GEXF edge's own type, read from the directed and mutual columns, or from the header when the file did not
|
|
414
|
+
* mix directions (no directed column); an edge whose own type disagrees with the header's is one Gephi never loads.
|
|
415
|
+
* @param header - the file's default edge type
|
|
416
|
+
* @param dirCol - the directed column, or null
|
|
417
|
+
* @param mutualCol - the mutual column, or null
|
|
418
|
+
* @returns per edge index, whether its own type is undirected and whether it disagrees with the header
|
|
419
|
+
*/
|
|
420
|
+
function gexfOwnDirection(
|
|
421
|
+
header: ReturnType<typeof gexfDefaultEdgeType>,
|
|
422
|
+
dirCol: ReturnType<GraphSnapshot["edges"]["byRole"]>,
|
|
423
|
+
mutualCol: ReturnType<GraphSnapshot["edges"]["byRole"]>,
|
|
424
|
+
): (f: number) => { ownUndirected: boolean; mismatched: boolean } {
|
|
425
|
+
const verdict = (
|
|
426
|
+
ownUndirected: boolean,
|
|
427
|
+
ownDirected: boolean,
|
|
428
|
+
): { ownUndirected: boolean; mismatched: boolean } => ({
|
|
429
|
+
ownUndirected,
|
|
430
|
+
mismatched: (header === "directed" && ownUndirected) || (header === "undirected" && ownDirected),
|
|
431
|
+
});
|
|
432
|
+
return (f) => {
|
|
433
|
+
if (dirCol === null) {
|
|
434
|
+
return verdict(header === "undirected", header !== "undirected");
|
|
435
|
+
}
|
|
436
|
+
const mutual = mutualCol?.value(f) === true;
|
|
437
|
+
return verdict(dirCol.value(f) === false && !mutual, dirCol.value(f) === true && !mutual);
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The edges of a format other than GEXF as Gephi draws them: each in its own direction, as thick as its weight (1
|
|
443
|
+
* for a weight of 0 or below, so the edge stays visible).
|
|
322
444
|
* @param snapshot - the graph
|
|
323
|
-
* @
|
|
324
|
-
* @param edge - the edge values read from the file
|
|
325
|
-
* @param budget - the work budget
|
|
326
|
-
* @returns the node and edge rules and the style columns they read
|
|
445
|
+
* @returns per edge, what Gephi draws
|
|
327
446
|
*/
|
|
328
|
-
|
|
329
|
-
snapshot
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
447
|
+
function otherFormatEdges(snapshot: GraphSnapshot): GephiDrawnEdges {
|
|
448
|
+
const first = firstHalf(snapshot);
|
|
449
|
+
const weightOf = gephiWeight(snapshot);
|
|
450
|
+
const directed = new Array<boolean>(snapshot.edgeCount).fill(snapshot.directed);
|
|
451
|
+
const col = snapshot.directed ? snapshot.edges.byRole("directed") : null;
|
|
452
|
+
const weight: number[] = [];
|
|
453
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
454
|
+
if (col !== null) {
|
|
455
|
+
directed[e] = plainCell(col.value(e)) !== false;
|
|
456
|
+
}
|
|
457
|
+
const w = weightOf(first(e));
|
|
458
|
+
weight.push(w > 0 ? w : 1);
|
|
459
|
+
}
|
|
460
|
+
return { directed, hidden: directed.map(() => false), weight };
|
|
335
461
|
}
|
|
336
462
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
463
|
+
/**
|
|
464
|
+
* The graph's `defaultedgetype` as written, lower case, or null when it wrote none.
|
|
465
|
+
* @param snapshot - the graph
|
|
466
|
+
* @returns the keyword
|
|
467
|
+
*/
|
|
468
|
+
function gexfDefaultEdgeType(snapshot: GraphSnapshot): string | null {
|
|
469
|
+
const written = (snapshot.meta.extra.gexf as { defaultedgetype?: unknown } | undefined)?.defaultedgetype;
|
|
470
|
+
return typeof written === "string" ? written.trim().toLowerCase() : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Each edge's weight as Gephi reads it: an edge attribute whose id or title is `weight` and is dynamic is the weight,
|
|
475
|
+
* at its value over the earliest interval (0 for an edge without one: its static `weight` is not read); otherwise
|
|
476
|
+
* the edge's `weight`, 1 for an edge with none.
|
|
477
|
+
* @param snapshot - the graph
|
|
478
|
+
* @returns the weight of an edge
|
|
479
|
+
*/
|
|
480
|
+
function gephiWeight(snapshot: GraphSnapshot): (e: number) => number {
|
|
481
|
+
const isWeight = (v: unknown): boolean => typeof v === "string" && v.toLowerCase() === "weight";
|
|
482
|
+
const dynamic = snapshot.edges
|
|
483
|
+
.names()
|
|
484
|
+
.map((name) => snapshot.edges.get(name))
|
|
485
|
+
.find(
|
|
486
|
+
(c) =>
|
|
487
|
+
c !== null && c.meta.dynamic && [c.meta.name, c.meta.origin?.id, c.meta.origin?.title].some(isWeight),
|
|
488
|
+
);
|
|
489
|
+
const table =
|
|
490
|
+
dynamic === undefined || dynamic === null
|
|
491
|
+
? undefined
|
|
492
|
+
: snapshot.extensions.get(temporalTableName("edge", dynamic.meta.name));
|
|
493
|
+
if (table !== undefined) {
|
|
494
|
+
const element = table.get(TEMPORAL_COLUMNS.element);
|
|
495
|
+
const start = table.get(TEMPORAL_COLUMNS.start);
|
|
496
|
+
const value = table.get(TEMPORAL_COLUMNS.value);
|
|
497
|
+
const earliest = new Map<number, { start: number; value: number }>();
|
|
498
|
+
for (let row = 0; element !== null && value !== null && row < table.rowCount; row++) {
|
|
499
|
+
const e = element.value(row) as number;
|
|
500
|
+
const at = start?.isSet(row) === true ? (start.value(row) as number) : -Infinity;
|
|
501
|
+
const v = Number(value.value(row));
|
|
502
|
+
const seen = earliest.get(e);
|
|
503
|
+
if (Number.isFinite(v) && (seen === undefined || at < seen.start)) {
|
|
504
|
+
earliest.set(e, { start: at, value: v });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return (e) => earliest.get(e)?.value ?? 0;
|
|
508
|
+
}
|
|
509
|
+
// the snapshot's own weights (the role column exists only when some edge has none), as their shortest decimal
|
|
510
|
+
const weights = explicitWeights(snapshot);
|
|
511
|
+
return (e) => {
|
|
512
|
+
const w = Number(weights.text(e) ?? 1);
|
|
513
|
+
return Number.isFinite(w) ? w : 1;
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The node values Gephi's per-element defaults are worked out from, each undefined when no node has one. */
|
|
518
|
+
interface GephiNodeValues {
|
|
519
|
+
/** Each node's fill. */
|
|
520
|
+
readonly fill?: readonly (StyleValue | null)[];
|
|
521
|
+
/** Each node's fill opacity (the alpha of its color), null for opaque. */
|
|
522
|
+
readonly fillOpacity?: readonly (StyleValue | null)[];
|
|
523
|
+
/** Each node's width, twice its Gephi size. */
|
|
524
|
+
readonly width?: readonly (StyleValue | null)[];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The edge values Gephi's per-element defaults give way to, each undefined when no edge has one. */
|
|
528
|
+
interface GephiEdgeValues {
|
|
529
|
+
/** Each edge's own color. */
|
|
530
|
+
readonly color?: readonly (StyleValue | null)[];
|
|
531
|
+
/** Each edge's own width. */
|
|
532
|
+
readonly width?: readonly (StyleValue | null)[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The rules of one target and the style columns they read. */
|
|
536
|
+
interface GephiTargetRules {
|
|
339
537
|
readonly rules: StyleRule[];
|
|
340
538
|
readonly columns: Map<string, StyleColumn>;
|
|
341
539
|
}
|
|
342
540
|
|
|
343
|
-
/** Preview's "darker" border: each channel of the fill times this (`DependantColor.java` line 55). */
|
|
344
|
-
const GEPHI_BORDER_DARKEN = 0.498;
|
|
345
|
-
/** Preview's border width as a share of the node's drawn width (`SizeUtils.java` lines 11, 37). */
|
|
346
|
-
const GEPHI_BORDER_SHARE = 0.08;
|
|
347
|
-
/** Gephi's node size when a node has none (`AbstractProcessor.java` lines 195-198). */
|
|
348
|
-
const GEPHI_NODE_SIZE = 10;
|
|
349
541
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
542
|
+
* A node color's channels as Gephi holds them: r, g, b and alpha, 0..255.
|
|
543
|
+
* @param fill - the node's fill, or null for Gephi's default
|
|
544
|
+
* @param opacity - its opacity, or null for opaque
|
|
545
|
+
* @returns the channels
|
|
546
|
+
*/
|
|
547
|
+
function nodeChannels(fill: StyleValue | null | undefined, opacity: StyleValue | null | undefined): number[] {
|
|
548
|
+
const n = Number.parseInt((typeof fill === "string" ? fill : GEPHI_NODE_FILL).slice(1), 16);
|
|
549
|
+
const a = typeof opacity === "number" ? Math.round(opacity * 255) : 255;
|
|
550
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, a];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* `#rrggbb` of three channels.
|
|
555
|
+
* @param c - r, g and b, 0..255
|
|
556
|
+
* @returns the color
|
|
557
|
+
*/
|
|
558
|
+
function hexOf(c: readonly number[]): string {
|
|
559
|
+
return `#${c
|
|
560
|
+
.slice(0, 3)
|
|
561
|
+
.map((v) => v.toString(16).padStart(2, "0"))
|
|
562
|
+
.join("")}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* A passthrough rule of a tool default that is itself per element, and its column, when any element takes it.
|
|
567
|
+
* @param out - the target's rules and columns, changed in place
|
|
568
|
+
* @param target - nodes or edges
|
|
569
|
+
* @param pair - the style property and its style column, which is also the rule's id after `<target>.tool.`
|
|
570
|
+
* @param column - the default per element, null where the element has its own value
|
|
571
|
+
* @param reason - why the values are per element
|
|
572
|
+
*/
|
|
573
|
+
function perElementDefault(
|
|
574
|
+
out: GephiTargetRules,
|
|
575
|
+
target: StyleTarget,
|
|
576
|
+
pair: readonly [string, string],
|
|
577
|
+
column: (StyleValue | null)[],
|
|
578
|
+
reason: string,
|
|
579
|
+
): void {
|
|
580
|
+
const [property, key] = pair;
|
|
581
|
+
const elements = column.filter((v) => v !== null).length;
|
|
582
|
+
if (elements === 0) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
out.columns.set(key, column);
|
|
586
|
+
out.rules.push({
|
|
587
|
+
id: `${target}.tool.${key}`,
|
|
588
|
+
target,
|
|
589
|
+
role: "passthrough",
|
|
590
|
+
origin: "tool",
|
|
591
|
+
where: { has: { style: key } },
|
|
592
|
+
map: { [property]: { kind: "passthrough" as const, by: { style: key } } },
|
|
593
|
+
facts: { elements, reason },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** What a GEXF drawing adds to gephiRules(): the file is a GEXF, drawn under these Preview settings. */
|
|
598
|
+
interface GephiDrawing {
|
|
599
|
+
/** The Preview settings, Gephi's defaults where none is given. */
|
|
600
|
+
readonly preview: GephiPreview;
|
|
601
|
+
/** Each edge's own viz color and its alpha, which `edge.color=original` draws. */
|
|
602
|
+
readonly original?: {
|
|
603
|
+
readonly color?: readonly (StyleValue | null)[];
|
|
604
|
+
readonly opacity?: readonly (StyleValue | null)[];
|
|
605
|
+
};
|
|
606
|
+
/** Each edge's label, drawn under `edge.label.show`. */
|
|
607
|
+
readonly edgeLabels?: readonly (StyleValue | null)[];
|
|
608
|
+
/** Whether the node labels Gephi's overlap avoidance hides are already left out (gephiOverlapHidden()). */
|
|
609
|
+
readonly overlapSettled?: boolean;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* A tool rule that sets the same values on every element.
|
|
614
|
+
* @param out - the target's rules, changed in place
|
|
615
|
+
* @param target - nodes or edges
|
|
616
|
+
* @param id - the rule's id after `<target>.tool.`
|
|
617
|
+
* @param set - the values
|
|
353
618
|
*/
|
|
354
|
-
|
|
619
|
+
function everyElement(out: GephiTargetRules, target: StyleTarget, id: string, set: Record<string, StyleValue>): void {
|
|
620
|
+
if (Object.keys(set).length > 0) {
|
|
621
|
+
out.rules.push({
|
|
622
|
+
id: `${target}.tool.${id}`,
|
|
623
|
+
target,
|
|
624
|
+
role: "group",
|
|
625
|
+
origin: "tool",
|
|
626
|
+
where: { all: true },
|
|
627
|
+
set,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
355
631
|
|
|
356
632
|
/**
|
|
357
|
-
* The
|
|
633
|
+
* The rules Gephi's preview defaults (DefaultPreset) make per element, as its SVG export draws every GEXF and GraphML
|
|
634
|
+
* 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
|
|
635
|
+
* tall; each edge in the mix of its two end nodes' colors and alphas, as thick as its weight, with an arrow at the
|
|
636
|
+
* target of a directed edge. An element's own value is kept (a file graphty wrote states every one). A GEXF drawing
|
|
637
|
+
* also merges and drops edges as Gephi's importer does (gephiEdges()) and follows the Preview settings it was drawn
|
|
638
|
+
* with.
|
|
358
639
|
* @param snapshot - the graph
|
|
359
|
-
* @param
|
|
640
|
+
* @param nodes - each node's fill, opacity and width
|
|
641
|
+
* @param edges - each edge's own color and width
|
|
360
642
|
* @param budget - the work budget
|
|
361
|
-
* @
|
|
643
|
+
* @param gexf - the GEXF drawing, or undefined for another format Gephi draws
|
|
644
|
+
* @returns the node and edge rules and the style columns they read
|
|
362
645
|
*/
|
|
363
|
-
function
|
|
646
|
+
export function gephiRules(
|
|
364
647
|
snapshot: GraphSnapshot,
|
|
365
|
-
|
|
648
|
+
nodes: GephiNodeValues,
|
|
649
|
+
edges: GephiEdgeValues,
|
|
366
650
|
budget: StyleReadContext["budget"],
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
651
|
+
gexf?: GephiDrawing,
|
|
652
|
+
): { readonly node: GephiTargetRules; readonly edge: GephiTargetRules } {
|
|
653
|
+
const preview = gexf?.preview ?? new GephiPreview();
|
|
654
|
+
const node: GephiTargetRules = { rules: [], columns: new Map() };
|
|
655
|
+
const edge: GephiTargetRules = { rules: [], columns: new Map() };
|
|
656
|
+
budget.spend("node", snapshot.nodeCount);
|
|
657
|
+
budget.spend("edge", snapshot.edgeCount);
|
|
658
|
+
const channels = Array.from({ length: snapshot.nodeCount }, (_, i) =>
|
|
659
|
+
nodeChannels(nodes.fill?.[i], nodes.fillOpacity?.[i]),
|
|
660
|
+
);
|
|
661
|
+
const labelSizes = nodeRules(node, snapshot, channels, nodes, preview);
|
|
662
|
+
edgeRules(edge, snapshot, channels, nodes, edges, gexf, preview);
|
|
663
|
+
nodeLabelRules(node, channels, labelSizes, preview, gexf?.overlapSettled === true);
|
|
664
|
+
labelBox(node, channels, nodes, preview);
|
|
665
|
+
return { node, edge };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* A node's width, twice its Gephi size.
|
|
670
|
+
* @param nodes - the node values
|
|
671
|
+
* @param i - the node
|
|
672
|
+
* @returns the width
|
|
673
|
+
*/
|
|
674
|
+
function nodeWidth(nodes: GephiNodeValues, i: number): number {
|
|
675
|
+
const w = nodes.width?.[i];
|
|
676
|
+
return typeof w === "number" ? w : GEPHI_NODE_WIDTH;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The node rules: opacity, border and label size.
|
|
681
|
+
* @param node - the node rules, changed in place
|
|
682
|
+
* @param snapshot - the graph
|
|
683
|
+
* @param channels - each node's color channels
|
|
684
|
+
* @param nodes - the node values
|
|
685
|
+
* @param preview - the settings
|
|
686
|
+
* @returns each node's label size
|
|
687
|
+
*/
|
|
688
|
+
function nodeRules(
|
|
689
|
+
node: GephiTargetRules,
|
|
690
|
+
snapshot: GraphSnapshot,
|
|
691
|
+
channels: readonly number[][],
|
|
692
|
+
nodes: GephiNodeValues,
|
|
693
|
+
preview: GephiPreview,
|
|
694
|
+
): number[] {
|
|
695
|
+
const nodeIndices = Array.from({ length: snapshot.nodeCount }, (_, i) => i);
|
|
696
|
+
// a node's alpha is drawn only under node.per.node.opacity, times node.opacity, on its fill and its border alike:
|
|
697
|
+
// a node not opaque is a fill of radius 0.84 s and a separate ring out to s, both at that alpha
|
|
698
|
+
const perNode = preview.on("node.per.node.opacity");
|
|
699
|
+
const alpha = channels.map((c) => ((perNode ? c[3] : 255) / 255) * (preview.num("node.opacity") / 100));
|
|
700
|
+
const translucent = alpha.map((a) => (a < 1 ? a : null));
|
|
701
|
+
perElementDefault(node, "node", ["fillOpacity", "preview-opacity"], translucent, "NODE_OPACITY");
|
|
702
|
+
perElementDefault(node, "node", ["borderOpacity", "preview-border-opacity"], translucent, "NODE_OPACITY");
|
|
703
|
+
// NodeRenderer: the border is DependantColor DARKER by default, half of each channel (+-1 from Gephi's float math)
|
|
704
|
+
const borderMode = preview.text("node.border.color");
|
|
705
|
+
perElementDefault(
|
|
706
|
+
node,
|
|
707
|
+
"node",
|
|
708
|
+
["borderColor", "border-shade"],
|
|
709
|
+
channels.map((c) => hexOf(dependantColor(borderMode, c) ?? c.map((v) => Math.floor(v / 2)))),
|
|
710
|
+
"NODE_FILL_SHADE",
|
|
711
|
+
);
|
|
712
|
+
// 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
|
|
713
|
+
// under node.border.fixed. Its label is the label font's size in proportion to the node's, round(size / 20 * s)
|
|
714
|
+
// (NODE_LABEL_PROPORTIONAL_SIZE; Arial 12 makes round(0.6 s)), never under 1.
|
|
715
|
+
const fixed = preview.on("node.border.fixed") ? preview.num("node.border.width") : null;
|
|
716
|
+
perElementDefault(
|
|
717
|
+
node,
|
|
718
|
+
"node",
|
|
719
|
+
["borderWidth", "size-border"],
|
|
720
|
+
nodeIndices.map((i) => {
|
|
721
|
+
// a fixed border is no wider than the node's size (radius): a size-3 node takes a border of 4 as 3
|
|
722
|
+
return fixed === null ? 0.08 * nodeWidth(nodes, i) : Math.min(fixed, nodeWidth(nodes, i) / 2);
|
|
723
|
+
}),
|
|
724
|
+
"NODE_SIZE",
|
|
725
|
+
);
|
|
726
|
+
const font = preview.font("node");
|
|
727
|
+
const labelSizes = nodeIndices.map((i) =>
|
|
728
|
+
preview.on("node.label.proportinalSize")
|
|
729
|
+
? Math.max(1, Math.round((font.size / 40) * nodeWidth(nodes, i)))
|
|
730
|
+
: font.size,
|
|
731
|
+
);
|
|
732
|
+
perElementDefault(node, "node", ["labelSize", "size-label"], labelSizes, "NODE_SIZE");
|
|
733
|
+
return labelSizes;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* The node label rules the Preview settings make beyond the size: color, outline, font and overlap avoidance.
|
|
738
|
+
* @param node - the node rules, changed in place
|
|
739
|
+
* @param channels - each node's color channels
|
|
740
|
+
* @param sizes - each node's label size
|
|
741
|
+
* @param preview - the settings
|
|
742
|
+
* @param settled - whether the labels overlap avoidance hides are already left out
|
|
743
|
+
*/
|
|
744
|
+
function nodeLabelRules(
|
|
745
|
+
node: GephiTargetRules,
|
|
746
|
+
channels: readonly number[][],
|
|
747
|
+
sizes: readonly number[],
|
|
748
|
+
preview: GephiPreview,
|
|
749
|
+
settled: boolean,
|
|
750
|
+
): void {
|
|
751
|
+
const color = preview.text("node.label.color");
|
|
752
|
+
if (color !== "#000000" && color !== "original") {
|
|
753
|
+
perElementDefault(
|
|
754
|
+
node,
|
|
755
|
+
"node",
|
|
756
|
+
["labelColor", "preview-label-color"],
|
|
757
|
+
channels.map((c) => hexOf(dependantColor(color, c) ?? [0, 0, 0])),
|
|
758
|
+
"NODE_LABEL_COLOR",
|
|
759
|
+
);
|
|
399
760
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
761
|
+
labelOutline(node, "node", channels, sizes, preview);
|
|
762
|
+
const font = preview.font("node");
|
|
763
|
+
everyElement(node, "node", "preview-label", fontValues(font, preview.on("node.label.customFont")));
|
|
764
|
+
// ponytail: where a node has no stored position Gephi's own grid (gephiOverlapHidden()) cannot be run; avoiding
|
|
765
|
+
// overlap, larger first, is that test without the grid's rounding. A rule only while it is on: graphty-element
|
|
766
|
+
// has no value that turns a lower rule's overlap avoidance off again.
|
|
767
|
+
if (preview.on("node.label.avoidOverlap") && !settled) {
|
|
768
|
+
node.columns.set(
|
|
769
|
+
AVOID_OVERLAP_KEY,
|
|
770
|
+
sizes.map(() => true),
|
|
771
|
+
);
|
|
772
|
+
node.rules.push({
|
|
773
|
+
id: "node.tool.label-avoid-overlap",
|
|
404
774
|
target: "node",
|
|
405
|
-
role: "
|
|
775
|
+
role: "group",
|
|
406
776
|
origin: "tool",
|
|
407
|
-
where: { has: {
|
|
408
|
-
|
|
409
|
-
facts: { reason: "NODE_NAME" },
|
|
777
|
+
where: { has: { style: AVOID_OVERLAP_KEY } },
|
|
778
|
+
set: { labelAvoidOverlap: true },
|
|
410
779
|
});
|
|
411
780
|
}
|
|
412
|
-
return { rules, columns };
|
|
413
781
|
}
|
|
414
782
|
|
|
415
783
|
/**
|
|
416
|
-
*
|
|
784
|
+
* Gephi's alpha for a Preview opacity in percent: Java's `Color` keeps it as one of 256 steps, cut down to a whole
|
|
785
|
+
* step (`labels-p10`'s 30 is drawn 76 / 255, not 77).
|
|
786
|
+
* @param percent - the opacity, 0 to 100
|
|
787
|
+
* @returns the alpha, 0 to 1
|
|
788
|
+
*/
|
|
789
|
+
function gephiAlpha(percent: number): number {
|
|
790
|
+
// ponytail: 1e-9 keeps a step that floating point lands a hair under (0.4 x 255) on its step
|
|
791
|
+
return Math.floor((Math.min(Math.max(percent, 0), 100) / 100) * 255 + 1e-9) / 255;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* The label box of `node.label.box`: Gephi strokes an unfilled box round the label, 0.08 of the node's size wide, in
|
|
796
|
+
* the color `node.label.box.color` derives from the node's and at `node.label.box.opacity` (`gexf-specimen-labels-p11`,
|
|
797
|
+
* `-p12`, `-p13`, `-p20`, `-p21`). The box is the text's own bounds -- its advance width and its font's ascent plus
|
|
798
|
+
* descent, with no leading -- grown by the stroke's width, so the stroke's inner edge lies on the text's bounds
|
|
799
|
+
* (`labels-p11`: a 15 px "Label" is 40.47 x 17.46 inside a 2.0 stroke): no padding, a line height of 1.
|
|
800
|
+
* @param node - the node rules, changed in place
|
|
801
|
+
* @param channels - each node's color channels
|
|
802
|
+
* @param nodes - the node values
|
|
803
|
+
* @param preview - the settings
|
|
804
|
+
*/
|
|
805
|
+
function labelBox(
|
|
806
|
+
node: GephiTargetRules,
|
|
807
|
+
channels: readonly number[][],
|
|
808
|
+
nodes: GephiNodeValues,
|
|
809
|
+
preview: GephiPreview,
|
|
810
|
+
): void {
|
|
811
|
+
if (!preview.on("node.label.box")) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const mode = preview.text("node.label.box.color");
|
|
815
|
+
perElementDefault(
|
|
816
|
+
node,
|
|
817
|
+
"node",
|
|
818
|
+
["labelBorderColor", "preview-box-color"],
|
|
819
|
+
channels.map((c) => hexOf(dependantColor(mode, c) ?? c)),
|
|
820
|
+
"LABEL_BOX",
|
|
821
|
+
);
|
|
822
|
+
perElementDefault(
|
|
823
|
+
node,
|
|
824
|
+
"node",
|
|
825
|
+
["labelBorderWidth", "preview-box-width"],
|
|
826
|
+
channels.map((_, i) => 0.04 * nodeWidth(nodes, i)),
|
|
827
|
+
"LABEL_BOX",
|
|
828
|
+
);
|
|
829
|
+
everyElement(node, "node", "preview-box", {
|
|
830
|
+
labelBorderOpacity: gephiAlpha(preview.num("node.label.box.opacity")),
|
|
831
|
+
labelPadding: 0,
|
|
832
|
+
labelLineHeight: 1,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The label font values a custom font sets.
|
|
838
|
+
* @param font - the font
|
|
839
|
+
* @param custom - whether the font is custom
|
|
840
|
+
* @returns the values
|
|
841
|
+
*/
|
|
842
|
+
function fontValues(font: GephiFont, custom: boolean): Record<string, StyleValue> {
|
|
843
|
+
if (!custom) {
|
|
844
|
+
return {};
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
labelFont: font.family,
|
|
848
|
+
...(font.bold ? { labelWeight: "bold" } : {}),
|
|
849
|
+
...(font.italic ? { labelItalic: true } : {}),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* The label outline of `<target>.label.outline.size` and `.color`: a stroke size / 32 of the label's size wide, round
|
|
855
|
+
* each letter, in the color its mode derives from the element's color and at `.outline.opacity` (Gephi's default 80;
|
|
856
|
+
* `gexf-specimen-labels-p9`, `-p10`, `-p17` .. `-p19`, `edge-labels-p5`, `-p6`).
|
|
857
|
+
* @param out - the rules, changed in place
|
|
858
|
+
* @param target - nodes or edges
|
|
859
|
+
* @param colors - each element's color channels
|
|
860
|
+
* @param sizes - each element's label size
|
|
861
|
+
* @param preview - the settings
|
|
862
|
+
*/
|
|
863
|
+
function labelOutline(
|
|
864
|
+
out: GephiTargetRules,
|
|
865
|
+
target: StyleTarget,
|
|
866
|
+
colors: readonly number[][],
|
|
867
|
+
sizes: readonly number[],
|
|
868
|
+
preview: GephiPreview,
|
|
869
|
+
): void {
|
|
870
|
+
const size = preview.num(`${target}.label.outline.size`);
|
|
871
|
+
if (size <= 0) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const mode = preview.text(`${target}.label.outline.color`);
|
|
875
|
+
perElementDefault(
|
|
876
|
+
out,
|
|
877
|
+
target,
|
|
878
|
+
["labelOutlineWidth", "preview-outline-width"],
|
|
879
|
+
sizes.map((s) => (size / 32) * s),
|
|
880
|
+
"LABEL_OUTLINE",
|
|
881
|
+
);
|
|
882
|
+
perElementDefault(
|
|
883
|
+
out,
|
|
884
|
+
target,
|
|
885
|
+
["labelOutlineColor", "preview-outline-color"],
|
|
886
|
+
colors.map((c) => hexOf(dependantColor(mode, c) ?? [255, 255, 255])),
|
|
887
|
+
"LABEL_OUTLINE",
|
|
888
|
+
);
|
|
889
|
+
everyElement(out, target, "preview-outline", {
|
|
890
|
+
labelOutlineOpacity: gephiAlpha(preview.num(`${target}.label.outline.opacity`)),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* The node labels Gephi's label overlap avoidance hides (`node.label.avoidOverlap`, Gephi 0.11
|
|
896
|
+
* NodeLabelRenderer.preProcess): largest font first, each label's box -- its Dialog advance width by its line height,
|
|
897
|
+
* centered on its node -- marks the cells of a `node.label.overlapGridSize` grid over the nodes' bounds, and a label
|
|
898
|
+
* 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
|
|
899
|
+
* size) or the font's, measured in the float size of the last label of the same whole size (Gephi's font cache),
|
|
900
|
+
* its labels taken largest node first.
|
|
901
|
+
* Decided where every node has a stored position; elsewhere graphty-element lays the graph out, so null.
|
|
417
902
|
* @param snapshot - the graph
|
|
418
|
-
* @param
|
|
419
|
-
* @param
|
|
420
|
-
* @param
|
|
421
|
-
* @returns
|
|
903
|
+
* @param labels - each node's label as drawn
|
|
904
|
+
* @param widths - each node's width (twice its size), or undefined for Gephi's default
|
|
905
|
+
* @param preview - the settings
|
|
906
|
+
* @returns whether each label is hidden, or null
|
|
422
907
|
*/
|
|
423
|
-
function
|
|
908
|
+
function gephiOverlapHidden(
|
|
424
909
|
snapshot: GraphSnapshot,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
):
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
910
|
+
labels: readonly (StyleValue | null)[],
|
|
911
|
+
widths: readonly (StyleValue | null)[] | undefined,
|
|
912
|
+
preview: GephiPreview,
|
|
913
|
+
): boolean[] | null {
|
|
914
|
+
const centers = gephiCenters(snapshot);
|
|
915
|
+
if (centers.includes(null)) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const f = Math.fround;
|
|
919
|
+
const grid = preview.num("node.label.overlapGridSize");
|
|
920
|
+
const proportional = preview.on("node.label.proportinalSize");
|
|
921
|
+
const base = f(preview.font("node").size * preview.num("node.label.scale"));
|
|
922
|
+
const nodeSizes = centers.map((_, i) => {
|
|
923
|
+
const w = widths?.[i];
|
|
924
|
+
return f((typeof w === "number" ? w : GEPHI_NODE_WIDTH) / 2);
|
|
925
|
+
});
|
|
926
|
+
const sizes = nodeSizes.map((nodeSize) =>
|
|
927
|
+
proportional ? f(f(base * nodeSize) * f(0.05)) : f(base * f(1 + f(nodeSize * f(0.00001)))),
|
|
928
|
+
);
|
|
929
|
+
// Gephi's label items come largest node first (file order among equals; measured on six corpus files)
|
|
930
|
+
const items = sizes.map((_, i) => i).sort((a, b) => nodeSizes[b] - nodeSizes[a]);
|
|
931
|
+
// Font.deriveFont(size).getSize() rounds; the cache keeps the last font of each whole size
|
|
932
|
+
const cached = new Map<number, number>();
|
|
933
|
+
items.forEach((i) => cached.set(Math.round(sizes[i]), sizes[i]));
|
|
934
|
+
// NodeItem Y is the file's y turned over
|
|
935
|
+
const at = centers.map((c) => [c?.[0] ?? 0, -(c?.[1] ?? 0)] as const);
|
|
936
|
+
// loops, not Math.min(...): a spread of every node overflows the call stack on a large graph
|
|
937
|
+
let [loX, loY, hiX, hiY] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
938
|
+
for (const [x, y] of at) {
|
|
939
|
+
[loX, loY, hiX, hiY] = [Math.min(loX, x), Math.min(loY, y), Math.max(hiX, x), Math.max(hiY, y)];
|
|
940
|
+
}
|
|
941
|
+
const [minX, minY] = [loX - grid, loY - grid];
|
|
942
|
+
const cols = Math.ceil((hiX + grid - minX) / grid);
|
|
943
|
+
const rows = Math.ceil((hiY + grid - minY) / grid);
|
|
944
|
+
const taken = new Set<number>();
|
|
945
|
+
const hidden = labels.map(() => false);
|
|
946
|
+
const order = [...items].sort((a, b) => sizes[b] - sizes[a]);
|
|
947
|
+
for (const i of order) {
|
|
948
|
+
const label = labels[i];
|
|
949
|
+
if (typeof label !== "string" || label.trim() === "") {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const size = cached.get(Math.round(sizes[i])) ?? sizes[i];
|
|
953
|
+
const half = [(dialogWidth(label) * size) / 2, (DIALOG_LINE_HEIGHT * size) / 2] as const;
|
|
954
|
+
const cells = gridCells(
|
|
955
|
+
{ min: [minX, minY], size: grid, cols, rows },
|
|
956
|
+
[at[i][0] - half[0], at[i][1] - half[1]],
|
|
957
|
+
[at[i][0] + half[0], at[i][1] + half[1]],
|
|
958
|
+
);
|
|
959
|
+
if (cells.some((k) => taken.has(k))) {
|
|
960
|
+
hidden[i] = true;
|
|
961
|
+
} else {
|
|
962
|
+
cells.forEach((k) => taken.add(k));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return hidden;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* The cells of a grid a box covers, as Gephi's label grid counts them (each corner's cell truncated, kept on the grid).
|
|
970
|
+
* @param grid - the grid: its lowest corner, cell size and cell counts
|
|
971
|
+
* @param grid.min - its lowest x and y
|
|
972
|
+
* @param grid.size - its cell size
|
|
973
|
+
* @param grid.cols - its columns
|
|
974
|
+
* @param grid.rows - its rows
|
|
975
|
+
* @param lo - the box's lowest x and y
|
|
976
|
+
* @param hi - the box's highest x and y
|
|
977
|
+
* @returns the cell indices, row by row
|
|
978
|
+
*/
|
|
979
|
+
function gridCells(
|
|
980
|
+
grid: {
|
|
981
|
+
readonly min: readonly [number, number];
|
|
982
|
+
readonly size: number;
|
|
983
|
+
readonly cols: number;
|
|
984
|
+
readonly rows: number;
|
|
985
|
+
},
|
|
986
|
+
lo: readonly [number, number],
|
|
987
|
+
hi: readonly [number, number],
|
|
988
|
+
): number[] {
|
|
989
|
+
const cell = (v: number, axis: 0 | 1): number =>
|
|
990
|
+
Math.min((axis === 0 ? grid.cols : grid.rows) - 1, Math.max(0, Math.trunc((v - grid.min[axis]) / grid.size)));
|
|
991
|
+
const cells: number[] = [];
|
|
992
|
+
for (let r = cell(lo[1], 1); r <= cell(hi[1], 1); r++) {
|
|
993
|
+
for (let c = cell(lo[0], 0); c <= cell(hi[0], 0); c++) {
|
|
994
|
+
cells.push(r * grid.cols + c);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return cells;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** Gephi's `edge.arc-curveness`: a curved edge is an arc of radius its nodes' distance over it (code only, 1.2). */
|
|
1001
|
+
const GEPHI_ARC_CURVENESS = 1.2;
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Each node's stored center, its viz:position; null without one (graphty-element lays such a node out, where Gephi
|
|
1005
|
+
* would put it at (0, 0)).
|
|
1006
|
+
* @param snapshot - the graph
|
|
1007
|
+
* @returns x and y by node
|
|
1008
|
+
*/
|
|
1009
|
+
function gephiCenters(snapshot: GraphSnapshot): ([number, number] | null)[] {
|
|
1010
|
+
const position = snapshot.nodes.byRole("position");
|
|
1011
|
+
return Array.from({ length: snapshot.nodeCount }, (_, i) => {
|
|
1012
|
+
const v = position?.isSet(i) === true ? (position.value(i) as ArrayLike<number>) : null;
|
|
1013
|
+
return v === null ? null : [v[0] || 0, v[1] || 0];
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* 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
|
|
1019
|
+
* as that end's node radius (its size), plus `edge.radius`, plus at the target of a directed edge its arrowhead
|
|
1020
|
+
* (`arrow.size` x its thickness); where the two cuts meet or pass each other, or a cut is longer than the arc, the
|
|
1021
|
+
* nodes swallow the edge and no line is drawn (Gephi 0.11 EdgeRenderer.CurvedEdgeRenderer.Helper,
|
|
1022
|
+
* computeTruncateAngle; gexf-cns-wos: 24 edges between overlapping nodes). Its arrowhead is still drawn, unless its
|
|
1023
|
+
* nodes are at one place (ArrowRenderer).
|
|
1024
|
+
* @param snapshot - the graph
|
|
1025
|
+
* @param nodes - the node values (their widths)
|
|
1026
|
+
* @param directed - whether each edge is drawn directed
|
|
1027
|
+
* @param thickness - each edge's drawn thickness
|
|
1028
|
+
* @param preview - the settings
|
|
1029
|
+
* @returns for each edge, "line" when only its line is swallowed, "all" when its nodes are at one place, else null
|
|
1030
|
+
*/
|
|
1031
|
+
function swallowedEdges(
|
|
1032
|
+
snapshot: GraphSnapshot,
|
|
1033
|
+
nodes: GephiNodeValues,
|
|
1034
|
+
directed: readonly boolean[],
|
|
1035
|
+
thickness: readonly number[],
|
|
1036
|
+
preview: GephiPreview,
|
|
1037
|
+
): ("line" | "all" | null)[] {
|
|
1038
|
+
const centers = gephiCenters(snapshot);
|
|
1039
|
+
const edgeRadius = preview.num("edge.radius");
|
|
1040
|
+
const arrowSize = preview.num("arrow.size");
|
|
1041
|
+
const arc = 2 * Math.asin(Math.min(1, GEPHI_ARC_CURVENESS / 2));
|
|
1042
|
+
return Array.from({ length: snapshot.edgeCount }, (_, e) => {
|
|
1043
|
+
const [a, b] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
|
|
1044
|
+
const [pa, pb] = [centers[a], centers[b]];
|
|
1045
|
+
if (a === b || pa === null || pb === null) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
const d = Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
|
|
1049
|
+
if (d === 0) {
|
|
1050
|
+
return "all";
|
|
1051
|
+
}
|
|
1052
|
+
const r = d / GEPHI_ARC_CURVENESS;
|
|
1053
|
+
// the angle a cut of chord length c takes off the arc; past the arc's length, more than the whole arc
|
|
1054
|
+
const cut = (c: number): number =>
|
|
1055
|
+
c >= r * arc ? Infinity : 2 * Math.atan2(c / 2, Math.sqrt(r * r - (c / 2) * (c / 2)));
|
|
1056
|
+
const source = edgeRadius + nodeWidth(nodes, a) / 2;
|
|
1057
|
+
const target = edgeRadius + nodeWidth(nodes, b) / 2 + (directed[e] ? arrowSize * thickness[e] : 0);
|
|
1058
|
+
return cut(source) + cut(target) >= arc ? "line" : null;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** How many points a Gephi self-loop's circle is drawn through. */
|
|
1063
|
+
const SELF_LOOP_STEPS = 96;
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Gephi's self-loops as bends round their node: a circle up and to the right of it, centered `r` right of and `r`
|
|
1067
|
+
* 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
|
|
1068
|
+
* 1.3 times the edge's weight (gephi PreviewPlugin EdgeRenderer, SelfLoopEdgeRenderer; measured on the GEXF specimen
|
|
1069
|
+
* sheets and gexf-graphology-rio: r = 0.5435 x the drawn radius + 0.33 x the stroke). The circle runs from the side
|
|
1070
|
+
* facing the node all the way round; its part inside the node is not drawn.
|
|
1071
|
+
* @param snapshot - the graph
|
|
1072
|
+
* @param thickness - each edge's drawn thickness
|
|
1073
|
+
* @param hidden - whether each edge is not drawn
|
|
1074
|
+
* @returns each loop's bends, as fractions of its node's half size; null for any other edge
|
|
1075
|
+
*/
|
|
1076
|
+
function selfLoopBends(
|
|
1077
|
+
snapshot: GraphSnapshot,
|
|
1078
|
+
thickness: readonly number[],
|
|
1079
|
+
hidden: readonly boolean[],
|
|
1080
|
+
): (StyleValue | null)[] {
|
|
1081
|
+
const sizes = vizColumn(snapshot, "node", "size");
|
|
1082
|
+
const sizeOf = (n: number): number => {
|
|
1083
|
+
const v = sizes === null ? null : Number(plainCell(snapshot.nodes.value(sizes, n)));
|
|
1084
|
+
return v !== null && Number.isFinite(v) && v > 0 ? v : GEPHI_NODE_WIDTH / 2;
|
|
1085
|
+
};
|
|
1086
|
+
const fraction = (v: number): string => String(Math.round(v * 1e4) / 1e4);
|
|
1087
|
+
return Array.from({ length: snapshot.edgeCount }, (_, e) => {
|
|
1088
|
+
const n = snapshot.edgeSource(e);
|
|
1089
|
+
if (n !== snapshot.edgeTarget(e) || hidden[e]) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
const size = sizeOf(n);
|
|
1093
|
+
const r = (0.5 * size + 0.33 * SELF_LOOP_THICKNESS * thickness[e]) / size;
|
|
1094
|
+
const points: string[] = [];
|
|
1095
|
+
for (let k = 0; k <= SELF_LOOP_STEPS; k++) {
|
|
1096
|
+
// from the point facing the node's center (225 degrees), clockwise all the way round
|
|
1097
|
+
const a = (5 * Math.PI) / 4 - (2 * Math.PI * k) / SELF_LOOP_STEPS;
|
|
1098
|
+
points.push(`${fraction(r + r * Math.cos(a))},${fraction(r + r * Math.sin(a))}`);
|
|
1099
|
+
}
|
|
1100
|
+
return points.join(" ");
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* The edge rules: arrows, thickness, the edges not drawn, color and opacity, curves and labels.
|
|
1106
|
+
* @param edge - the edge rules, changed in place
|
|
1107
|
+
* @param snapshot - the graph
|
|
1108
|
+
* @param channels - each node's color channels
|
|
1109
|
+
* @param nodes - the node values (their widths)
|
|
1110
|
+
* @param edges - each edge's own color and width
|
|
1111
|
+
* @param gexf - the GEXF drawing, or undefined
|
|
1112
|
+
* @param preview - the settings
|
|
1113
|
+
*/
|
|
1114
|
+
function edgeRules(
|
|
1115
|
+
edge: GephiTargetRules,
|
|
1116
|
+
snapshot: GraphSnapshot,
|
|
1117
|
+
channels: readonly number[][],
|
|
1118
|
+
nodes: GephiNodeValues,
|
|
1119
|
+
edges: GephiEdgeValues,
|
|
1120
|
+
gexf: GephiDrawing | undefined,
|
|
1121
|
+
preview: GephiPreview,
|
|
1122
|
+
): void {
|
|
1123
|
+
const drawn = gephiEdges(snapshot, gexf !== undefined);
|
|
1124
|
+
const arrowSize = preview.num("arrow.size");
|
|
1125
|
+
if (drawn.directed.some(Boolean) && arrowSize > 0) {
|
|
1126
|
+
edge.columns.set(
|
|
434
1127
|
DIRECTED_KEY,
|
|
435
|
-
directed.map((d) => (d ? true : null)),
|
|
1128
|
+
drawn.directed.map((d) => (d ? true : null)),
|
|
436
1129
|
);
|
|
437
|
-
|
|
1130
|
+
// arrow.size scales the arrowhead from its default 3 (gexf-specimen-arrows-p2); 0 draws none (-p3)
|
|
1131
|
+
edge.rules.push({
|
|
438
1132
|
id: "edge.tool.arrow",
|
|
439
1133
|
target: "edge",
|
|
440
1134
|
role: "group",
|
|
441
1135
|
origin: "tool",
|
|
442
1136
|
where: { has: { style: DIRECTED_KEY } },
|
|
443
|
-
set: { targetArrow: "triangle" },
|
|
1137
|
+
set: { targetArrow: "triangle", ...(arrowSize === 3 ? {} : { arrowSize: arrowSize / 3 }) },
|
|
444
1138
|
});
|
|
445
1139
|
}
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1140
|
+
// Gephi draws an edge as thick as its weight (gephiEdges(): merged and shifted); a width of its own is the file's
|
|
1141
|
+
const hidden = preview.on("edge.show") ? drawn.hidden : drawn.hidden.map(() => true);
|
|
1142
|
+
const thickness = edgeThickness(
|
|
1143
|
+
drawn.weight,
|
|
1144
|
+
hidden.map((h) => !h),
|
|
1145
|
+
preview,
|
|
452
1146
|
);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1147
|
+
perElementDefault(
|
|
1148
|
+
edge,
|
|
1149
|
+
"edge",
|
|
1150
|
+
WEIGHT_WIDTH,
|
|
1151
|
+
// Gephi's self-loop is 1.3 times as thick (measured: weight 1 draws 1.3, 3.25 draws 4.225)
|
|
1152
|
+
withoutOwn(edges.width, snapshot.edgeCount, (e) =>
|
|
1153
|
+
snapshot.edgeSource(e) === snapshot.edgeTarget(e) ? SELF_LOOP_THICKNESS * thickness[e] : thickness[e],
|
|
1154
|
+
),
|
|
1155
|
+
"EDGE_WEIGHT",
|
|
1156
|
+
);
|
|
1157
|
+
perElementDefault(edge, "edge", ["bends", "self-loop"], selfLoopBends(snapshot, thickness, hidden), "NODE_SIZE");
|
|
1158
|
+
// a curved edge its nodes (and its arrowhead) swallow draws no line; its arrowhead is still drawn
|
|
1159
|
+
const swallowed = preview.on("edge.curved")
|
|
1160
|
+
? swallowedEdges(snapshot, nodes, drawn.directed, thickness, preview)
|
|
1161
|
+
: hidden.map(() => null);
|
|
1162
|
+
// the edges Gephi does not draw: one of another direction than the graph's, one merged into a parallel edge, or a
|
|
1163
|
+
// swallowed one with no arrowhead to keep
|
|
1164
|
+
hidden.forEach((h, e) => {
|
|
1165
|
+
const arrow = drawn.directed[e] && arrowSize > 0 && swallowed[e] === "line";
|
|
1166
|
+
hidden[e] = h || (swallowed[e] !== null && !arrow);
|
|
464
1167
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1168
|
+
if (hidden.some(Boolean)) {
|
|
1169
|
+
edge.columns.set(
|
|
1170
|
+
HIDDEN_KEY,
|
|
1171
|
+
hidden.map((h) => (h ? true : null)),
|
|
1172
|
+
);
|
|
1173
|
+
edge.rules.push({
|
|
1174
|
+
id: "edge.tool.gephi-hidden",
|
|
470
1175
|
target: "edge",
|
|
471
|
-
role: "
|
|
1176
|
+
role: "group",
|
|
472
1177
|
origin: "tool",
|
|
473
|
-
where: { has: { style:
|
|
474
|
-
|
|
475
|
-
facts: { elements: unsized, reason: "EDGE_WEIGHT" },
|
|
1178
|
+
where: { has: { style: HIDDEN_KEY } },
|
|
1179
|
+
set: { visible: false },
|
|
476
1180
|
});
|
|
477
1181
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1182
|
+
const colors = edgeColors(snapshot, channels, gexf, preview);
|
|
1183
|
+
perElementDefault(
|
|
1184
|
+
edge,
|
|
1185
|
+
"edge",
|
|
1186
|
+
["color", MIXED_COLOR_KEY],
|
|
1187
|
+
withoutOwn(edges.color, snapshot.edgeCount, (e) => hexOf(colors[e])),
|
|
1188
|
+
"END_NODE_COLORS",
|
|
1189
|
+
);
|
|
1190
|
+
perElementDefault(
|
|
1191
|
+
edge,
|
|
1192
|
+
"edge",
|
|
1193
|
+
["opacity", "end-opacity"],
|
|
1194
|
+
withoutOwn(edges.color, snapshot.edgeCount, (e) => colors[e][3] / 255).map((v, e) => {
|
|
1195
|
+
// a swallowed edge Gephi draws only the arrowhead of draws no line
|
|
1196
|
+
if (swallowed[e] !== null && !hidden[e]) {
|
|
1197
|
+
return 0;
|
|
1198
|
+
}
|
|
1199
|
+
return v === 1 ? null : v;
|
|
1200
|
+
}),
|
|
1201
|
+
"END_NODE_COLORS",
|
|
1202
|
+
);
|
|
1203
|
+
everyElement(edge, "edge", "preview-curve", preview.on("edge.curved") ? {} : { curved: false });
|
|
1204
|
+
// Gephi's `edge.radius` leaves that much empty between each node and its edges' ends, arrowheads too
|
|
1205
|
+
const nodeGap = preview.num("edge.radius");
|
|
1206
|
+
everyElement(edge, "edge", "preview-radius", nodeGap > 0 ? { nodeGap } : {});
|
|
1207
|
+
edgeLabelRules(edge, colors, gexf, preview);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Each edge's color and alpha (0..255) under `edge.color` and `edge.opacity`: MIXED (the default) is each channel,
|
|
1212
|
+
* alpha too, the truncated mean of the two end nodes'; `source` and `target` are that node's; `original` is the edge's
|
|
1213
|
+
* own viz color, black without one; `#rrggbb` is that color, opaque (`gexf-specimen-edges-lines-p3` .. `-p7`).
|
|
1214
|
+
* graphty-element cannot bind an edge's color to its nodes' drawn colors, so the color is frozen here.
|
|
1215
|
+
* @param snapshot - the graph
|
|
1216
|
+
* @param channels - each node's color channels
|
|
1217
|
+
* @param gexf - the GEXF drawing, or undefined
|
|
1218
|
+
* @param preview - the settings
|
|
1219
|
+
* @returns each edge's r, g, b and alpha
|
|
1220
|
+
*/
|
|
1221
|
+
function edgeColors(
|
|
1222
|
+
snapshot: GraphSnapshot,
|
|
1223
|
+
channels: readonly number[][],
|
|
1224
|
+
gexf: GephiDrawing | undefined,
|
|
1225
|
+
preview: GephiPreview,
|
|
1226
|
+
): number[][] {
|
|
1227
|
+
const mode = preview.text("edge.color");
|
|
1228
|
+
const factor = preview.num("edge.opacity") / 100;
|
|
1229
|
+
const own = (e: number): number[] => {
|
|
1230
|
+
const c = gexf?.original?.color?.[e];
|
|
1231
|
+
const a = gexf?.original?.opacity?.[e];
|
|
1232
|
+
return nodeChannels(typeof c === "string" ? c : GEPHI_NODE_FILL, typeof a === "number" ? a : null);
|
|
1233
|
+
};
|
|
1234
|
+
const custom = dependantColor(mode, [0, 0, 0]);
|
|
1235
|
+
return Array.from({ length: snapshot.edgeCount }, (_, e) => {
|
|
1236
|
+
const s = channels[snapshot.edgeSource(e)];
|
|
1237
|
+
const t = channels[snapshot.edgeTarget(e)];
|
|
1238
|
+
let c: number[];
|
|
1239
|
+
if (mode === "source") {
|
|
1240
|
+
c = [...s];
|
|
1241
|
+
} else if (mode === "target") {
|
|
1242
|
+
c = [...t];
|
|
1243
|
+
} else if (mode === "original") {
|
|
1244
|
+
c = own(e);
|
|
1245
|
+
} else if (mode.startsWith("#") && custom !== null) {
|
|
1246
|
+
c = [...custom, 255];
|
|
1247
|
+
} else {
|
|
1248
|
+
c = s.map((v, k) => Math.floor((v + t[k]) / 2));
|
|
1249
|
+
}
|
|
1250
|
+
return factor === 1 ? c : [c[0], c[1], c[2], c[3] * factor];
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* The edge label rules of `edge.label.show`: the font's size (not in proportion to anything), color, outline and
|
|
1256
|
+
* font (`gexf-specimen-edge-labels-p2` .. `-p15`).
|
|
1257
|
+
* @param edge - the edge rules, changed in place
|
|
1258
|
+
* @param colors - each edge's color and alpha
|
|
1259
|
+
* @param gexf - the GEXF drawing, or undefined
|
|
1260
|
+
* @param preview - the settings
|
|
1261
|
+
*/
|
|
1262
|
+
function edgeLabelRules(
|
|
1263
|
+
edge: GephiTargetRules,
|
|
1264
|
+
colors: readonly number[][],
|
|
1265
|
+
gexf: GephiDrawing | undefined,
|
|
1266
|
+
preview: GephiPreview,
|
|
1267
|
+
): void {
|
|
1268
|
+
if (gexf?.edgeLabels === undefined || !preview.on("edge.label.show")) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const font = preview.font("edge");
|
|
1272
|
+
const color = preview.text("edge.label.color");
|
|
1273
|
+
if (color !== "original") {
|
|
1274
|
+
perElementDefault(
|
|
1275
|
+
edge,
|
|
1276
|
+
"edge",
|
|
1277
|
+
["labelColor", "preview-label-color"],
|
|
1278
|
+
colors.map((c) => hexOf(dependantColor(color, c) ?? [0, 0, 0])),
|
|
1279
|
+
"EDGE_LABEL_COLOR",
|
|
1280
|
+
);
|
|
489
1281
|
}
|
|
490
|
-
|
|
1282
|
+
labelOutline(
|
|
1283
|
+
edge,
|
|
1284
|
+
"edge",
|
|
1285
|
+
colors,
|
|
1286
|
+
colors.map(() => font.size),
|
|
1287
|
+
preview,
|
|
1288
|
+
);
|
|
1289
|
+
// Gephi centers an edge label on its line's middle (gexf-specimen-edge-labels-p2: 0.04 of the font off it)
|
|
1290
|
+
everyElement(edge, "edge", "preview-label", {
|
|
1291
|
+
labelSize: font.size,
|
|
1292
|
+
labelPosition: "center",
|
|
1293
|
+
...fontValues(font, preview.on("edge.label.customFont")),
|
|
1294
|
+
});
|
|
491
1295
|
}
|
|
492
1296
|
|
|
493
1297
|
/**
|
|
@@ -506,24 +1310,6 @@ function gephiMix(a: string, b: string): string {
|
|
|
506
1310
|
return `#${out.toString(16).padStart(6, "0")}`;
|
|
507
1311
|
}
|
|
508
1312
|
|
|
509
|
-
/**
|
|
510
|
-
* Preview's "darker" border color: each channel times 0.498, rounded (`ColorUtils.java` lines 22-34).
|
|
511
|
-
* @param c - the fill, `#rrggbb`
|
|
512
|
-
* @returns the border color
|
|
513
|
-
*/
|
|
514
|
-
function gephiDarker(c: string): string {
|
|
515
|
-
const x = Number.parseInt(c.slice(1), 16);
|
|
516
|
-
let out = 0;
|
|
517
|
-
for (const shift of [16, 8, 0]) {
|
|
518
|
-
out |= Math.min(255, Math.round(((x >> shift) & 255) * GEPHI_BORDER_DARKEN)) << shift;
|
|
519
|
-
}
|
|
520
|
-
return `#${out.toString(16).padStart(6, "0")}`;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const mix = (a: StyleValue, b: StyleValue): StyleValue => gephiMix(String(a), String(b));
|
|
524
|
-
const darker = (c: StyleValue): StyleValue => gephiDarker(String(c));
|
|
525
|
-
const round6 = (x: number): number => Math.round(x * 1e6) / 1e6;
|
|
526
|
-
|
|
527
1313
|
/**
|
|
528
1314
|
* The edge whose source Gephi draws for each edge: an undirected edge in a directed graph is one edge in the file
|
|
529
1315
|
* and two halves here, and the second half takes the first half's.
|
|
@@ -557,10 +1343,10 @@ function withoutOwn(
|
|
|
557
1343
|
}
|
|
558
1344
|
|
|
559
1345
|
/**
|
|
560
|
-
* The styles of a GEXF file
|
|
561
|
-
* (black size-10 discs
|
|
562
|
-
*
|
|
563
|
-
*
|
|
1346
|
+
* The styles of a GEXF file as Gephi's preview draws it: each node's label, fill and size, Gephi's defaults beneath
|
|
1347
|
+
* (black size-10 discs, curved edges) and its per-element defaults (gephiRules()); the viz values Gephi does not draw
|
|
1348
|
+
* are left out (withoutUndrawn()); plus a `graphty.styles` block the file holds in `<meta>`, whose file keeps every
|
|
1349
|
+
* value.
|
|
564
1350
|
* @param snapshot - the graph the import built
|
|
565
1351
|
* @param context - the work budget and the block text the import kept
|
|
566
1352
|
* @returns the styles
|
|
@@ -569,18 +1355,47 @@ export function gexfStyles(snapshot: GraphSnapshot, context: StyleReadContext):
|
|
|
569
1355
|
const notes = new StyleNotes();
|
|
570
1356
|
const node = vizValues(snapshot, "node", notes, context);
|
|
571
1357
|
const edge = vizValues(snapshot, "edge", notes, context);
|
|
572
|
-
const block
|
|
573
|
-
|
|
1358
|
+
const { block, preview } = keptOf(context.kept);
|
|
1359
|
+
const nodeOpacity = node.fillOpacity;
|
|
1360
|
+
const original = { color: edge.color, opacity: edge.opacity };
|
|
1361
|
+
if (block === null) {
|
|
1362
|
+
withoutUndrawn(node, edge);
|
|
1363
|
+
} else {
|
|
574
1364
|
exactFromBlock(snapshot, block, node, edge);
|
|
575
1365
|
}
|
|
576
|
-
const
|
|
1366
|
+
const labels = drawnLabels(node.label, preview, "node", block !== null);
|
|
1367
|
+
const overlapHidden =
|
|
1368
|
+
block === null && labels !== undefined && preview.on("node.label.avoidOverlap")
|
|
1369
|
+
? gephiOverlapHidden(snapshot, labels, node.width, preview)
|
|
1370
|
+
: null;
|
|
1371
|
+
overlapHidden?.forEach((hide, i) => {
|
|
1372
|
+
if (hide && labels !== undefined) {
|
|
1373
|
+
labels[i] = null;
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
if (labels === undefined) {
|
|
1377
|
+
delete node.label;
|
|
1378
|
+
} else {
|
|
1379
|
+
node.label = labels;
|
|
1380
|
+
}
|
|
1381
|
+
const edgeLabels = drawnLabels(edgeLabelValues(snapshot), preview, "edge", block !== null);
|
|
1382
|
+
if (edgeLabels !== undefined) {
|
|
1383
|
+
edge.label = edgeLabels;
|
|
1384
|
+
}
|
|
1385
|
+
const rules = gephiRules(
|
|
1386
|
+
snapshot,
|
|
1387
|
+
{ fill: node.fill, fillOpacity: nodeOpacity, width: node.width },
|
|
1388
|
+
{ color: edge.color, width: edge.width },
|
|
1389
|
+
context.budget,
|
|
1390
|
+
{ preview, original, edgeLabels, overlapSettled: overlapHidden !== null },
|
|
1391
|
+
);
|
|
577
1392
|
return {
|
|
578
1393
|
canonical: true,
|
|
579
1394
|
dialect: "gexf",
|
|
580
1395
|
tool: "gephi",
|
|
581
1396
|
units: "relative",
|
|
582
|
-
node: { values: node, ...
|
|
583
|
-
edge: { values: edge, ...
|
|
1397
|
+
node: { values: node, ...rules.node },
|
|
1398
|
+
edge: { values: edge, ...rules.edge },
|
|
584
1399
|
notes: notes.list(),
|
|
585
1400
|
opacityInColor: true,
|
|
586
1401
|
block,
|
|
@@ -591,6 +1406,61 @@ export function gexfStyles(snapshot: GraphSnapshot, context: StyleReadContext):
|
|
|
591
1406
|
};
|
|
592
1407
|
}
|
|
593
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* What the import kept for the styles: the `graphty.styles` block, and the Preview settings of the `gephiPreview`
|
|
1411
|
+
* option.
|
|
1412
|
+
* @param kept - the kept value: the block's text, or `{ block, gephiPreview }`
|
|
1413
|
+
* @returns the block (null without one) and the Preview settings
|
|
1414
|
+
*/
|
|
1415
|
+
function keptOf(kept: unknown): { readonly block: string | null; readonly preview: GephiPreview } {
|
|
1416
|
+
if (typeof kept === "string") {
|
|
1417
|
+
return { block: kept, preview: new GephiPreview() };
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof kept === "object" && kept !== null) {
|
|
1420
|
+
const { block, gephiPreview } = kept as { block?: unknown; gephiPreview?: Record<string, string> };
|
|
1421
|
+
return { block: typeof block === "string" ? block : null, preview: new GephiPreview(gephiPreview) };
|
|
1422
|
+
}
|
|
1423
|
+
return { block: null, preview: new GephiPreview() };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Each edge's `label` attribute.
|
|
1428
|
+
* @param snapshot - the graph
|
|
1429
|
+
* @returns the labels, or undefined when no edge has one
|
|
1430
|
+
*/
|
|
1431
|
+
function edgeLabelValues(snapshot: GraphSnapshot): (StyleValue | null)[] | undefined {
|
|
1432
|
+
const col = snapshot.edges.byRole("label");
|
|
1433
|
+
if (col === null) {
|
|
1434
|
+
return undefined;
|
|
1435
|
+
}
|
|
1436
|
+
return Array.from({ length: snapshot.edgeCount }, (_, e) => {
|
|
1437
|
+
const v = plainCell(col.value(e));
|
|
1438
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Leaves out the viz values Gephi does not draw, for a file graphty did not write: its preview draws every node an
|
|
1444
|
+
* opaque disc (a node's alpha only tints its edges) and every edge a solid line in its end nodes' mixed color, as
|
|
1445
|
+
* thick as its weight (graphstore keeps no edge thickness or shape, and EdgeColor MIXED ignores an edge's color).
|
|
1446
|
+
* Every `viz:shape` is drawn a disc, an `image` shape too: Gephi Toolkit 0.11.3 draws no picture
|
|
1447
|
+
* (`gexf-specimen-images-charts`: a data: URI and a web URL both drawn as the node's colored disc).
|
|
1448
|
+
* @param node - the node values, changed in place
|
|
1449
|
+
* @param edge - the edge values, changed in place
|
|
1450
|
+
*/
|
|
1451
|
+
function withoutUndrawn(
|
|
1452
|
+
node: Record<string, (StyleValue | null)[]>,
|
|
1453
|
+
edge: Record<string, (StyleValue | null)[]>,
|
|
1454
|
+
): void {
|
|
1455
|
+
delete node.shape;
|
|
1456
|
+
delete node.image;
|
|
1457
|
+
delete node.fillOpacity;
|
|
1458
|
+
delete edge.color;
|
|
1459
|
+
delete edge.opacity;
|
|
1460
|
+
delete edge.width;
|
|
1461
|
+
delete edge.lineStyle;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
594
1464
|
/**
|
|
595
1465
|
* For a file graphty wrote: where a viz value is the nearest GEXF value of what the `graphty.styles` block gives (a
|
|
596
1466
|
* hexagon written as a disc, a width and height written as one size), the block's exact value is the truth, so a
|
|
@@ -607,49 +1477,33 @@ function exactFromBlock(
|
|
|
607
1477
|
node: Record<string, (StyleValue | null)[]>,
|
|
608
1478
|
edge: Record<string, (StyleValue | null)[]>,
|
|
609
1479
|
): void {
|
|
610
|
-
const
|
|
611
|
-
const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "gexf") : null;
|
|
612
|
-
if (ex === null) {
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
const exact = (
|
|
616
|
-
values: Record<string, (StyleValue | null)[]>,
|
|
617
|
-
target: StyleTarget,
|
|
618
|
-
property: string,
|
|
619
|
-
written: (v: StyleValue, i: number) => StyleValue | null,
|
|
620
|
-
): void => {
|
|
621
|
-
const native = values[property];
|
|
622
|
-
if (native === undefined) {
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
const want = resolveStyles(ex, target, property as never, snapshot);
|
|
626
|
-
for (let i = 0; i < native.length; i++) {
|
|
627
|
-
const w = want[i];
|
|
628
|
-
// a size is written from the width and the height together: one the block leaves unset reads back too
|
|
629
|
-
const writable = w !== null || property === "width" || property === "height";
|
|
630
|
-
if (writable && native[i] !== w && native[i] !== null && native[i] === written(w as StyleValue, i)) {
|
|
631
|
-
native[i] = w;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
const width = resolveStyles(ex, "node", "width", snapshot);
|
|
636
|
-
const height = resolveStyles(ex, "node", "height", snapshot);
|
|
637
|
-
const size = (i: number): StyleValue | null => {
|
|
638
|
-
const w = width[i];
|
|
639
|
-
const h = height[i];
|
|
640
|
-
return typeof w === "number" && typeof h === "number" ? Math.max(w, h) : (w ?? h);
|
|
641
|
-
};
|
|
642
|
-
const read = new Map([
|
|
1480
|
+
const shapeRead = new Map([
|
|
643
1481
|
["disc", "ellipse"],
|
|
644
1482
|
["square", "rectangle"],
|
|
645
1483
|
]);
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1484
|
+
const size = (want: ElementValues): StyleValue | null => {
|
|
1485
|
+
const w = want("width");
|
|
1486
|
+
const h = want("height");
|
|
1487
|
+
return typeof w === "number" && typeof h === "number" ? Math.max(w, h) : (w ?? h);
|
|
1488
|
+
};
|
|
1489
|
+
// what the viz element written for an element's block values reads back as
|
|
1490
|
+
const written: Readonly<Record<string, (want: ElementValues) => StyleValue | null>> = {
|
|
1491
|
+
shape: (want) => {
|
|
1492
|
+
const { value } = gexfNodeShape(String(want("shape")));
|
|
1493
|
+
return shapeRead.get(value) ?? value;
|
|
1494
|
+
},
|
|
1495
|
+
width: size,
|
|
1496
|
+
height: size,
|
|
1497
|
+
lineStyle: (want) => gexfEdgeShape(String(want("lineStyle"))).value,
|
|
1498
|
+
};
|
|
1499
|
+
takeFromBlock(snapshot, text, "gexf", (target) => ({
|
|
1500
|
+
values: target === "node" ? node : edge,
|
|
1501
|
+
properties: target === "node" ? ["shape", "width", "height"] : ["lineStyle"],
|
|
1502
|
+
readBack: (_i, want) => (property) => written[property](want),
|
|
1503
|
+
// a size is written from the width and the height together: one the block leaves unset reads back too
|
|
1504
|
+
take: (property, wanted, native) =>
|
|
1505
|
+
native !== null && (wanted !== null || property === "width" || property === "height"),
|
|
1506
|
+
}));
|
|
653
1507
|
}
|
|
654
1508
|
|
|
655
1509
|
// ============================================================ writing
|
|
@@ -718,7 +1572,7 @@ export function gexfStyledExport(
|
|
|
718
1572
|
const s = nodeShapes[i];
|
|
719
1573
|
return (
|
|
720
1574
|
colorElement(fill[i], fillOpacity[i], indent) +
|
|
721
|
-
(size === null ? "" : `${indent}<viz:size value="${String(size)}"/>\n`) +
|
|
1575
|
+
(size === null ? "" : `${indent}<viz:size value="${String(size / 2)}"/>\n`) +
|
|
722
1576
|
shapeElement(s, s === "image" ? String(image[i]) : null, indent)
|
|
723
1577
|
);
|
|
724
1578
|
},
|