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