@graphty/graph-io 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GEXF exporter (design sections 8.5, 3.6, 3.7, 5.1, 5.10; research note 07 section 9): writes
|
|
3
|
+
* GEXF 1.3 by default (1.2 on request), restoring declared attributes from `origin` (id, title,
|
|
4
|
+
* type, defaults, options), the viz namespace from the position / color / size / shape /
|
|
5
|
+
* thickness role columns, containment from the parent / parents columns, element lifetimes from
|
|
6
|
+
* the temporal role columns (with the text companions of design section 5.1), and dynamic
|
|
7
|
+
* attribute values from the temporal extension tables of design section 5.10. Expanded
|
|
8
|
+
* mixed-direction pairs are folded back through the `pair` / `directed` / `mutual` roles into one
|
|
9
|
+
* undirected or mutual edge; weights are written for explicit rows only (the role-weight column's
|
|
10
|
+
* validity, design section 3.7). check() lists every loss before anything is written: the generic
|
|
11
|
+
* capability gaps (W_OPEN_INTERVAL in 1.3, u32 / u8 dtypes, strides, nested json, graph
|
|
12
|
+
* attributes, foreign extension tables) and the GEXF-specific ones (1.2 has no `kind`, no
|
|
13
|
+
* timestamps and no typed lists; role columns of an unexpected shape; roles the format cannot
|
|
14
|
+
* carry; attribute titles the importer would rename).
|
|
15
|
+
*/
|
|
16
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
17
|
+
import { mapDeclaredType } from "../../common/declared-types.js";
|
|
18
|
+
import { pairFolding } from "../../common/direction.js";
|
|
19
|
+
import { escapeXmlAttribute, escapeXmlText } from "../../common/escape.js";
|
|
20
|
+
import { capabilities, checkCapabilities, LOSS, sanitizeIds } from "../../common/export.js";
|
|
21
|
+
import { formatF32, formatF64, formatInteger } from "../../common/format.js";
|
|
22
|
+
import { isCanonicalIntegerText } from "../../common/ids.js";
|
|
23
|
+
import { joinListText } from "../../common/lists.js";
|
|
24
|
+
import { resolveExportOptions } from "../../common/options.js";
|
|
25
|
+
import { formatTemporal, formatTimeValue } from "../../common/temporal.js";
|
|
26
|
+
import { explicitWeights } from "../../common/weights.js";
|
|
27
|
+
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
28
|
+
import { xmlIllegalTextNotes } from "../../common/xml.js";
|
|
29
|
+
import { canonicalScalarType, GEXF_FORMAT, GEXF_NAMESPACES, isListType, isScalarType, NODE_COLUMNS, OPEN_END, OPEN_START, parseTemporalTableName, RESERVED_EDGE_NAMES, RESERVED_NODE_NAMES, TEMPORAL_COLUMNS, VIZ_NAMESPACE, } from "./schema.js";
|
|
30
|
+
/** LossNote codes specific to the GEXF exporter, next to the shared LOSS codes. */
|
|
31
|
+
export const GEXF_LOSS = Object.freeze({
|
|
32
|
+
/** GEXF 1.2 has no parallel-edge `kind`; the kind column is dropped. */
|
|
33
|
+
KIND_DROPPED: "W_GEXF_KIND_DROPPED",
|
|
34
|
+
/** GEXF 1.2 has no timestamps; a timestamp becomes a closed interval [t, t]. */
|
|
35
|
+
TIMESTAMP_AS_INTERVAL: "W_TIMESTAMP_AS_INTERVAL",
|
|
36
|
+
/** GEXF 1.2 liststring items are separated by `|`; an item containing one cannot be split back. */
|
|
37
|
+
LIST_SEPARATOR: "W_LIST_SEPARATOR",
|
|
38
|
+
/** A role column of a shape GEXF cannot map (a string `start`, a 2-component color); written as a plain attribute. */
|
|
39
|
+
ROLE_SHAPE: "W_ROLE_SHAPE",
|
|
40
|
+
/** A temporal extension table without the element / start / end / value columns of design section 5.10. */
|
|
41
|
+
TEMPORAL_TABLE_SHAPE: "W_TEMPORAL_TABLE_SHAPE",
|
|
42
|
+
/** An attribute whose title the importer would rename on re-import (a reserved name). */
|
|
43
|
+
ATTRIBUTE_RENAMED: "W_ATTRIBUTE_RENAMED",
|
|
44
|
+
/** A cell, default or option the declared type cannot express (skipped). */
|
|
45
|
+
VALUE_UNWRITABLE: "W_VALUE_UNWRITABLE",
|
|
46
|
+
/** A declared type the target version lacks (1.2: date, dateTime, typed lists...); the canonical type is written. */
|
|
47
|
+
DECLARED_TYPE: "W_DECLARED_TYPE",
|
|
48
|
+
/** A node id whose text reads back as the other type under the canonical rule (design section 4.1): a non-integer number, a string of integer text. */
|
|
49
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
50
|
+
/** A viz role column (position, color, size, thickness) that is not f32; the importer reads viz values as f32. */
|
|
51
|
+
VIZ_DTYPE: "W_GEXF_VIZ_DTYPE",
|
|
52
|
+
/** A plain `weight` edge column reads back as THE weight (the importer's weightFrom default). */
|
|
53
|
+
WEIGHT_KEY_CLASH: LOSS.WEIGHT_KEY_CLASH,
|
|
54
|
+
/** A dict column without declared options gains one from its dictionary on re-import. */
|
|
55
|
+
OPTIONS_GAINED: LOSS.OPTIONS_GAINED,
|
|
56
|
+
/** A string cell holding a character XML 1.0 forbids; export() throws E_COLUMN_TYPE. */
|
|
57
|
+
XML_ILLEGAL_CHAR: LOSS.XML_ILLEGAL_CHAR,
|
|
58
|
+
});
|
|
59
|
+
const DTYPES_KEPT = ["f32", "f64", "i32", "bool", "dict", "string"];
|
|
60
|
+
/** GEXF 1.3: everything the model has but nested json, strides, graph attributes and open intervals. */
|
|
61
|
+
const CAPABILITIES_1_3 = capabilities({
|
|
62
|
+
mixedDirection: true,
|
|
63
|
+
multiEdges: true,
|
|
64
|
+
selfLoops: true,
|
|
65
|
+
edgeIds: "optional",
|
|
66
|
+
idCharset: "any",
|
|
67
|
+
dtypes: DTYPES_KEPT,
|
|
68
|
+
lists: true,
|
|
69
|
+
defaults: true,
|
|
70
|
+
options: true,
|
|
71
|
+
hierarchy: true,
|
|
72
|
+
temporal: "dynamic-values",
|
|
73
|
+
positions: true,
|
|
74
|
+
viz: true,
|
|
75
|
+
});
|
|
76
|
+
/** GEXF 1.2: no parallel edges, required edge ids, `liststring` only. */
|
|
77
|
+
const CAPABILITIES_1_2 = capabilities({
|
|
78
|
+
...CAPABILITIES_1_3,
|
|
79
|
+
multiEdges: false,
|
|
80
|
+
edgeIds: "required",
|
|
81
|
+
});
|
|
82
|
+
const STRUCTURAL_ROLES = new Set(["directed", "pair", "mutual", "weight", "timeText"]);
|
|
83
|
+
/** The attribute title the importer reads THE weight from by default (its weightFrom default). */
|
|
84
|
+
const DEFAULT_WEIGHT_TITLE = "weight";
|
|
85
|
+
const NUMERIC_DTYPES = new Set(["f32", "f64", "i32", "u32", "u8"]);
|
|
86
|
+
const TEXT_DTYPES = new Set(["string", "dict"]);
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the format-specific options.
|
|
89
|
+
* @param options - the caller's options
|
|
90
|
+
* @returns the version to write
|
|
91
|
+
*/
|
|
92
|
+
function resolveVersion(options) {
|
|
93
|
+
const version = options?.version;
|
|
94
|
+
if (version === undefined) {
|
|
95
|
+
return "1.3";
|
|
96
|
+
}
|
|
97
|
+
if (version !== "1.2" && version !== "1.3") {
|
|
98
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option version: ${JSON.stringify(version)} is not "1.2" or "1.3"`, {
|
|
99
|
+
option: "version",
|
|
100
|
+
found: version,
|
|
101
|
+
supported: ["1.2", "1.3"],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return version;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Decide everything about an export: the capability notes, the role columns of each domain, the
|
|
108
|
+
* attribute declarations and their formatters, the temporal tables and the graph header values.
|
|
109
|
+
* @param snapshot - the snapshot
|
|
110
|
+
* @param options - the caller's options
|
|
111
|
+
* @returns the plan
|
|
112
|
+
*/
|
|
113
|
+
function planExport(snapshot, options) {
|
|
114
|
+
const version = resolveVersion(options);
|
|
115
|
+
const resolved = resolveExportOptions(options);
|
|
116
|
+
const caps = version === "1.2" ? CAPABILITIES_1_2 : CAPABILITIES_1_3;
|
|
117
|
+
const notes = [
|
|
118
|
+
...checkCapabilities(snapshot, caps, resolved, {
|
|
119
|
+
openIntervals: version === "1.2",
|
|
120
|
+
temporalText: true,
|
|
121
|
+
positionDtype: "f32",
|
|
122
|
+
roles: MAPPED_ROLES,
|
|
123
|
+
roleNames: ROLE_NAMES,
|
|
124
|
+
}),
|
|
125
|
+
];
|
|
126
|
+
const note = (code, message, column = null, count = null) => {
|
|
127
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
128
|
+
};
|
|
129
|
+
notes.push(...xmlIllegalTextNotes(snapshot));
|
|
130
|
+
let typeChanges = 0;
|
|
131
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
132
|
+
const id = snapshot.ids.idOf(i);
|
|
133
|
+
if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(id)) {
|
|
134
|
+
typeChanges++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (typeChanges > 0) {
|
|
138
|
+
note(GEXF_LOSS.ID_TEXT_TYPE, `${typeChanges} node id(s) change type when read back under ids: "canonical" (string ids that are integer text, non-integer numbers); the file's idtype is not honoured by the importer`, null, typeChanges);
|
|
139
|
+
}
|
|
140
|
+
const tables = collectTemporalTables(snapshot, version, note);
|
|
141
|
+
const nodeRoles = collectRoles(snapshot.nodes, "node", note);
|
|
142
|
+
const edgeRoles = collectRoles(snapshot.edges, "edge", note);
|
|
143
|
+
const nodeAttrs = collectAttributes(snapshot, "node", nodeRoles, tables.get("node") ?? new Map(), version, note);
|
|
144
|
+
const edgeAttrs = collectAttributes(snapshot, "edge", edgeRoles, tables.get("edge") ?? new Map(), version, note);
|
|
145
|
+
const temporal = hasLifetime(nodeRoles) ||
|
|
146
|
+
hasLifetime(edgeRoles) ||
|
|
147
|
+
nodeAttrs.some((a) => a.dynamic) ||
|
|
148
|
+
edgeAttrs.some((a) => a.dynamic);
|
|
149
|
+
if (version === "1.2") {
|
|
150
|
+
for (const [domain, roles] of [
|
|
151
|
+
["node", nodeRoles],
|
|
152
|
+
["edge", edgeRoles],
|
|
153
|
+
]) {
|
|
154
|
+
if (roles.timestamp !== null || roles.timestamps !== null) {
|
|
155
|
+
note(GEXF_LOSS.TIMESTAMP_AS_INTERVAL, `${domain} timestamps are written as closed intervals; GEXF 1.2 has no timestamp representation`, (roles.timestamp ?? roles.timestamps)?.meta.name ?? null, null);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (edgeRoles.kind !== null) {
|
|
159
|
+
note(GEXF_LOSS.KIND_DROPPED, `edge column "${edgeRoles.kind.meta.name}" (kind) cannot be written; GEXF 1.2 has no edge kind`, edgeRoles.kind.meta.name, edgeRoles.kind.length - edgeRoles.kind.nullCount);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const { meta } = snapshot;
|
|
163
|
+
let { timeRepresentation } = meta;
|
|
164
|
+
if (timeRepresentation === null && temporal) {
|
|
165
|
+
const timestampOnly = (nodeRoles.timestamp !== null || edgeRoles.timestamp !== null) &&
|
|
166
|
+
nodeRoles.start === null &&
|
|
167
|
+
nodeRoles.end === null &&
|
|
168
|
+
edgeRoles.start === null &&
|
|
169
|
+
edgeRoles.end === null;
|
|
170
|
+
timeRepresentation = timestampOnly ? "timestamp" : null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
version,
|
|
174
|
+
options: resolved,
|
|
175
|
+
notes,
|
|
176
|
+
nodeRoles,
|
|
177
|
+
edgeRoles,
|
|
178
|
+
nodeAttrs,
|
|
179
|
+
edgeAttrs,
|
|
180
|
+
timeFormat: meta.timeFormat,
|
|
181
|
+
temporal,
|
|
182
|
+
timeRepresentation,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether a domain carries element lifetimes.
|
|
187
|
+
* @param roles - the domain's role columns
|
|
188
|
+
* @returns true when any temporal role column exists
|
|
189
|
+
*/
|
|
190
|
+
function hasLifetime(roles) {
|
|
191
|
+
return (roles.start !== null ||
|
|
192
|
+
roles.end !== null ||
|
|
193
|
+
roles.timestamp !== null ||
|
|
194
|
+
roles.spells !== null ||
|
|
195
|
+
roles.timestamps !== null);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether a column is a scalar numeric column.
|
|
199
|
+
* @param column - the column
|
|
200
|
+
* @returns true for f32 / f64 / i32 / u32 / u8 with one component
|
|
201
|
+
*/
|
|
202
|
+
function isNumericScalar(column) {
|
|
203
|
+
return NUMERIC_DTYPES.has(column.dtype) && column.meta.components === 1;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Whether a column is a numeric column with a given stride.
|
|
207
|
+
* @param column - the column
|
|
208
|
+
* @param components - the accepted strides
|
|
209
|
+
* @returns true when numeric with one of the strides
|
|
210
|
+
*/
|
|
211
|
+
function isNumericVector(column, components) {
|
|
212
|
+
return NUMERIC_DTYPES.has(column.dtype) && components.includes(column.meta.components);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Whether a column holds text (string or dict).
|
|
216
|
+
* @param column - the column
|
|
217
|
+
* @returns true for string / dict
|
|
218
|
+
*/
|
|
219
|
+
function isText(column) {
|
|
220
|
+
return TEXT_DTYPES.has(column.dtype);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether a column is a list of numbers with a given item stride.
|
|
224
|
+
* @param column - the column
|
|
225
|
+
* @param itemComponents - the required item stride
|
|
226
|
+
* @returns true for a numeric list of that stride
|
|
227
|
+
*/
|
|
228
|
+
function isNumericList(column, itemComponents) {
|
|
229
|
+
return (column.dtype === "list" &&
|
|
230
|
+
column.meta.itemDtype !== null &&
|
|
231
|
+
NUMERIC_DTYPES.has(column.meta.itemDtype) &&
|
|
232
|
+
(column.meta.itemComponents ?? 1) === itemComponents);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Whether a role column has the shape its GEXF field needs.
|
|
236
|
+
* @param role - the role
|
|
237
|
+
* @param column - the column
|
|
238
|
+
* @param domain - node or edge
|
|
239
|
+
* @returns true when writable
|
|
240
|
+
*/
|
|
241
|
+
function roleShapeOk(role, column, domain) {
|
|
242
|
+
switch (role) {
|
|
243
|
+
case "label":
|
|
244
|
+
case "shape":
|
|
245
|
+
return isText(column);
|
|
246
|
+
case "kind":
|
|
247
|
+
return domain === "edge" && isText(column);
|
|
248
|
+
case "id":
|
|
249
|
+
return domain === "edge" && (isText(column) || isNumericScalar(column));
|
|
250
|
+
case "start":
|
|
251
|
+
case "end":
|
|
252
|
+
case "timestamp":
|
|
253
|
+
case "open":
|
|
254
|
+
return isNumericScalar(column);
|
|
255
|
+
case "size":
|
|
256
|
+
return domain === "node" && isNumericScalar(column);
|
|
257
|
+
case "thickness":
|
|
258
|
+
return domain === "edge" && isNumericScalar(column);
|
|
259
|
+
case "spells":
|
|
260
|
+
return isNumericList(column, 2);
|
|
261
|
+
case "timestamps":
|
|
262
|
+
return isNumericList(column, 1);
|
|
263
|
+
case "position":
|
|
264
|
+
return domain === "node" && isNumericVector(column, [2, 3]);
|
|
265
|
+
case "color":
|
|
266
|
+
return isNumericVector(column, [3, 4]);
|
|
267
|
+
case "parent":
|
|
268
|
+
return domain === "node" && column.dtype === "u32" && column.meta.components === 1;
|
|
269
|
+
case "parents":
|
|
270
|
+
return domain === "node" && column.dtype === "list" && column.meta.itemDtype === "u32";
|
|
271
|
+
default:
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const VIZ_NUMERIC_ROLES = new Set(["position", "color", "size", "thickness"]);
|
|
276
|
+
const MAPPED_ROLES = new Set([
|
|
277
|
+
"label",
|
|
278
|
+
"id",
|
|
279
|
+
"kind",
|
|
280
|
+
"start",
|
|
281
|
+
"end",
|
|
282
|
+
"timestamp",
|
|
283
|
+
"spells",
|
|
284
|
+
"timestamps",
|
|
285
|
+
"open",
|
|
286
|
+
"position",
|
|
287
|
+
"color",
|
|
288
|
+
"size",
|
|
289
|
+
"shape",
|
|
290
|
+
"thickness",
|
|
291
|
+
"parent",
|
|
292
|
+
"parents",
|
|
293
|
+
]);
|
|
294
|
+
/** The column name the importer gives each mapped role on re-import (design section 5.6 fixed names). */
|
|
295
|
+
const ROLE_NAMES = Object.freeze({
|
|
296
|
+
label: NODE_COLUMNS.label,
|
|
297
|
+
id: "id",
|
|
298
|
+
kind: "kind",
|
|
299
|
+
start: NODE_COLUMNS.start,
|
|
300
|
+
end: NODE_COLUMNS.end,
|
|
301
|
+
timestamp: NODE_COLUMNS.timestamp,
|
|
302
|
+
spells: NODE_COLUMNS.spells,
|
|
303
|
+
timestamps: NODE_COLUMNS.timestamps,
|
|
304
|
+
open: NODE_COLUMNS.open,
|
|
305
|
+
position: NODE_COLUMNS.position,
|
|
306
|
+
color: NODE_COLUMNS.color,
|
|
307
|
+
size: NODE_COLUMNS.size,
|
|
308
|
+
shape: NODE_COLUMNS.shape,
|
|
309
|
+
thickness: "thickness",
|
|
310
|
+
parent: NODE_COLUMNS.parent,
|
|
311
|
+
parents: NODE_COLUMNS.parents,
|
|
312
|
+
});
|
|
313
|
+
/**
|
|
314
|
+
* Find the role columns of a table; a role column of the wrong shape is noted and left to the
|
|
315
|
+
* attribute pass.
|
|
316
|
+
* @param table - the node or edge table
|
|
317
|
+
* @param domain - node or edge
|
|
318
|
+
* @param note - the note recorder
|
|
319
|
+
* @returns the role columns
|
|
320
|
+
*/
|
|
321
|
+
function collectRoles(table, domain, note) {
|
|
322
|
+
const companions = new Map();
|
|
323
|
+
const roles = {
|
|
324
|
+
label: null,
|
|
325
|
+
id: null,
|
|
326
|
+
kind: null,
|
|
327
|
+
start: null,
|
|
328
|
+
end: null,
|
|
329
|
+
timestamp: null,
|
|
330
|
+
spells: null,
|
|
331
|
+
timestamps: null,
|
|
332
|
+
open: null,
|
|
333
|
+
position: null,
|
|
334
|
+
color: null,
|
|
335
|
+
size: null,
|
|
336
|
+
shape: null,
|
|
337
|
+
shapeUri: null,
|
|
338
|
+
thickness: null,
|
|
339
|
+
parent: null,
|
|
340
|
+
parents: null,
|
|
341
|
+
companions,
|
|
342
|
+
};
|
|
343
|
+
for (const column of table) {
|
|
344
|
+
const { meta } = column;
|
|
345
|
+
const forName = meta.extra.for;
|
|
346
|
+
if ((meta.role === "timeText" || typeof forName === "string") && column.dtype === "string") {
|
|
347
|
+
if (typeof forName === "string") {
|
|
348
|
+
companions.set(forName, column);
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (domain === "node" && meta.name === NODE_COLUMNS.shapeUri && meta.origin?.namespace === VIZ_NAMESPACE) {
|
|
353
|
+
roles.shapeUri = column.dtype === "string" ? column : null;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const { role } = meta;
|
|
357
|
+
if (role === null || !MAPPED_ROLES.has(role)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!roleShapeOk(role, column, domain)) {
|
|
361
|
+
note(GEXF_LOSS.ROLE_SHAPE, `${domain} column "${meta.name}" (${role}) has a shape GEXF cannot map (${describeShape(column)}); written as a plain attribute`, meta.name, null);
|
|
362
|
+
// the generic check skips role columns; report what the attribute path loses
|
|
363
|
+
const set = column.length - column.nullCount;
|
|
364
|
+
if (NUMERIC_DTYPES.has(column.dtype) && meta.components > 1) {
|
|
365
|
+
note(LOSS.COMPONENTS, `${domain} column "${meta.name}" has ${meta.components} components; written as a list`, meta.name, set);
|
|
366
|
+
}
|
|
367
|
+
if (column.dtype === "json") {
|
|
368
|
+
note(LOSS.JSON, `${domain} column "${meta.name}" holds nested values; written as JSON text`, meta.name, set);
|
|
369
|
+
}
|
|
370
|
+
if (column.dtype === "u32" || column.dtype === "u8") {
|
|
371
|
+
note(LOSS.DTYPE, `${domain} column "${meta.name}" is ${column.dtype}; the format cannot keep that dtype`, meta.name, set);
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (VIZ_NUMERIC_ROLES.has(role) && column.dtype !== "f32") {
|
|
376
|
+
note(GEXF_LOSS.VIZ_DTYPE, `${domain} column "${meta.name}" (${role}) is ${column.dtype}; viz values read back as f32`, meta.name, column.length - column.nullCount);
|
|
377
|
+
}
|
|
378
|
+
roles[role] = column;
|
|
379
|
+
}
|
|
380
|
+
return roles;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* A short description of a column's shape for messages.
|
|
384
|
+
* @param column - the column
|
|
385
|
+
* @returns "dtype x components" or "list of item"
|
|
386
|
+
*/
|
|
387
|
+
function describeShape(column) {
|
|
388
|
+
if (column.dtype === "list") {
|
|
389
|
+
return `list of ${column.meta.itemDtype ?? "?"} x ${column.meta.itemComponents ?? 1}`;
|
|
390
|
+
}
|
|
391
|
+
return `${column.dtype} x ${column.meta.components}`;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Resolve every temporal extension table (design section 5.10) into its columns, grouped by
|
|
395
|
+
* domain and keyed by the static column's name; a table without the required columns is noted.
|
|
396
|
+
* @param snapshot - the snapshot
|
|
397
|
+
* @param version - the target version
|
|
398
|
+
* @param note - the note recorder
|
|
399
|
+
* @returns tables by domain, then by column name
|
|
400
|
+
*/
|
|
401
|
+
function collectTemporalTables(snapshot, version, note) {
|
|
402
|
+
const out = new Map();
|
|
403
|
+
for (const [name, table] of snapshot.extensions) {
|
|
404
|
+
const parsed = parseTemporalTableName(name);
|
|
405
|
+
if (parsed === null) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const element = table.get(TEMPORAL_COLUMNS.element);
|
|
409
|
+
const start = table.get(TEMPORAL_COLUMNS.start);
|
|
410
|
+
const end = table.get(TEMPORAL_COLUMNS.end);
|
|
411
|
+
const value = table.get(TEMPORAL_COLUMNS.value);
|
|
412
|
+
if (element === null ||
|
|
413
|
+
element.dtype !== "u32" ||
|
|
414
|
+
start === null ||
|
|
415
|
+
!isNumericScalar(start) ||
|
|
416
|
+
end === null ||
|
|
417
|
+
!isNumericScalar(end) ||
|
|
418
|
+
value === null) {
|
|
419
|
+
note(GEXF_LOSS.TEMPORAL_TABLE_SHAPE, `extension table "${name}" lacks the element / start / end / value columns and cannot be written`, name, table.rowCount);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const rows = new Map();
|
|
423
|
+
for (let r = 0; r < table.rowCount; r++) {
|
|
424
|
+
if (!element.isSet(r)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const e = element.value(r);
|
|
428
|
+
const list = rows.get(e);
|
|
429
|
+
if (list === undefined) {
|
|
430
|
+
rows.set(e, [r]);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
list.push(r);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const typed = attributeType(value, version);
|
|
437
|
+
const open = optionalNumeric(table.get(TEMPORAL_COLUMNS.open));
|
|
438
|
+
if (version === "1.3" && open !== null && open.length - open.nullCount > 0) {
|
|
439
|
+
note(LOSS.OPEN_INTERVAL, `dynamic values of "${name}" carry open intervals, which GEXF 1.3 cannot write`, name, open.length - open.nullCount);
|
|
440
|
+
}
|
|
441
|
+
const resolved = {
|
|
442
|
+
element,
|
|
443
|
+
start,
|
|
444
|
+
end,
|
|
445
|
+
value,
|
|
446
|
+
startText: optionalText(table.get(TEMPORAL_COLUMNS.startText)),
|
|
447
|
+
endText: optionalText(table.get(TEMPORAL_COLUMNS.endText)),
|
|
448
|
+
valueText: isTemporalType(typed.type) ? optionalText(table.get(TEMPORAL_COLUMNS.valueText)) : null,
|
|
449
|
+
open,
|
|
450
|
+
rows,
|
|
451
|
+
format: typed.format,
|
|
452
|
+
};
|
|
453
|
+
let byDomain = out.get(parsed.domain);
|
|
454
|
+
if (byDomain === undefined) {
|
|
455
|
+
byDomain = new Map();
|
|
456
|
+
out.set(parsed.domain, byDomain);
|
|
457
|
+
}
|
|
458
|
+
byDomain.set(parsed.column, resolved);
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* A string column, or null.
|
|
464
|
+
* @param column - the column or null
|
|
465
|
+
* @returns the column when it is a string column
|
|
466
|
+
*/
|
|
467
|
+
function optionalText(column) {
|
|
468
|
+
return column !== null && column.dtype === "string" ? column : null;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* A numeric scalar column, or null.
|
|
472
|
+
* @param column - the column or null
|
|
473
|
+
* @returns the column when numeric
|
|
474
|
+
*/
|
|
475
|
+
function optionalNumeric(column) {
|
|
476
|
+
return column !== null && isNumericScalar(column) ? column : null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The GEXF type of a column and the formatter of its values: the declared `origin.type` when the
|
|
480
|
+
* version knows it and it agrees with the dtype, the canonical type of the dtype otherwise; lists
|
|
481
|
+
* become `list<item>` (1.3) or `liststring` (1.2); a stride column becomes a list of its lanes;
|
|
482
|
+
* json becomes a string of JSON text.
|
|
483
|
+
* @param column - the column
|
|
484
|
+
* @param version - the target version
|
|
485
|
+
* @returns the type text, whether it is a list, and the formatter
|
|
486
|
+
*/
|
|
487
|
+
function attributeType(column, version) {
|
|
488
|
+
const { meta } = column;
|
|
489
|
+
const originType = meta.origin?.type ?? null;
|
|
490
|
+
if (meta.dtype === "list" || (NUMERIC_DTYPES.has(meta.dtype) && meta.components > 1)) {
|
|
491
|
+
const itemDtype = meta.dtype === "list" ? (meta.itemDtype ?? "string") : meta.dtype;
|
|
492
|
+
let itemType = canonicalScalarType(itemDtype);
|
|
493
|
+
if (originType !== null &&
|
|
494
|
+
meta.dtype === "list" &&
|
|
495
|
+
isListType(originType, "1.3") &&
|
|
496
|
+
agrees(originType, "list", itemDtype)) {
|
|
497
|
+
itemType = originType.slice(4);
|
|
498
|
+
}
|
|
499
|
+
if (version === "1.2") {
|
|
500
|
+
const itemFormat = scalarFormatter(itemType, itemDtype);
|
|
501
|
+
return {
|
|
502
|
+
type: "liststring",
|
|
503
|
+
list: true,
|
|
504
|
+
itemType: "string",
|
|
505
|
+
format: listFormatter(itemFormat, version),
|
|
506
|
+
dropped: itemType === "string" ? null : `list${itemType}`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
type: `list${itemType}`,
|
|
511
|
+
list: true,
|
|
512
|
+
itemType,
|
|
513
|
+
format: listFormatter(scalarFormatter(itemType, itemDtype), version),
|
|
514
|
+
dropped: null,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
let type = canonicalScalarType(meta.dtype);
|
|
518
|
+
let dropped = null;
|
|
519
|
+
const temporal = originType === null || meta.dtype !== "f64" ? null : gexfTemporalType(originType);
|
|
520
|
+
if (temporal !== null) {
|
|
521
|
+
// a date / dateTime column of any format (Neo4j spells them date / datetime /
|
|
522
|
+
// localdatetime) keeps its temporal type, so the text companion is written back
|
|
523
|
+
if (isScalarType(temporal, version)) {
|
|
524
|
+
type = temporal;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
dropped = temporal;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (originType !== null && agrees(originType, meta.dtype, null)) {
|
|
531
|
+
if (isScalarType(originType, version)) {
|
|
532
|
+
type = originType;
|
|
533
|
+
}
|
|
534
|
+
else if (isScalarType(originType, "1.3")) {
|
|
535
|
+
dropped = originType;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { type, list: false, itemType: type, format: scalarFormatter(type, meta.dtype), dropped };
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The GEXF temporal type a declared origin type of any format maps to: `date` for a date,
|
|
542
|
+
* `dateTime` for a date-time with or without zone (GEXF's own spellings, Neo4j's `date` /
|
|
543
|
+
* `datetime` / `localdatetime`), null for a non-temporal type or a time of day (GEXF has none).
|
|
544
|
+
* @param originType - the declared type text
|
|
545
|
+
* @returns "date", "dateTime" or null
|
|
546
|
+
*/
|
|
547
|
+
function gexfTemporalType(originType) {
|
|
548
|
+
switch (originType.toLowerCase()) {
|
|
549
|
+
case "date":
|
|
550
|
+
return "date";
|
|
551
|
+
case "datetime":
|
|
552
|
+
case "localdatetime":
|
|
553
|
+
return "dateTime";
|
|
554
|
+
default:
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Whether a declared GEXF type maps back to a dtype (so the exporter may restore it verbatim).
|
|
560
|
+
* @param type - the declared type text
|
|
561
|
+
* @param dtype - the column dtype
|
|
562
|
+
* @param itemDtype - the item dtype of a list column, or null
|
|
563
|
+
* @returns true when mapDeclaredType agrees
|
|
564
|
+
*/
|
|
565
|
+
function agrees(type, dtype, itemDtype) {
|
|
566
|
+
const spec = mapDeclaredType("gexf", type, "f64");
|
|
567
|
+
if (spec === null) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
if (dtype === "list") {
|
|
571
|
+
return spec.list && spec.itemDtype === itemDtype;
|
|
572
|
+
}
|
|
573
|
+
if (spec.list) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
// a dict column is a string column with options
|
|
577
|
+
return spec.dtype === dtype || (dtype === "dict" && spec.dtype === "string");
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* The formatter of one scalar value under a GEXF type.
|
|
581
|
+
* @param type - the GEXF scalar type text
|
|
582
|
+
* @param dtype - the column (or item) dtype the values come from
|
|
583
|
+
* @returns the formatter
|
|
584
|
+
*/
|
|
585
|
+
function scalarFormatter(type, dtype) {
|
|
586
|
+
switch (type) {
|
|
587
|
+
case "boolean":
|
|
588
|
+
return (value) => {
|
|
589
|
+
if (typeof value === "boolean") {
|
|
590
|
+
return value ? "true" : "false";
|
|
591
|
+
}
|
|
592
|
+
if (typeof value !== "number") {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
return value !== 0 ? "true" : "false";
|
|
596
|
+
};
|
|
597
|
+
case "integer":
|
|
598
|
+
case "long":
|
|
599
|
+
case "byte":
|
|
600
|
+
case "short":
|
|
601
|
+
case "biginteger":
|
|
602
|
+
return (value) => {
|
|
603
|
+
if (typeof value === "number") {
|
|
604
|
+
return Number.isInteger(value) ? formatInteger(value) : formatF64(value);
|
|
605
|
+
}
|
|
606
|
+
return typeof value === "string" ? value : null;
|
|
607
|
+
};
|
|
608
|
+
case "float":
|
|
609
|
+
return (value) => (typeof value === "number" ? formatF32(value) : null);
|
|
610
|
+
case "double":
|
|
611
|
+
case "bigdecimal":
|
|
612
|
+
return (value) => {
|
|
613
|
+
if (typeof value === "number") {
|
|
614
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
615
|
+
}
|
|
616
|
+
return typeof value === "string" ? value : null;
|
|
617
|
+
};
|
|
618
|
+
case "date":
|
|
619
|
+
case "dateTime":
|
|
620
|
+
return (value) => {
|
|
621
|
+
if (typeof value === "number") {
|
|
622
|
+
return formatTemporal(value, type);
|
|
623
|
+
}
|
|
624
|
+
return typeof value === "string" ? value : null;
|
|
625
|
+
};
|
|
626
|
+
default:
|
|
627
|
+
return (value) => {
|
|
628
|
+
switch (typeof value) {
|
|
629
|
+
case "string":
|
|
630
|
+
return value;
|
|
631
|
+
case "number":
|
|
632
|
+
return dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
633
|
+
case "boolean":
|
|
634
|
+
return value ? "true" : "false";
|
|
635
|
+
case "object":
|
|
636
|
+
return value === null ? null : JSON.stringify(value);
|
|
637
|
+
default:
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* The formatter of a list value: items through the item formatter, joined per version.
|
|
645
|
+
* @param item - the item formatter
|
|
646
|
+
* @param version - the target version (1.3 brackets, 1.2 pipes)
|
|
647
|
+
* @returns the formatter; null when any item cannot be written
|
|
648
|
+
*/
|
|
649
|
+
function listFormatter(item, version) {
|
|
650
|
+
return (value) => {
|
|
651
|
+
if (!Array.isArray(value) && !ArrayBuffer.isView(value)) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const items = [];
|
|
655
|
+
for (const entry of Array.from(value)) {
|
|
656
|
+
const text = item(entry);
|
|
657
|
+
if (text === null) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
items.push(text);
|
|
661
|
+
}
|
|
662
|
+
return joinListText(items, version === "1.2" ? "pipe" : "gexf");
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Decide the `<attribute>` declarations of a domain: every column that is neither structural, a
|
|
667
|
+
* companion nor a mapped role column, in declaration order, plus the temporal tables without a
|
|
668
|
+
* static column (the dynamic weight) as dynamic attributes.
|
|
669
|
+
* @param snapshot - the snapshot
|
|
670
|
+
* @param domain - node or edge
|
|
671
|
+
* @param roles - the domain's role columns
|
|
672
|
+
* @param tables - the domain's temporal tables by column name
|
|
673
|
+
* @param version - the target version
|
|
674
|
+
* @param note - the note recorder
|
|
675
|
+
* @returns the declarations
|
|
676
|
+
*/
|
|
677
|
+
function collectAttributes(snapshot, domain, roles, tables, version, note) {
|
|
678
|
+
const table = domain === "node" ? snapshot.nodes : snapshot.edges;
|
|
679
|
+
const reserved = domain === "node" ? RESERVED_NODE_NAMES : RESERVED_EDGE_NAMES;
|
|
680
|
+
const roleColumns = new Set();
|
|
681
|
+
for (const value of Object.values(roles)) {
|
|
682
|
+
if (value !== null && !(value instanceof Map)) {
|
|
683
|
+
roleColumns.add(value);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const usedIds = new Set();
|
|
687
|
+
const specs = [];
|
|
688
|
+
const usedTables = new Set();
|
|
689
|
+
const declare = (column, name, origin, typed, dynamic, temporal) => {
|
|
690
|
+
const id = uniqueId(origin.id ?? name, name, usedIds);
|
|
691
|
+
const title = origin.title ?? name;
|
|
692
|
+
if (typed.dropped !== null) {
|
|
693
|
+
note(GEXF_LOSS.DECLARED_TYPE, `${domain} column "${name}" is declared ${typed.dropped}, which GEXF ${version} lacks; written as ${typed.type}`, name, null);
|
|
694
|
+
}
|
|
695
|
+
if (domain === "edge" && column !== null && column.meta.role === null && title === DEFAULT_WEIGHT_TITLE) {
|
|
696
|
+
note(GEXF_LOSS.WEIGHT_KEY_CLASH, `edge column "${name}" is written as an attribute titled "${title}", which the importer reads as THE weight (weightFrom); it reads back as the weight, not as a column`, name, column.length - column.nullCount);
|
|
697
|
+
}
|
|
698
|
+
const reimportName = reserved.has(title) ? `${title}#${id}` : title;
|
|
699
|
+
if (reimportName !== name) {
|
|
700
|
+
note(GEXF_LOSS.ATTRIBUTE_RENAMED, `${domain} column "${name}" is written with title "${title}" and reads back as "${reimportName}"`, name, null);
|
|
701
|
+
}
|
|
702
|
+
const { defaultText, optionsText } = column === null
|
|
703
|
+
? { defaultText: null, optionsText: null }
|
|
704
|
+
: declaredTexts(column, domain, typed, version, note);
|
|
705
|
+
const companion = column === null ? null : (roles.companions.get(name) ?? null);
|
|
706
|
+
if (companion !== null && !isTemporalType(typed.type)) {
|
|
707
|
+
note(LOSS.TEMPORAL_TEXT, `${domain} column "${companion.meta.name}" (the lexical form of "${name}") cannot be written: "${name}" is written as ${typed.type}, not a GEXF temporal type`, companion.meta.name, companion.length - companion.nullCount);
|
|
708
|
+
}
|
|
709
|
+
specs.push({
|
|
710
|
+
id,
|
|
711
|
+
title,
|
|
712
|
+
type: typed.type,
|
|
713
|
+
column,
|
|
714
|
+
companion: isTemporalType(typed.type) ? companion : null,
|
|
715
|
+
format: typed.format,
|
|
716
|
+
defaultText,
|
|
717
|
+
optionsText,
|
|
718
|
+
dynamic,
|
|
719
|
+
table: temporal,
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
for (const column of table) {
|
|
723
|
+
const { meta } = column;
|
|
724
|
+
if (roleColumns.has(column) || (meta.role !== null && STRUCTURAL_ROLES.has(meta.role))) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (typeof meta.extra.for === "string" && column.dtype === "string") {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (domain === "node" && column === roles.shapeUri) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const temporal = tables.get(meta.name) ?? null;
|
|
734
|
+
if (temporal !== null) {
|
|
735
|
+
usedTables.add(meta.name);
|
|
736
|
+
}
|
|
737
|
+
declare(column, meta.name, { id: meta.origin?.id ?? null, title: meta.origin?.title ?? null }, attributeType(column, version), meta.dynamic || temporal !== null, temporal);
|
|
738
|
+
}
|
|
739
|
+
for (const [name, temporal] of tables) {
|
|
740
|
+
if (usedTables.has(name)) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const { origin } = temporal.value.meta;
|
|
744
|
+
const { weightOrigin } = snapshot.meta;
|
|
745
|
+
const weightId = domain === "edge" && weightOrigin !== null && (weightOrigin.title ?? weightOrigin.id) === name
|
|
746
|
+
? weightOrigin.id
|
|
747
|
+
: null;
|
|
748
|
+
declare(null, name, { id: origin?.id ?? weightId, title: null }, attributeType(temporal.value, version), true, temporal);
|
|
749
|
+
}
|
|
750
|
+
return specs;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* The `<default>` and `<options>` texts of a declared attribute, with the notes about what cannot
|
|
754
|
+
* be written as the declared type, about a dictionary written as options, and about 1.2 list items
|
|
755
|
+
* holding the `|` separator.
|
|
756
|
+
* @param column - the column
|
|
757
|
+
* @param domain - node or edge
|
|
758
|
+
* @param typed - the attribute's GEXF type and formatter
|
|
759
|
+
* @param version - the target version
|
|
760
|
+
* @param note - the note recorder
|
|
761
|
+
* @returns the default and options texts (null when absent or unwritable)
|
|
762
|
+
*/
|
|
763
|
+
function declaredTexts(column, domain, typed, version, note) {
|
|
764
|
+
const { meta } = column;
|
|
765
|
+
const { name } = meta;
|
|
766
|
+
let defaultText = null;
|
|
767
|
+
let optionsText = null;
|
|
768
|
+
if (meta.default !== undefined) {
|
|
769
|
+
defaultText = typed.format(meta.default);
|
|
770
|
+
if (defaultText === null) {
|
|
771
|
+
note(GEXF_LOSS.VALUE_UNWRITABLE, `${domain} column "${name}": the default cannot be written as ${typed.type}`, name, null);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const options = meta.options ?? (column.dtype === "dict" ? column.dictionary : null);
|
|
775
|
+
if (meta.options === null && options !== null && options.length > 0) {
|
|
776
|
+
note(GEXF_LOSS.OPTIONS_GAINED, `${domain} column "${name}" declares no options; its dictionary is written as <options> and reads back as declared options`, name, null);
|
|
777
|
+
}
|
|
778
|
+
if (options !== null) {
|
|
779
|
+
const itemFormat = typed.list ? scalarFormatter(typed.itemType, meta.itemDtype ?? "string") : typed.format;
|
|
780
|
+
const texts = [];
|
|
781
|
+
let ok = true;
|
|
782
|
+
for (const option of options) {
|
|
783
|
+
const text = itemFormat(option);
|
|
784
|
+
if (text === null) {
|
|
785
|
+
ok = false;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
texts.push(text);
|
|
789
|
+
}
|
|
790
|
+
if (ok) {
|
|
791
|
+
optionsText = joinListText(texts, version === "1.2" ? "pipe" : "gexf");
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
note(GEXF_LOSS.VALUE_UNWRITABLE, `${domain} column "${name}": the options cannot be written as ${typed.type}`, name, null);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (version === "1.2" && column.dtype === "list") {
|
|
798
|
+
let count = 0;
|
|
799
|
+
for (let r = 0; r < column.length; r++) {
|
|
800
|
+
if (column.isSet(r) && listItems(column, r).some((item) => String(item).includes("|"))) {
|
|
801
|
+
count++;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (count > 0) {
|
|
805
|
+
note(GEXF_LOSS.LIST_SEPARATOR, `${domain} column "${name}": ${count} row(s) hold an item containing "|", the 1.2 list separator`, name, count);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return { defaultText, optionsText };
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Whether a GEXF type is a temporal one whose values have text companions.
|
|
812
|
+
* @param type - the type text
|
|
813
|
+
* @returns true for date / dateTime
|
|
814
|
+
*/
|
|
815
|
+
function isTemporalType(type) {
|
|
816
|
+
return type === "date" || type === "dateTime";
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* A unique attribute id within a class.
|
|
820
|
+
* @param preferred - the id to try first (the origin id or the name)
|
|
821
|
+
* @param name - the column name, tried second
|
|
822
|
+
* @param used - ids already taken
|
|
823
|
+
* @returns a free id, recorded as used
|
|
824
|
+
*/
|
|
825
|
+
function uniqueId(preferred, name, used) {
|
|
826
|
+
const candidates = [preferred, name];
|
|
827
|
+
for (const candidate of candidates) {
|
|
828
|
+
if (candidate.length > 0 && !used.has(candidate)) {
|
|
829
|
+
used.add(candidate);
|
|
830
|
+
return candidate;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (let n = 2;; n++) {
|
|
834
|
+
const candidate = `${name}#${n}`;
|
|
835
|
+
if (!used.has(candidate)) {
|
|
836
|
+
used.add(candidate);
|
|
837
|
+
return candidate;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// ============================================================ writing
|
|
842
|
+
/**
|
|
843
|
+
* The text of a time bound: the companion text when present, the formatted number otherwise.
|
|
844
|
+
* @param column - the numeric column
|
|
845
|
+
* @param companion - its text companion, or null
|
|
846
|
+
* @param row - the row
|
|
847
|
+
* @param timeFormat - the graph's timeformat
|
|
848
|
+
* @returns the text
|
|
849
|
+
*/
|
|
850
|
+
function timeText(column, companion, row, timeFormat) {
|
|
851
|
+
if (companion !== null && companion.isSet(row)) {
|
|
852
|
+
return companion.value(row);
|
|
853
|
+
}
|
|
854
|
+
return formatTimeValue(column.value(row), timeFormat);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* The `start` / `end` / `timestamp` / open attributes of one element.
|
|
858
|
+
* @param roles - the domain's role columns
|
|
859
|
+
* @param row - the element's row
|
|
860
|
+
* @param plan - the plan
|
|
861
|
+
* @returns the attribute text, starting with a space when non-empty
|
|
862
|
+
*/
|
|
863
|
+
function lifetimeAttrs(roles, row, plan) {
|
|
864
|
+
let out = "";
|
|
865
|
+
const { timeFormat, version } = plan;
|
|
866
|
+
let bits = 0;
|
|
867
|
+
if (version === "1.2" && roles.open !== null && roles.open.isSet(row)) {
|
|
868
|
+
bits = roles.open.value(row);
|
|
869
|
+
}
|
|
870
|
+
const write = (name, column, open) => {
|
|
871
|
+
if (column === null || !column.isSet(row)) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const value = column.value(row);
|
|
875
|
+
if (!Number.isFinite(value)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const text = timeText(column, roles.companions.get(column.meta.name) ?? null, row, timeFormat);
|
|
879
|
+
// GEXF 1.2: `startopen` / `endopen` hold the time of a non-inclusive bound (dynamics.xsd
|
|
880
|
+
// time-type) and replace `start` / `end`
|
|
881
|
+
out += ` ${open ? `${name}open` : name}="${escapeXmlAttribute(text)}"`;
|
|
882
|
+
};
|
|
883
|
+
if (roles.timestamp !== null && roles.timestamp.isSet(row)) {
|
|
884
|
+
if (version === "1.3" && plan.timeRepresentation === "timestamp") {
|
|
885
|
+
write("timestamp", roles.timestamp, false);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
write("start", roles.timestamp, false);
|
|
889
|
+
write("end", roles.timestamp, false);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
write("start", roles.start, (bits & OPEN_START) !== 0);
|
|
894
|
+
write("end", roles.end, (bits & OPEN_END) !== 0);
|
|
895
|
+
}
|
|
896
|
+
if (version === "1.3" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
|
|
897
|
+
const items = listItems(roles.timestamps, row).map((t) => formatTimeValue(t, timeFormat));
|
|
898
|
+
out += ` timestamps="${escapeXmlAttribute(`<[${items.join(", ")}]>`)}"`;
|
|
899
|
+
}
|
|
900
|
+
return out;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The `<spells>` element of one element: its spells column, plus (1.2) its timestamps as [t, t].
|
|
904
|
+
* @param roles - the domain's role columns
|
|
905
|
+
* @param row - the element's row
|
|
906
|
+
* @param plan - the plan
|
|
907
|
+
* @param indent - the indentation of the element
|
|
908
|
+
* @returns the lines, or an empty string
|
|
909
|
+
*/
|
|
910
|
+
function spellsElement(roles, row, plan, indent) {
|
|
911
|
+
const pairs = [];
|
|
912
|
+
if (roles.spells !== null && roles.spells.isSet(row)) {
|
|
913
|
+
for (const pair of listItems(roles.spells, row)) {
|
|
914
|
+
const [s, e] = Array.from(pair);
|
|
915
|
+
pairs.push([s, e]);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (plan.version === "1.2" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
|
|
919
|
+
for (const t of listItems(roles.timestamps, row)) {
|
|
920
|
+
pairs.push([t, t]);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (pairs.length === 0) {
|
|
924
|
+
return "";
|
|
925
|
+
}
|
|
926
|
+
let out = `${indent}<spells>\n`;
|
|
927
|
+
for (const [s, e] of pairs) {
|
|
928
|
+
let attrs = "";
|
|
929
|
+
if (Number.isFinite(s)) {
|
|
930
|
+
attrs += ` start="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
|
|
931
|
+
}
|
|
932
|
+
if (Number.isFinite(e)) {
|
|
933
|
+
attrs += ` end="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
|
|
934
|
+
}
|
|
935
|
+
out += `${indent} <spell${attrs}/>\n`;
|
|
936
|
+
}
|
|
937
|
+
return `${out}${indent}</spells>\n`;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* The `<attvalues>` element of one node or edge: static cells and dynamic rows.
|
|
941
|
+
* @param attrs - the domain's declarations
|
|
942
|
+
* @param row - the element's row
|
|
943
|
+
* @param plan - the plan
|
|
944
|
+
* @param indent - the indentation of the element
|
|
945
|
+
* @returns the lines, or an empty string
|
|
946
|
+
*/
|
|
947
|
+
function attvaluesElement(attrs, row, plan, indent) {
|
|
948
|
+
let out = "";
|
|
949
|
+
for (const spec of attrs) {
|
|
950
|
+
const { column } = spec;
|
|
951
|
+
if (column !== null && column.isSet(row)) {
|
|
952
|
+
let text;
|
|
953
|
+
if (spec.companion !== null && spec.companion.isSet(row)) {
|
|
954
|
+
text = spec.companion.value(row);
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
text = spec.format(cellValue(column, row));
|
|
958
|
+
}
|
|
959
|
+
if (text !== null) {
|
|
960
|
+
out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"/>\n`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const { table } = spec;
|
|
964
|
+
if (table === null) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const rows = table.rows.get(row);
|
|
968
|
+
if (rows === undefined) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
for (const r of rows) {
|
|
972
|
+
if (!table.value.isSet(r)) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
let text;
|
|
976
|
+
if (table.valueText !== null && table.valueText.isSet(r)) {
|
|
977
|
+
text = table.valueText.value(r);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
text = table.format(cellValue(table.value, r));
|
|
981
|
+
}
|
|
982
|
+
if (text === null) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
out += `${indent} <attvalue for="${escapeXmlAttribute(spec.id)}" value="${escapeXmlAttribute(text)}"${timedAttrs(table, r, plan)}/>\n`;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return out.length === 0 ? "" : `${indent}<attvalues>\n${out}${indent}</attvalues>\n`;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* The time bounds of one temporal table row as attributes.
|
|
992
|
+
* @param table - the table
|
|
993
|
+
* @param r - the row
|
|
994
|
+
* @param plan - the plan
|
|
995
|
+
* @returns the attribute text, starting with a space when non-empty
|
|
996
|
+
*/
|
|
997
|
+
function timedAttrs(table, r, plan) {
|
|
998
|
+
const start = table.start.isSet(r) ? table.start.value(r) : -Infinity;
|
|
999
|
+
const end = table.end.isSet(r) ? table.end.value(r) : Infinity;
|
|
1000
|
+
let out = "";
|
|
1001
|
+
if (plan.version === "1.3" && plan.timeRepresentation === "timestamp" && start === end && Number.isFinite(start)) {
|
|
1002
|
+
return ` timestamp="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
|
|
1003
|
+
}
|
|
1004
|
+
let bits = 0;
|
|
1005
|
+
if (plan.version === "1.2" && table.open !== null && table.open.isSet(r)) {
|
|
1006
|
+
bits = table.open.value(r);
|
|
1007
|
+
}
|
|
1008
|
+
if (Number.isFinite(start)) {
|
|
1009
|
+
const name = (bits & OPEN_START) !== 0 ? "startopen" : "start";
|
|
1010
|
+
out += ` ${name}="${escapeXmlAttribute(timeText(table.start, table.startText, r, plan.timeFormat))}"`;
|
|
1011
|
+
}
|
|
1012
|
+
if (Number.isFinite(end)) {
|
|
1013
|
+
const name = (bits & OPEN_END) !== 0 ? "endopen" : "end";
|
|
1014
|
+
out += ` ${name}="${escapeXmlAttribute(timeText(table.end, table.endText, r, plan.timeFormat))}"`;
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* The value of a set cell as a plain JS value: lists through sliceOf, json through values, a
|
|
1020
|
+
* stride column as an array of lanes.
|
|
1021
|
+
* @param column - the column
|
|
1022
|
+
* @param row - the row
|
|
1023
|
+
* @returns the value
|
|
1024
|
+
*/
|
|
1025
|
+
function cellValue(column, row) {
|
|
1026
|
+
switch (column.dtype) {
|
|
1027
|
+
case "list":
|
|
1028
|
+
return column.sliceOf(row);
|
|
1029
|
+
case "json":
|
|
1030
|
+
return column.values[row];
|
|
1031
|
+
default: {
|
|
1032
|
+
const value = column.value(row);
|
|
1033
|
+
return ArrayBuffer.isView(value) ? Array.from(value) : value;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Whether `viz:position` gets a z attribute: always for a 3-d source, and for a 2-d source
|
|
1039
|
+
* (`extra.sourceDims === 2`) only when some node has since been given a non-zero z.
|
|
1040
|
+
* @param position - the position role column, or null
|
|
1041
|
+
* @returns true when z is written
|
|
1042
|
+
*/
|
|
1043
|
+
function positionWritesZ(position) {
|
|
1044
|
+
if (position === null || position.meta.components < 3) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
if (position.meta.extra.sourceDims !== 2) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
const { components } = position.meta;
|
|
1051
|
+
let data;
|
|
1052
|
+
switch (position.dtype) {
|
|
1053
|
+
case "f32":
|
|
1054
|
+
case "f64":
|
|
1055
|
+
case "i32":
|
|
1056
|
+
case "u32":
|
|
1057
|
+
case "u8":
|
|
1058
|
+
({ data } = position);
|
|
1059
|
+
break;
|
|
1060
|
+
default:
|
|
1061
|
+
// A non-numeric position column has no z lane to inspect; write z so nothing is lost.
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
for (let i = 0; i < position.length; i++) {
|
|
1065
|
+
if (position.isSet(i) && data[i * components + 2] !== 0) {
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* The items of a set row of a list column.
|
|
1073
|
+
* @param column - a list column
|
|
1074
|
+
* @param row - the row
|
|
1075
|
+
* @returns the items; empty when the column is not a list
|
|
1076
|
+
*/
|
|
1077
|
+
function listItems(column, row) {
|
|
1078
|
+
return column.dtype === "list" ? column.sliceOf(row) : [];
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* The viz elements of one node or edge.
|
|
1082
|
+
* @param roles - the domain's role columns
|
|
1083
|
+
* @param row - the element's row
|
|
1084
|
+
* @param indent - the indentation
|
|
1085
|
+
* @param writeZ - whether viz:position gets its z attribute
|
|
1086
|
+
* @returns the lines, or an empty string
|
|
1087
|
+
*/
|
|
1088
|
+
function vizElements(roles, row, indent, writeZ) {
|
|
1089
|
+
let out = "";
|
|
1090
|
+
const { color } = roles;
|
|
1091
|
+
if (color !== null && color.isSet(row)) {
|
|
1092
|
+
const lanes = Array.from(color.value(row));
|
|
1093
|
+
const scale = color.dtype === "u8" ? 1 : 255;
|
|
1094
|
+
const channel = (v) => String(Math.max(0, Math.min(255, Math.round(v * scale))));
|
|
1095
|
+
let attrs = ` r="${channel(lanes[0])}" g="${channel(lanes[1])}" b="${channel(lanes[2])}"`;
|
|
1096
|
+
if (lanes.length > 3) {
|
|
1097
|
+
const a = color.dtype === "u8" ? lanes[3] / 255 : lanes[3];
|
|
1098
|
+
if (a !== 1) {
|
|
1099
|
+
attrs += ` a="${formatF32(Math.fround(a))}"`;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
out += `${indent}<viz:color${attrs}/>\n`;
|
|
1103
|
+
}
|
|
1104
|
+
const { position } = roles;
|
|
1105
|
+
if (position !== null && position.isSet(row)) {
|
|
1106
|
+
const lanes = Array.from(position.value(row));
|
|
1107
|
+
const fmt = position.dtype === "f32" ? formatF32 : formatF64;
|
|
1108
|
+
let attrs = ` x="${fmt(lanes[0])}" y="${fmt(lanes[1])}"`;
|
|
1109
|
+
if (lanes.length > 2 && writeZ) {
|
|
1110
|
+
attrs += ` z="${fmt(lanes[2])}"`;
|
|
1111
|
+
}
|
|
1112
|
+
out += `${indent}<viz:position${attrs}/>\n`;
|
|
1113
|
+
}
|
|
1114
|
+
for (const [name, column] of [
|
|
1115
|
+
["size", roles.size],
|
|
1116
|
+
["thickness", roles.thickness],
|
|
1117
|
+
]) {
|
|
1118
|
+
if (column !== null && column.isSet(row)) {
|
|
1119
|
+
const value = column.value(row);
|
|
1120
|
+
const text = column.dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
1121
|
+
out += `${indent}<viz:${name} value="${escapeXmlAttribute(text)}"/>\n`;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const { shape } = roles;
|
|
1125
|
+
if (shape !== null && shape.isSet(row)) {
|
|
1126
|
+
let attrs = ` value="${escapeXmlAttribute(String(shape.value(row)))}"`;
|
|
1127
|
+
if (roles.shapeUri !== null && roles.shapeUri.isSet(row)) {
|
|
1128
|
+
attrs += ` uri="${escapeXmlAttribute(roles.shapeUri.value(row))}"`;
|
|
1129
|
+
}
|
|
1130
|
+
out += `${indent}<viz:shape${attrs}/>\n`;
|
|
1131
|
+
}
|
|
1132
|
+
return out;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* The `<attributes>` groups of one class.
|
|
1136
|
+
* @param cls - node or edge
|
|
1137
|
+
* @param specs - the declarations
|
|
1138
|
+
* @param indent - the indentation
|
|
1139
|
+
* @returns the lines, or an empty string
|
|
1140
|
+
*/
|
|
1141
|
+
function attributesGroups(cls, specs, indent) {
|
|
1142
|
+
let out = "";
|
|
1143
|
+
for (const mode of ["static", "dynamic"]) {
|
|
1144
|
+
const group = specs.filter((s) => s.dynamic === (mode === "dynamic"));
|
|
1145
|
+
if (group.length === 0) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
out += `${indent}<attributes class="${cls}" mode="${mode}">\n`;
|
|
1149
|
+
for (const spec of group) {
|
|
1150
|
+
const head = `${indent} <attribute id="${escapeXmlAttribute(spec.id)}" title="${escapeXmlAttribute(spec.title)}" type="${spec.type}"`;
|
|
1151
|
+
if (spec.defaultText === null && spec.optionsText === null) {
|
|
1152
|
+
out += `${head}/>\n`;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
out += `${head}>\n`;
|
|
1156
|
+
if (spec.defaultText !== null) {
|
|
1157
|
+
out += `${indent} <default>${escapeXmlText(spec.defaultText)}</default>\n`;
|
|
1158
|
+
}
|
|
1159
|
+
if (spec.optionsText !== null) {
|
|
1160
|
+
out += `${indent} <options>${escapeXmlText(spec.optionsText)}</options>\n`;
|
|
1161
|
+
}
|
|
1162
|
+
out += `${indent} </attribute>\n`;
|
|
1163
|
+
}
|
|
1164
|
+
out += `${indent}</attributes>\n`;
|
|
1165
|
+
}
|
|
1166
|
+
return out;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* The `type` of a logical edge after folding an expanded pair (design section 3.6).
|
|
1170
|
+
* @param snapshot - the snapshot
|
|
1171
|
+
* @param e - the logical edge index
|
|
1172
|
+
* @param folding - the pair-folding view (mutual pairs fold: GEXF has the mutual type)
|
|
1173
|
+
* @returns the edge type, or null when the edge is the mirror half of a pair
|
|
1174
|
+
*/
|
|
1175
|
+
function edgeType(snapshot, e, folding) {
|
|
1176
|
+
if (!snapshot.directed) {
|
|
1177
|
+
return "undirected";
|
|
1178
|
+
}
|
|
1179
|
+
if (folding.folded(e)) {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
if (!folding.sourceDirected(e)) {
|
|
1183
|
+
return "undirected";
|
|
1184
|
+
}
|
|
1185
|
+
if (folding.mateOf(e) !== INVALID_INDEX || folding.isMutual(e)) {
|
|
1186
|
+
return "mutual";
|
|
1187
|
+
}
|
|
1188
|
+
return "directed";
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Write the document as text parts.
|
|
1192
|
+
* @param snapshot - the snapshot
|
|
1193
|
+
* @param options - the caller's options
|
|
1194
|
+
* @yields one part per element or line group
|
|
1195
|
+
* @returns nothing
|
|
1196
|
+
*/
|
|
1197
|
+
function* writeGexf(snapshot, options) {
|
|
1198
|
+
const plan = planExport(snapshot, options);
|
|
1199
|
+
const { version } = plan;
|
|
1200
|
+
const ids = sanitizeIds(snapshot, "any", plan.options.sanitizeIds);
|
|
1201
|
+
const idText = (index) => escapeXmlAttribute(String(ids.idAt(index)));
|
|
1202
|
+
const ns = GEXF_NAMESPACES[version];
|
|
1203
|
+
const { meta } = snapshot;
|
|
1204
|
+
yield '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
1205
|
+
yield `<gexf xmlns="${ns.gexf}" xmlns:viz="${ns.viz}" version="${version}">\n`;
|
|
1206
|
+
yield metaElement(meta);
|
|
1207
|
+
yield graphStart(snapshot, plan);
|
|
1208
|
+
yield attributesGroups("node", plan.nodeAttrs, " ");
|
|
1209
|
+
yield attributesGroups("edge", plan.edgeAttrs, " ");
|
|
1210
|
+
// nodes
|
|
1211
|
+
const { nodeRoles } = plan;
|
|
1212
|
+
const writeZ = positionWritesZ(nodeRoles.position);
|
|
1213
|
+
yield ` <nodes count="${snapshot.nodeCount}">\n`;
|
|
1214
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1215
|
+
let attrs = ` id="${idText(i)}"`;
|
|
1216
|
+
if (nodeRoles.label !== null && nodeRoles.label.isSet(i)) {
|
|
1217
|
+
attrs += ` label="${escapeXmlAttribute(String(nodeRoles.label.value(i)))}"`;
|
|
1218
|
+
}
|
|
1219
|
+
if (nodeRoles.parent !== null && nodeRoles.parent.isSet(i)) {
|
|
1220
|
+
attrs += ` pid="${idText(nodeRoles.parent.value(i))}"`;
|
|
1221
|
+
}
|
|
1222
|
+
attrs += lifetimeAttrs(nodeRoles, i, plan);
|
|
1223
|
+
let body = attvaluesElement(plan.nodeAttrs, i, plan, " ");
|
|
1224
|
+
if (nodeRoles.parents !== null && nodeRoles.parents.isSet(i)) {
|
|
1225
|
+
const parents = listItems(nodeRoles.parents, i);
|
|
1226
|
+
if (parents.length > 0) {
|
|
1227
|
+
body += " <parents>\n";
|
|
1228
|
+
for (const p of parents) {
|
|
1229
|
+
body += ` <parent for="${idText(p)}"/>\n`;
|
|
1230
|
+
}
|
|
1231
|
+
body += " </parents>\n";
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
body += spellsElement(nodeRoles, i, plan, " ");
|
|
1235
|
+
body += vizElements(nodeRoles, i, " ", writeZ);
|
|
1236
|
+
yield body.length === 0 ? ` <node${attrs}/>\n` : ` <node${attrs}>\n${body} </node>\n`;
|
|
1237
|
+
}
|
|
1238
|
+
yield " </nodes>\n";
|
|
1239
|
+
// edges: the count is decided in a cheap first pass (no strings built) so the section
|
|
1240
|
+
// streams element by element instead of buffering every edge
|
|
1241
|
+
const { edgeRoles } = plan;
|
|
1242
|
+
const edgeList = snapshot.edgeList();
|
|
1243
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
1244
|
+
const weights = explicitWeights(snapshot);
|
|
1245
|
+
const defaultType = snapshot.directed ? "directed" : "undirected";
|
|
1246
|
+
let written = 0;
|
|
1247
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1248
|
+
if (edgeType(snapshot, e, folding) !== null) {
|
|
1249
|
+
written++;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
yield ` <edges count="${written}">\n`;
|
|
1253
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1254
|
+
const type = edgeType(snapshot, e, folding);
|
|
1255
|
+
if (type === null) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
let attrs = "";
|
|
1259
|
+
if (edgeRoles.id !== null && edgeRoles.id.isSet(e)) {
|
|
1260
|
+
attrs += ` id="${escapeXmlAttribute(String(edgeRoles.id.value(e)))}"`;
|
|
1261
|
+
}
|
|
1262
|
+
else if (version === "1.2") {
|
|
1263
|
+
attrs += ` id="e${e}"`;
|
|
1264
|
+
}
|
|
1265
|
+
attrs += ` source="${idText(edgeList.src[e])}" target="${idText(edgeList.dst[e])}"`;
|
|
1266
|
+
if (type !== defaultType) {
|
|
1267
|
+
attrs += ` type="${type}"`;
|
|
1268
|
+
}
|
|
1269
|
+
const weight = weights.text(e);
|
|
1270
|
+
if (weight !== null) {
|
|
1271
|
+
attrs += ` weight="${weight}"`;
|
|
1272
|
+
}
|
|
1273
|
+
if (edgeRoles.label !== null && edgeRoles.label.isSet(e)) {
|
|
1274
|
+
attrs += ` label="${escapeXmlAttribute(String(edgeRoles.label.value(e)))}"`;
|
|
1275
|
+
}
|
|
1276
|
+
if (version === "1.3" && edgeRoles.kind !== null && edgeRoles.kind.isSet(e)) {
|
|
1277
|
+
attrs += ` kind="${escapeXmlAttribute(String(edgeRoles.kind.value(e)))}"`;
|
|
1278
|
+
}
|
|
1279
|
+
attrs += lifetimeAttrs(edgeRoles, e, plan);
|
|
1280
|
+
let body = attvaluesElement(plan.edgeAttrs, e, plan, " ");
|
|
1281
|
+
body += spellsElement(edgeRoles, e, plan, " ");
|
|
1282
|
+
body += vizElements(edgeRoles, e, " ", false);
|
|
1283
|
+
yield body.length === 0 ? ` <edge${attrs}/>\n` : ` <edge${attrs}>\n${body} </edge>\n`;
|
|
1284
|
+
}
|
|
1285
|
+
yield " </edges>\n";
|
|
1286
|
+
yield " </graph>\n";
|
|
1287
|
+
yield "</gexf>\n";
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* The `<meta>` element.
|
|
1291
|
+
* @param meta - the graph meta
|
|
1292
|
+
* @returns the lines, or an empty string when nothing is set
|
|
1293
|
+
*/
|
|
1294
|
+
function metaElement(meta) {
|
|
1295
|
+
let body = "";
|
|
1296
|
+
if (meta.creator !== null) {
|
|
1297
|
+
body += ` <creator>${escapeXmlText(meta.creator)}</creator>\n`;
|
|
1298
|
+
}
|
|
1299
|
+
if (meta.description !== null) {
|
|
1300
|
+
body += ` <description>${escapeXmlText(meta.description)}</description>\n`;
|
|
1301
|
+
}
|
|
1302
|
+
if (meta.keywords.length > 0) {
|
|
1303
|
+
body += ` <keywords>${escapeXmlText(meta.keywords.join(", "))}</keywords>\n`;
|
|
1304
|
+
}
|
|
1305
|
+
const modified = meta.modified === null ? "" : ` lastmodifieddate="${escapeXmlAttribute(meta.modified)}"`;
|
|
1306
|
+
if (body.length === 0 && modified.length === 0) {
|
|
1307
|
+
return "";
|
|
1308
|
+
}
|
|
1309
|
+
return body.length === 0 ? ` <meta${modified}/>\n` : ` <meta${modified}>\n${body} </meta>\n`;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* The `<graph>` start tag with its header attributes.
|
|
1313
|
+
* @param snapshot - the snapshot
|
|
1314
|
+
* @param plan - the plan
|
|
1315
|
+
* @returns the line
|
|
1316
|
+
*/
|
|
1317
|
+
function graphStart(snapshot, plan) {
|
|
1318
|
+
const { meta } = snapshot;
|
|
1319
|
+
let attrs = ` defaultedgetype="${snapshot.directed ? "directed" : "undirected"}"`;
|
|
1320
|
+
let mode = meta.mode ?? "static";
|
|
1321
|
+
if (plan.temporal && mode === "static") {
|
|
1322
|
+
mode = "dynamic";
|
|
1323
|
+
}
|
|
1324
|
+
if (plan.version === "1.2" && mode === "slice") {
|
|
1325
|
+
mode = "dynamic";
|
|
1326
|
+
}
|
|
1327
|
+
attrs += ` mode="${mode}"`;
|
|
1328
|
+
let { idType } = meta;
|
|
1329
|
+
if (idType === null) {
|
|
1330
|
+
const { kind } = snapshot.ids;
|
|
1331
|
+
if (kind === "string") {
|
|
1332
|
+
idType = "string";
|
|
1333
|
+
}
|
|
1334
|
+
else if (kind !== "mixed") {
|
|
1335
|
+
idType = "integer";
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (idType === "integer" || idType === "string") {
|
|
1339
|
+
attrs += ` idtype="${idType}"`;
|
|
1340
|
+
}
|
|
1341
|
+
if (plan.timeFormat !== null) {
|
|
1342
|
+
attrs += ` timeformat="${plan.timeFormat}"`;
|
|
1343
|
+
}
|
|
1344
|
+
else if (plan.temporal) {
|
|
1345
|
+
attrs += ' timeformat="double"';
|
|
1346
|
+
}
|
|
1347
|
+
if (plan.version === "1.3" && plan.timeRepresentation !== null) {
|
|
1348
|
+
attrs += ` timerepresentation="${plan.timeRepresentation}"`;
|
|
1349
|
+
}
|
|
1350
|
+
const gexfExtra = meta.extra.gexf;
|
|
1351
|
+
if (typeof gexfExtra === "object" && gexfExtra !== null) {
|
|
1352
|
+
for (const key of ["start", "end", "timestamp"]) {
|
|
1353
|
+
const value = gexfExtra[key];
|
|
1354
|
+
if (typeof value === "string") {
|
|
1355
|
+
attrs += ` ${key}="${escapeXmlAttribute(value)}"`;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return ` <graph${attrs}>\n`;
|
|
1360
|
+
}
|
|
1361
|
+
/** The GEXF exporter (design section 8.5); `capabilities` describes the default 1.3 output. */
|
|
1362
|
+
export const gexfExporter = Object.freeze({
|
|
1363
|
+
format: GEXF_FORMAT,
|
|
1364
|
+
capabilities: CAPABILITIES_1_3,
|
|
1365
|
+
/**
|
|
1366
|
+
* Pre-flight: what export() would lose.
|
|
1367
|
+
* @param snapshot - the snapshot
|
|
1368
|
+
* @param options - format-specific and common options
|
|
1369
|
+
* @returns the loss notes, empty when the export is exact
|
|
1370
|
+
*/
|
|
1371
|
+
check(snapshot, options) {
|
|
1372
|
+
return Object.freeze(planExport(snapshot, options).notes);
|
|
1373
|
+
},
|
|
1374
|
+
/**
|
|
1375
|
+
* Write the snapshot as UTF-8 chunks.
|
|
1376
|
+
* @param snapshot - the snapshot
|
|
1377
|
+
* @param options - format-specific and common options
|
|
1378
|
+
* @returns the chunks
|
|
1379
|
+
*/
|
|
1380
|
+
export(snapshot, options) {
|
|
1381
|
+
return encodeChunks(writeGexf(snapshot, options));
|
|
1382
|
+
},
|
|
1383
|
+
/**
|
|
1384
|
+
* Write the snapshot as one string.
|
|
1385
|
+
* @param snapshot - the snapshot
|
|
1386
|
+
* @param options - format-specific and common options
|
|
1387
|
+
* @returns the document
|
|
1388
|
+
*/
|
|
1389
|
+
async exportToString(snapshot, options) {
|
|
1390
|
+
return joinText(writeGexf(snapshot, options));
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
/** The capabilities of a 1.2 export, for callers that pass `version: "1.2"`. */
|
|
1394
|
+
export const GEXF_1_2_CAPABILITIES = CAPABILITIES_1_2;
|
|
1395
|
+
//# sourceMappingURL=exporter.js.map
|