@graphty/graph-io 0.3.40 → 0.3.41
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/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-DBb8zuNE.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-DBb8zuNE.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
- package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-BqtmlxNr.js} +545 -459
- package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-CS35R6Fg.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-CS35R6Fg.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-D8r1N2dU.js} +28 -16
- package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
- package/dist/chunks/{importer-CPqnIY8m.js → importer-BKC7AmCA.js} +42 -29
- package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-BdWHmlmG.js} +4 -4
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-BdWHmlmG.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-BfbzWk6k.js} +202 -181
- package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-BmaZcskq.js} +73 -60
- package/dist/chunks/importer-BmaZcskq.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-DviEsHy4.js} +92 -64
- package/dist/chunks/importer-DviEsHy4.js.map +1 -0
- package/dist/chunks/{importer-DcKB97kW.js → importer-S-abxJHn.js} +5 -5
- package/dist/chunks/{importer-DcKB97kW.js.map → importer-S-abxJHn.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-r29cWnEf.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-r29cWnEf.js.map} +1 -1
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DdF-etPT.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DdF-etPT.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Uc3LkABx.js} +15 -12
- package/dist/chunks/records-Uc3LkABx.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
- package/dist/chunks/text-Bh9SaAob.js.map +1 -0
- package/dist/csv.js +67 -49
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +195 -97
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +26 -26
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +34 -9
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +180 -102
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +77 -25
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +16 -6
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +18 -10
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +65 -37
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +34 -24
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +17 -13
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +7 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +79 -40
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +38 -12
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/attributes.ts +59 -19
- package/src/common/input.ts +216 -102
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +103 -39
- package/src/formats/csv/importer.ts +17 -6
- package/src/formats/csv/records.ts +19 -10
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/gexf/exporter.ts +67 -36
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +44 -24
- package/src/formats/graphml/importer.ts +18 -11
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +88 -40
- package/src/formats/xgmml/exporter.ts +40 -12
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
- package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
- package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
- package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
- package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
|
@@ -1794,100 +1794,141 @@ function importAdjacency(ctx, root) {
|
|
|
1794
1794
|
ctx.setHeader(directed);
|
|
1795
1795
|
// adjacency_data writes the graph dict as a list of [key, value] pairs
|
|
1796
1796
|
ctx.writeGraphDict(isPairList(root.graph) ? Object.fromEntries(root.graph) : root.graph, "graph");
|
|
1797
|
-
|
|
1798
|
-
if (key !== "nodes" && key !== "adjacency" && key !== "directed" && key !== "multigraph" && key !== "graph") {
|
|
1799
|
-
report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${describe(root[key])}) is not read; dropped`, {
|
|
1800
|
-
element: key,
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1797
|
+
reportUnreadAdjacencyKeys(ctx, root);
|
|
1804
1798
|
const lists = adjacency ?? [];
|
|
1805
1799
|
ctx.sink.reserve(nodes.length, lists.reduce((sum, list) => sum + (Array.isArray(list) ? list.length : 0), 0));
|
|
1806
1800
|
const idKey = ctx.json.nodeIdKey ?? "id";
|
|
1807
1801
|
ctx.reportNodeIdFrom("adjacency", `the ${JSON.stringify(idKey)} key`);
|
|
1808
|
-
const owners = [];
|
|
1809
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1810
|
-
const element = `nodes[${i}]`;
|
|
1811
|
-
const record = nodes[i];
|
|
1812
|
-
let pushed = null;
|
|
1813
|
-
if (!isJsonObject(record)) {
|
|
1814
|
-
ctx.badElement("node", element);
|
|
1815
|
-
}
|
|
1816
|
-
else {
|
|
1817
|
-
const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
|
|
1818
|
-
if (id === null) {
|
|
1819
|
-
ctx.countSkipped("node");
|
|
1820
|
-
}
|
|
1821
|
-
else {
|
|
1822
|
-
const index = ctx.pushNode(id, element);
|
|
1823
|
-
if (index >= 0) {
|
|
1824
|
-
pushed = id;
|
|
1825
|
-
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
owners.push(pushed);
|
|
1830
|
-
}
|
|
1802
|
+
const owners = nodes.map((record, i) => pushAdjacencyNode(ctx, record, `nodes[${i}]`, idKey));
|
|
1831
1803
|
throwIfAborted(ctx.options.signal);
|
|
1832
|
-
const
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1804
|
+
const reading = {
|
|
1805
|
+
owners,
|
|
1806
|
+
nodeCount: nodes.length,
|
|
1807
|
+
directed,
|
|
1808
|
+
kind: ctx.uniformKind(),
|
|
1809
|
+
idKey,
|
|
1810
|
+
// undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
|
|
1811
|
+
pending: new Map(),
|
|
1812
|
+
disagreeing: 0,
|
|
1813
|
+
};
|
|
1837
1814
|
for (let i = 0; i < lists.length; i++) {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1815
|
+
readAdjacencyList(ctx, reading, i, lists[i]);
|
|
1816
|
+
}
|
|
1817
|
+
reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, reading.disagreeing, reading.pending);
|
|
1818
|
+
ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
|
|
1819
|
+
}
|
|
1820
|
+
/** The top-level keys of an adjacency_data document that are read. */
|
|
1821
|
+
const ADJACENCY_DOCUMENT_KEYS = new Set(["nodes", "adjacency", "directed", "multigraph", "graph"]);
|
|
1822
|
+
/**
|
|
1823
|
+
* Report the top-level keys of an adjacency_data document that are not read.
|
|
1824
|
+
* @param ctx - the context
|
|
1825
|
+
* @param root - the document
|
|
1826
|
+
*/
|
|
1827
|
+
function reportUnreadAdjacencyKeys(ctx, root) {
|
|
1828
|
+
for (const key of Object.keys(root)) {
|
|
1829
|
+
if (!ADJACENCY_DOCUMENT_KEYS.has(key)) {
|
|
1830
|
+
ctx.report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${describe(root[key])}) is not read; dropped`, {
|
|
1831
|
+
element: key,
|
|
1849
1832
|
});
|
|
1850
|
-
report.counts.skippedEdges += list.length;
|
|
1851
|
-
continue;
|
|
1852
1833
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Push one node record of an adjacency_data document.
|
|
1838
|
+
* @param ctx - the context
|
|
1839
|
+
* @param record - the node record
|
|
1840
|
+
* @param element - the element name
|
|
1841
|
+
* @param idKey - the key that holds the id
|
|
1842
|
+
* @returns the node's id, or null when it was skipped
|
|
1843
|
+
*/
|
|
1844
|
+
function pushAdjacencyNode(ctx, record, element, idKey) {
|
|
1845
|
+
if (!isJsonObject(record)) {
|
|
1846
|
+
ctx.badElement("node", element);
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
|
|
1850
|
+
if (id === null) {
|
|
1851
|
+
ctx.countSkipped("node");
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
const index = ctx.pushNode(id, element);
|
|
1855
|
+
if (index < 0) {
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
1859
|
+
return id;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Read one adjacency list: the edges of nodes[i]. A list whose owner was skipped is skipped whole.
|
|
1863
|
+
* @param ctx - the context
|
|
1864
|
+
* @param reading - the read's state
|
|
1865
|
+
* @param i - the list's position
|
|
1866
|
+
* @param list - the list
|
|
1867
|
+
*/
|
|
1868
|
+
function readAdjacencyList(ctx, reading, i, list) {
|
|
1869
|
+
const { report } = ctx;
|
|
1870
|
+
const element = `adjacency[${i}]`;
|
|
1871
|
+
if (!Array.isArray(list)) {
|
|
1872
|
+
report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not an array`, { element });
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const source = i < reading.owners.length ? reading.owners[i] : null;
|
|
1876
|
+
if (source === null) {
|
|
1877
|
+
const why = i < reading.nodeCount ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
|
|
1878
|
+
report.error("missing-value", JSON_ISSUE.BAD_INDEX, `${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`, {
|
|
1879
|
+
element,
|
|
1880
|
+
});
|
|
1881
|
+
report.counts.skippedEdges += list.length;
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
for (let j = 0; j < list.length; j++) {
|
|
1885
|
+
readAdjacencyEntry(ctx, reading, source, list[j], element, `${element}[${j}]`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Read one adjacency entry as an edge from the list's owner, unless it is the mirror of an
|
|
1890
|
+
* undirected edge already read.
|
|
1891
|
+
* @param ctx - the context
|
|
1892
|
+
* @param reading - the read's state
|
|
1893
|
+
* @param source - the list's owner
|
|
1894
|
+
* @param record - the entry
|
|
1895
|
+
* @param element - the list's element name (attribute issues name it)
|
|
1896
|
+
* @param entry - the entry's element name
|
|
1897
|
+
*/
|
|
1898
|
+
function readAdjacencyEntry(ctx, reading, source, record, element, entry) {
|
|
1899
|
+
const { idKey } = reading;
|
|
1900
|
+
if (!isJsonObject(record)) {
|
|
1901
|
+
ctx.badElement("edge", entry);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (!hasKey(record, idKey)) {
|
|
1905
|
+
ctx.missingEndpoint(entry, idKey);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
const target = ctx.coerceId(record[idKey], `${entry}.${idKey}`);
|
|
1910
|
+
if (target === null) {
|
|
1911
|
+
ctx.countSkipped("edge");
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (!reading.directed) {
|
|
1915
|
+
const mirror = mirroredEntry(reading.pending, source, target, record, idKey);
|
|
1916
|
+
reading.disagreeing += mirror === "disagrees" ? 1 : 0;
|
|
1917
|
+
if (mirror !== "new") {
|
|
1918
|
+
return;
|
|
1883
1919
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1920
|
+
}
|
|
1921
|
+
const edge = ctx.pushEdge(source, target, reading.kind, ctx.weightOf(record), entry);
|
|
1922
|
+
const { weightFrom } = ctx.options;
|
|
1923
|
+
for (const key of Object.keys(record)) {
|
|
1924
|
+
if (key !== idKey && key !== weightFrom) {
|
|
1925
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
1886
1926
|
}
|
|
1887
1927
|
}
|
|
1888
1928
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1929
|
+
catch (err) {
|
|
1930
|
+
ctx.skip(err, "edge", entry);
|
|
1931
|
+
}
|
|
1891
1932
|
}
|
|
1892
1933
|
/**
|
|
1893
1934
|
* Report what an adjacency_data document leaves inconsistent: nodes without an adjacency list
|
|
@@ -2040,26 +2081,8 @@ function importGraphology(ctx, root) {
|
|
|
2040
2081
|
element: "options",
|
|
2041
2082
|
});
|
|
2042
2083
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
({ type } = options);
|
|
2046
|
-
}
|
|
2047
|
-
else {
|
|
2048
|
-
if (options.type !== undefined && options.type !== null) {
|
|
2049
|
-
report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`, { element: "options.type" });
|
|
2050
|
-
}
|
|
2051
|
-
type = "mixed";
|
|
2052
|
-
}
|
|
2053
|
-
let directed;
|
|
2054
|
-
if (type === "mixed") {
|
|
2055
|
-
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2056
|
-
const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
|
|
2057
|
-
directed = objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
|
|
2058
|
-
}
|
|
2059
|
-
else {
|
|
2060
|
-
directed = type === "directed";
|
|
2061
|
-
}
|
|
2062
|
-
ctx.setHeader(directed);
|
|
2084
|
+
const type = graphologyType(ctx, options);
|
|
2085
|
+
ctx.setHeader(graphologyDirected(ctx, type, edges));
|
|
2063
2086
|
const multi = hasKey(options, "multi") ? flagOf(options.multi, "options.multi", false, report) : null;
|
|
2064
2087
|
const allowSelfLoops = hasKey(options, "allowSelfLoops")
|
|
2065
2088
|
? flagOf(options.allowSelfLoops, "options.allowSelfLoops", true, report)
|
|
@@ -2068,76 +2091,147 @@ function importGraphology(ctx, root) {
|
|
|
2068
2091
|
ctx.reportNodeIdFrom("graphology", "the node key");
|
|
2069
2092
|
ctx.sink.reserve(nodes.length, edges.length);
|
|
2070
2093
|
for (let i = 0; i < nodes.length; i++) {
|
|
2071
|
-
|
|
2072
|
-
const record = nodes[i];
|
|
2073
|
-
if (!isJsonObject(record)) {
|
|
2074
|
-
ctx.badElement("node", element);
|
|
2075
|
-
continue;
|
|
2076
|
-
}
|
|
2077
|
-
const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
|
|
2078
|
-
if (id === null) {
|
|
2079
|
-
ctx.countSkipped("node");
|
|
2080
|
-
continue;
|
|
2081
|
-
}
|
|
2082
|
-
pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
|
|
2094
|
+
pushGraphologyNode(ctx, nodes[i], `nodes[${i}]`);
|
|
2083
2095
|
}
|
|
2084
2096
|
throwIfAborted(ctx.options.signal);
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2097
|
+
const reading = {
|
|
2098
|
+
ids: ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true),
|
|
2099
|
+
type,
|
|
2100
|
+
allowSelfLoops,
|
|
2101
|
+
// what the edges do against the declared options: graphology itself refuses such a document
|
|
2102
|
+
violations: { selfLoops: 0, parallels: 0, flags: 0 },
|
|
2103
|
+
pairs: multi === false ? new Set() : null,
|
|
2104
|
+
};
|
|
2089
2105
|
for (let i = 0; i < edges.length; i++) {
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2106
|
+
readGraphologyEdge(ctx, reading, edges[i], `edges[${i}]`);
|
|
2107
|
+
}
|
|
2108
|
+
reportGraphologyViolations(ctx, reading.violations, type);
|
|
2109
|
+
ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* A graphology document's `options.type`; anything else is reported and read as "mixed".
|
|
2113
|
+
* @param ctx - the context
|
|
2114
|
+
* @param options - the options object
|
|
2115
|
+
* @returns the type
|
|
2116
|
+
*/
|
|
2117
|
+
function graphologyType(ctx, options) {
|
|
2118
|
+
if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
|
|
2119
|
+
return options.type;
|
|
2120
|
+
}
|
|
2121
|
+
if (options.type !== undefined && options.type !== null) {
|
|
2122
|
+
ctx.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`, { element: "options.type" });
|
|
2123
|
+
}
|
|
2124
|
+
return "mixed";
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* The header direction of a graphology document: its type, or for "mixed" directed unless every
|
|
2128
|
+
* edge object says undirected (the dialect default when there is none).
|
|
2129
|
+
* @param ctx - the context
|
|
2130
|
+
* @param type - options.type
|
|
2131
|
+
* @param edges - the edge records
|
|
2132
|
+
* @returns whether the header is directed
|
|
2133
|
+
*/
|
|
2134
|
+
function graphologyDirected(ctx, type, edges) {
|
|
2135
|
+
if (type !== "mixed") {
|
|
2136
|
+
return type === "directed";
|
|
2137
|
+
}
|
|
2138
|
+
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2139
|
+
const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
|
|
2140
|
+
return objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Push one graphology node record: `key` the id, `attributes` the columns.
|
|
2144
|
+
* @param ctx - the context
|
|
2145
|
+
* @param record - the node record
|
|
2146
|
+
* @param element - the element name
|
|
2147
|
+
*/
|
|
2148
|
+
function pushGraphologyNode(ctx, record, element) {
|
|
2149
|
+
if (!isJsonObject(record)) {
|
|
2150
|
+
ctx.badElement("node", element);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
|
|
2154
|
+
if (id === null) {
|
|
2155
|
+
ctx.countSkipped("node");
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Read one graphology edge record, counting what it does against the declared options.
|
|
2162
|
+
* @param ctx - the context
|
|
2163
|
+
* @param reading - the read's state
|
|
2164
|
+
* @param record - the edge record
|
|
2165
|
+
* @param element - the element name
|
|
2166
|
+
*/
|
|
2167
|
+
function readGraphologyEdge(ctx, reading, record, element) {
|
|
2168
|
+
if (!isJsonObject(record)) {
|
|
2169
|
+
ctx.badElement("edge", element);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2173
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
2178
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
2179
|
+
if (u === null || v === null) {
|
|
2180
|
+
ctx.countSkipped("edge");
|
|
2181
|
+
return;
|
|
2099
2182
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
if (type === "mixed") {
|
|
2109
|
-
kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
|
|
2110
|
-
}
|
|
2111
|
-
else {
|
|
2112
|
-
kind = type;
|
|
2113
|
-
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2114
|
-
violations.flags++;
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
const { attributes: rawAttributes } = record;
|
|
2118
|
-
if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
|
|
2119
|
-
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(rawAttributes)}`, { element });
|
|
2120
|
-
}
|
|
2121
|
-
const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
|
|
2122
|
-
const idValue = ctx.edgeIdValue(ids, record.key);
|
|
2123
|
-
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2124
|
-
ctx.setEdgeId(ids, edge, idValue);
|
|
2125
|
-
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2126
|
-
if (allowSelfLoops === false && u === v) {
|
|
2127
|
-
violations.selfLoops++;
|
|
2128
|
-
}
|
|
2129
|
-
if (pairs !== null) {
|
|
2130
|
-
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2131
|
-
violations.parallels += pairs.has(pair) ? 1 : 0;
|
|
2132
|
-
pairs.add(pair);
|
|
2133
|
-
}
|
|
2183
|
+
const kind = graphologyEdgeKind(ctx, reading, record, element);
|
|
2184
|
+
const attributes = graphologyEdgeAttributes(ctx, record, element);
|
|
2185
|
+
const idValue = ctx.edgeIdValue(reading.ids, record.key);
|
|
2186
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2187
|
+
ctx.setEdgeId(reading.ids, edge, idValue);
|
|
2188
|
+
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2189
|
+
if (reading.allowSelfLoops === false && u === v) {
|
|
2190
|
+
reading.violations.selfLoops++;
|
|
2134
2191
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2192
|
+
if (reading.pairs !== null) {
|
|
2193
|
+
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2194
|
+
reading.violations.parallels += reading.pairs.has(pair) ? 1 : 0;
|
|
2195
|
+
reading.pairs.add(pair);
|
|
2137
2196
|
}
|
|
2138
2197
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2198
|
+
catch (err) {
|
|
2199
|
+
ctx.skip(err, "edge", element);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* The direction of one graphology edge: its `undirected` flag under type "mixed", else the type
|
|
2204
|
+
* (a contrary flag is counted as a violation).
|
|
2205
|
+
* @param ctx - the context
|
|
2206
|
+
* @param reading - the read's state
|
|
2207
|
+
* @param record - the edge record
|
|
2208
|
+
* @param element - the element name
|
|
2209
|
+
* @returns the edge's kind
|
|
2210
|
+
*/
|
|
2211
|
+
function graphologyEdgeKind(ctx, reading, record, element) {
|
|
2212
|
+
const { type } = reading;
|
|
2213
|
+
if (type === "mixed") {
|
|
2214
|
+
return flagOf(record.undirected, `${element}.undirected`, false, ctx.report) ? "undirected" : "directed";
|
|
2215
|
+
}
|
|
2216
|
+
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2217
|
+
reading.violations.flags++;
|
|
2218
|
+
}
|
|
2219
|
+
return type;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* The `attributes` object of a graphology edge; any other non-null value is reported and read as
|
|
2223
|
+
* no attributes.
|
|
2224
|
+
* @param ctx - the context
|
|
2225
|
+
* @param record - the edge record
|
|
2226
|
+
* @param element - the element name
|
|
2227
|
+
* @returns the attributes
|
|
2228
|
+
*/
|
|
2229
|
+
function graphologyEdgeAttributes(ctx, record, element) {
|
|
2230
|
+
const { attributes } = record;
|
|
2231
|
+
if (attributes !== undefined && attributes !== null && !isJsonObject(attributes)) {
|
|
2232
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: attributes must be an object, found ${describe(attributes)}`, { element });
|
|
2233
|
+
}
|
|
2234
|
+
return isJsonObject(attributes) ? attributes : {};
|
|
2141
2235
|
}
|
|
2142
2236
|
/**
|
|
2143
2237
|
* Report the edges that contradict a graphology document's declared options (W_JSON_INCONSISTENT,
|
|
@@ -2212,21 +2306,7 @@ function importJgf(ctx, root) {
|
|
|
2212
2306
|
report.fail(JSON_ISSUE.SHAPE, `graph.nodes must be an object keyed by id or an array, found ${describe(nodesRaw)}`);
|
|
2213
2307
|
}
|
|
2214
2308
|
const nodeRecords = Array.isArray(nodesRaw) ? nodesRaw : Object.values(nodesRaw ?? {});
|
|
2215
|
-
|
|
2216
|
-
if (typeof graph.directed === "boolean") {
|
|
2217
|
-
({ directed } = graph);
|
|
2218
|
-
}
|
|
2219
|
-
else if (ctx.explicitDefaultDirected) {
|
|
2220
|
-
directed = ctx.options.defaultDirected;
|
|
2221
|
-
}
|
|
2222
|
-
else {
|
|
2223
|
-
if (graph.directed !== undefined && graph.directed !== null) {
|
|
2224
|
-
report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `graph.directed is ${describe(graph.directed)}, not a boolean`, { element: "graph.directed" });
|
|
2225
|
-
}
|
|
2226
|
-
// the spec default is true; a file whose every edge says directed: false is read as undirected
|
|
2227
|
-
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2228
|
-
directed = objects.length === 0 || !objects.every((edge) => edge.directed === false);
|
|
2229
|
-
}
|
|
2309
|
+
const directed = jgfDirected(ctx, graph, edges);
|
|
2230
2310
|
ctx.setHeader(directed);
|
|
2231
2311
|
ctx.writeGraphDict(graph.metadata, "graph.metadata");
|
|
2232
2312
|
const shape = {
|
|
@@ -2240,44 +2320,99 @@ function importJgf(ctx, root) {
|
|
|
2240
2320
|
const labelColumn = ctx.nodes.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(nodeRecords, "label"));
|
|
2241
2321
|
if (isJsonObject(nodesRaw)) {
|
|
2242
2322
|
for (const key of Object.keys(nodesRaw)) {
|
|
2243
|
-
|
|
2244
|
-
const record = nodesRaw[key];
|
|
2245
|
-
if (record !== null && !isJsonObject(record)) {
|
|
2246
|
-
ctx.badElement("node", element);
|
|
2247
|
-
continue;
|
|
2248
|
-
}
|
|
2249
|
-
const id = ctx.coerceId(key, element);
|
|
2250
|
-
if (id === null) {
|
|
2251
|
-
ctx.countSkipped("node");
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2254
|
-
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
2255
|
-
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`, { element });
|
|
2256
|
-
}
|
|
2257
|
-
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
2323
|
+
pushJgfKeyedNode(ctx, key, nodesRaw[key], labelColumn);
|
|
2258
2324
|
}
|
|
2259
2325
|
}
|
|
2260
2326
|
else {
|
|
2261
2327
|
for (let i = 0; i < nodeRecords.length; i++) {
|
|
2262
|
-
|
|
2263
|
-
const record = nodeRecords[i];
|
|
2264
|
-
if (!isJsonObject(record)) {
|
|
2265
|
-
ctx.badElement("node", element);
|
|
2266
|
-
continue;
|
|
2267
|
-
}
|
|
2268
|
-
const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
|
|
2269
|
-
if (id === null) {
|
|
2270
|
-
ctx.countSkipped("node");
|
|
2271
|
-
continue;
|
|
2272
|
-
}
|
|
2273
|
-
pushJgfNode(ctx, id, record, labelColumn, element);
|
|
2328
|
+
pushJgfListedNode(ctx, nodeRecords[i], labelColumn, `nodes[${i}]`);
|
|
2274
2329
|
}
|
|
2275
2330
|
}
|
|
2276
2331
|
throwIfAborted(ctx.options.signal);
|
|
2332
|
+
const writeJgfEdge = jgfEdgeWriter(ctx, edges, hyperedges);
|
|
2333
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2334
|
+
readJgfEdge(ctx, edges[i], `edges[${i}]`, directed, writeJgfEdge);
|
|
2335
|
+
}
|
|
2336
|
+
importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
|
|
2337
|
+
ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* The header direction of a JGF graph: `directed`, else the defaultDirected option when given,
|
|
2341
|
+
* else the spec default true unless every edge says directed: false.
|
|
2342
|
+
* @param ctx - the context
|
|
2343
|
+
* @param graph - the graph object
|
|
2344
|
+
* @param edges - the edge records
|
|
2345
|
+
* @returns whether the header is directed
|
|
2346
|
+
*/
|
|
2347
|
+
function jgfDirected(ctx, graph, edges) {
|
|
2348
|
+
if (typeof graph.directed === "boolean") {
|
|
2349
|
+
return graph.directed;
|
|
2350
|
+
}
|
|
2351
|
+
if (ctx.explicitDefaultDirected) {
|
|
2352
|
+
return ctx.options.defaultDirected;
|
|
2353
|
+
}
|
|
2354
|
+
if (graph.directed !== undefined && graph.directed !== null) {
|
|
2355
|
+
ctx.report.warning("validation-error", JSON_ISSUE.BAD_FLAG, `graph.directed is ${describe(graph.directed)}, not a boolean`, { element: "graph.directed" });
|
|
2356
|
+
}
|
|
2357
|
+
// the spec default is true; a file whose every edge says directed: false is read as undirected
|
|
2358
|
+
const objects = edges.filter((edge) => isJsonObject(edge));
|
|
2359
|
+
return objects.length === 0 || !objects.every((edge) => edge.directed === false);
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Push one node of a JGF v2 `nodes` object, keyed by its id (a null record is a node with nothing).
|
|
2363
|
+
* @param ctx - the context
|
|
2364
|
+
* @param key - the node's key
|
|
2365
|
+
* @param record - the node record
|
|
2366
|
+
* @param labelColumn - the label column
|
|
2367
|
+
*/
|
|
2368
|
+
function pushJgfKeyedNode(ctx, key, record, labelColumn) {
|
|
2369
|
+
const element = `nodes[${JSON.stringify(key)}]`;
|
|
2370
|
+
if (record !== null && !isJsonObject(record)) {
|
|
2371
|
+
ctx.badElement("node", element);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const id = ctx.coerceId(key, element);
|
|
2375
|
+
if (id === null) {
|
|
2376
|
+
ctx.countSkipped("node");
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
2380
|
+
ctx.report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element}: the inner id ${describe(record.id)} differs from the key; the key is the id, the inner one is kept as id${SUFFIX.element}`, { element });
|
|
2381
|
+
}
|
|
2382
|
+
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Push one node of a JGF v1 `nodes` array, with its `id`.
|
|
2386
|
+
* @param ctx - the context
|
|
2387
|
+
* @param record - the node record
|
|
2388
|
+
* @param labelColumn - the label column
|
|
2389
|
+
* @param element - the element name
|
|
2390
|
+
*/
|
|
2391
|
+
function pushJgfListedNode(ctx, record, labelColumn, element) {
|
|
2392
|
+
if (!isJsonObject(record)) {
|
|
2393
|
+
ctx.badElement("node", element);
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
|
|
2397
|
+
if (id === null) {
|
|
2398
|
+
ctx.countSkipped("node");
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
pushJgfNode(ctx, id, record, labelColumn, element);
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* The writer of JGF edges: declares the edge id, relation and label columns the edges and
|
|
2405
|
+
* hyperedges need, and returns the function that pushes one edge into them.
|
|
2406
|
+
* @param ctx - the context
|
|
2407
|
+
* @param edges - the edge records
|
|
2408
|
+
* @param hyperedges - the hyperedge records
|
|
2409
|
+
* @returns the edge writer
|
|
2410
|
+
*/
|
|
2411
|
+
function jgfEdgeWriter(ctx, edges, hyperedges) {
|
|
2277
2412
|
const ids = ctx.declareEdgeIds([...edges, ...hyperedges], (edge) => edge.id, "id", false);
|
|
2278
2413
|
const relationColumn = ctx.edges.declareIf({ name: "relation", dtype: "string", role: "kind", nullable: true }, anyHas(edges, "relation") || anyHas(hyperedges, "relation"));
|
|
2279
2414
|
const edgeLabelColumn = ctx.edges.declareIf({ name: "label", dtype: "string", role: "label", nullable: true }, anyHas(edges, "label") || anyHas(hyperedges, "label"));
|
|
2280
|
-
|
|
2415
|
+
return (record, u, v, kind, element, structural = JGF_EDGE_KEYS) => {
|
|
2281
2416
|
const { metadata } = record;
|
|
2282
2417
|
const dict = isJsonObject(metadata) ? metadata : {};
|
|
2283
2418
|
const idValue = ctx.edgeIdValue(ids, record.id);
|
|
@@ -2286,39 +2421,43 @@ function importJgf(ctx, root) {
|
|
|
2286
2421
|
ctx.writeStringField(ctx.edges, relationColumn, edge, record.relation, "relation", element);
|
|
2287
2422
|
ctx.writeStringField(ctx.edges, edgeLabelColumn, edge, record.label, "label", element);
|
|
2288
2423
|
if (metadata !== undefined && metadata !== null && !isJsonObject(metadata)) {
|
|
2289
|
-
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
2424
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
2290
2425
|
element,
|
|
2291
2426
|
});
|
|
2292
2427
|
}
|
|
2293
2428
|
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
|
|
2294
2429
|
};
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
ctx.
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Read one JGF edge record: its ends and its own `directed` flag (the header's by default).
|
|
2433
|
+
* @param ctx - the context
|
|
2434
|
+
* @param record - the edge record
|
|
2435
|
+
* @param element - the element name
|
|
2436
|
+
* @param directed - the header direction
|
|
2437
|
+
* @param writeJgfEdge - the edge writer
|
|
2438
|
+
*/
|
|
2439
|
+
function readJgfEdge(ctx, record, element, directed, writeJgfEdge) {
|
|
2440
|
+
if (!isJsonObject(record)) {
|
|
2441
|
+
ctx.badElement("edge", element);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2445
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
try {
|
|
2449
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
2450
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
2451
|
+
if (u === null || v === null) {
|
|
2452
|
+
ctx.countSkipped("edge");
|
|
2453
|
+
return;
|
|
2318
2454
|
}
|
|
2455
|
+
const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, ctx.report);
|
|
2456
|
+
writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
|
|
2457
|
+
}
|
|
2458
|
+
catch (err) {
|
|
2459
|
+
ctx.skip(err, "edge", element);
|
|
2319
2460
|
}
|
|
2320
|
-
importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
|
|
2321
|
-
ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
|
|
2322
2461
|
}
|
|
2323
2462
|
/**
|
|
2324
2463
|
* The graph object of a JGF document: `graph`, or the graph of `graphs` that graphIndex /
|
|
@@ -2626,111 +2765,150 @@ function distinctMembers(ctx, members, element) {
|
|
|
2626
2765
|
* @param root - the document
|
|
2627
2766
|
*/
|
|
2628
2767
|
function importCytoscape(ctx, root) {
|
|
2629
|
-
const {
|
|
2630
|
-
let elements;
|
|
2631
|
-
let extra;
|
|
2632
|
-
if (Array.isArray(root)) {
|
|
2633
|
-
elements = root;
|
|
2634
|
-
}
|
|
2635
|
-
else if (isJsonObject(root)) {
|
|
2636
|
-
({ elements } = root);
|
|
2637
|
-
ctx.writeGraphDict(root.data, "data");
|
|
2638
|
-
const rest = {};
|
|
2639
|
-
for (const key of Object.keys(root)) {
|
|
2640
|
-
if (key !== "elements" && key !== "data") {
|
|
2641
|
-
rest[key] = root[key];
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
if (Object.keys(rest).length > 0) {
|
|
2645
|
-
extra = rest;
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
else {
|
|
2649
|
-
report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
|
|
2650
|
-
}
|
|
2768
|
+
const { elements, extra } = cytoscapeRoot(ctx, root);
|
|
2651
2769
|
const { nodes, edges } = cytoscapeSections(ctx, elements);
|
|
2652
2770
|
ctx.setHeader(ctx.defaultDirected("cytoscape"));
|
|
2653
2771
|
ctx.reportNodeIdFrom("cytoscape", "data.id");
|
|
2654
2772
|
ctx.sink.reserve(nodes.length, edges.length);
|
|
2655
2773
|
const dataHas = (items, key) => items.some((item) => isJsonObject(item) && isJsonObject(item.data) && item.data[key] !== undefined);
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2774
|
+
const reading = {
|
|
2775
|
+
positionColumn: ctx.nodes.declareIf({
|
|
2776
|
+
name: POSITION_COLUMN,
|
|
2777
|
+
dtype: "f64",
|
|
2778
|
+
components: 3,
|
|
2779
|
+
role: "position",
|
|
2780
|
+
mutable: true,
|
|
2781
|
+
nullable: true,
|
|
2782
|
+
// 3D layouts written by Cytoscape-compatible tools carry a z
|
|
2783
|
+
extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
|
|
2784
|
+
origin: { format: "json", namespace: "cytoscape" },
|
|
2785
|
+
}, anyHas(nodes, "position")),
|
|
2786
|
+
classesColumn: ctx.nodes.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(nodes, "classes")),
|
|
2787
|
+
parents: [],
|
|
2788
|
+
point: [0, 0, 0],
|
|
2789
|
+
};
|
|
2668
2790
|
const parentColumn = ctx.nodes.declareIf({ name: PARENT_COLUMN, dtype: "u32", role: "parent", refersTo: "node", nullable: true }, dataHas(nodes, "parent"));
|
|
2669
2791
|
const edgeClassesColumn = ctx.edges.declareIf({ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true }, anyHas(edges, "classes"));
|
|
2670
|
-
const parents = [];
|
|
2671
|
-
const point = [0, 0, 0];
|
|
2672
2792
|
for (let i = 0; i < nodes.length; i++) {
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2793
|
+
readCytoscapeNode(ctx, reading, nodes[i], `nodes[${i}]`);
|
|
2794
|
+
}
|
|
2795
|
+
resolveCytoscapeParents(ctx, reading.parents, parentColumn);
|
|
2796
|
+
throwIfAborted(ctx.options.signal);
|
|
2797
|
+
importCytoscapeEdges(ctx, edges, edgeClassesColumn);
|
|
2798
|
+
ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* The elements of a Cytoscape document (the document itself when it is an array) and, for an
|
|
2802
|
+
* object, its graph `data` written and its other top-level keys kept.
|
|
2803
|
+
* @param ctx - the context
|
|
2804
|
+
* @param root - the document
|
|
2805
|
+
* @returns the elements and the other top-level keys, if any
|
|
2806
|
+
*/
|
|
2807
|
+
function cytoscapeRoot(ctx, root) {
|
|
2808
|
+
if (Array.isArray(root)) {
|
|
2809
|
+
return { elements: root };
|
|
2810
|
+
}
|
|
2811
|
+
if (!isJsonObject(root)) {
|
|
2812
|
+
ctx.report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
|
|
2813
|
+
}
|
|
2814
|
+
ctx.writeGraphDict(root.data, "data");
|
|
2815
|
+
const rest = {};
|
|
2816
|
+
for (const key of Object.keys(root)) {
|
|
2817
|
+
if (key !== "elements" && key !== "data") {
|
|
2818
|
+
rest[key] = root[key];
|
|
2678
2819
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2820
|
+
}
|
|
2821
|
+
return Object.keys(rest).length > 0 ? { elements: root.elements, extra: rest } : { elements: root.elements };
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Read one Cytoscape node element: `data.id` the id, the other `data` keys attributes (`parent`
|
|
2825
|
+
* kept for later), `position`, `classes` and the element-level keys.
|
|
2826
|
+
* @param ctx - the context
|
|
2827
|
+
* @param reading - the read's state
|
|
2828
|
+
* @param record - the element
|
|
2829
|
+
* @param element - the element name
|
|
2830
|
+
*/
|
|
2831
|
+
function readCytoscapeNode(ctx, reading, record, element) {
|
|
2832
|
+
const { report } = ctx;
|
|
2833
|
+
if (!isJsonObject(record) || !isJsonObject(record.data)) {
|
|
2834
|
+
ctx.badElement("node", element, "an element with a data object");
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
const { data } = record;
|
|
2838
|
+
const id = ctx.coerceId(hasKey(data, "id") ? data.id : undefined, element);
|
|
2839
|
+
if (id === null) {
|
|
2840
|
+
ctx.countSkipped("node");
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const index = ctx.pushNode(id, element);
|
|
2844
|
+
if (index < 0) {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (present(data, "source") && present(data, "target")) {
|
|
2848
|
+
report.warning("validation-error", JSON_ISSUE.INCONSISTENT, `${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`, { element });
|
|
2849
|
+
}
|
|
2850
|
+
try {
|
|
2851
|
+
writeCytoscapeData(ctx, reading, index, data, element);
|
|
2852
|
+
writeCytoscapePosition(ctx, reading, index, record.position, element);
|
|
2853
|
+
writeClasses(ctx, ctx.nodes, reading.classesColumn, index, record.classes, element);
|
|
2854
|
+
writeElementKeys(ctx.nodes, index, record, element);
|
|
2855
|
+
}
|
|
2856
|
+
catch (err) {
|
|
2857
|
+
report.recordError(err, { element: String(id) });
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Write a Cytoscape node's `data` keys as attributes; `parent` is kept as a link to resolve later.
|
|
2862
|
+
* @param ctx - the context
|
|
2863
|
+
* @param reading - the read's state
|
|
2864
|
+
* @param index - the node index
|
|
2865
|
+
* @param data - the element's data object
|
|
2866
|
+
* @param element - the element name
|
|
2867
|
+
*/
|
|
2868
|
+
function writeCytoscapeData(ctx, reading, index, data, element) {
|
|
2869
|
+
for (const key of Object.keys(data)) {
|
|
2870
|
+
if (key === "id") {
|
|
2683
2871
|
continue;
|
|
2684
2872
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2873
|
+
if (key !== "parent") {
|
|
2874
|
+
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
2687
2875
|
continue;
|
|
2688
2876
|
}
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
try {
|
|
2693
|
-
for (const key of Object.keys(data)) {
|
|
2694
|
-
if (key === "id") {
|
|
2695
|
-
continue;
|
|
2696
|
-
}
|
|
2697
|
-
if (key === "parent") {
|
|
2698
|
-
const raw = data.parent;
|
|
2699
|
-
if (raw !== undefined && raw !== null) {
|
|
2700
|
-
const parent = ctx.coerceId(raw, `${element}.data.parent`);
|
|
2701
|
-
if (parent !== null) {
|
|
2702
|
-
parents.push({ index, parent, element });
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
continue;
|
|
2706
|
-
}
|
|
2707
|
-
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
2708
|
-
}
|
|
2709
|
-
const { position } = record;
|
|
2710
|
-
if (position !== undefined && position !== null) {
|
|
2711
|
-
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
2712
|
-
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
2713
|
-
point[0] = position.x;
|
|
2714
|
-
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
2715
|
-
point[1] = flipY(position.y);
|
|
2716
|
-
point[2] = z;
|
|
2717
|
-
ctx.nodes.set(positionColumn, index, point);
|
|
2718
|
-
}
|
|
2719
|
-
else {
|
|
2720
|
-
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite numbers`, { element });
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
|
|
2724
|
-
writeElementKeys(ctx.nodes, index, record, element);
|
|
2877
|
+
const raw = data.parent;
|
|
2878
|
+
if (raw === undefined || raw === null) {
|
|
2879
|
+
continue;
|
|
2725
2880
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2881
|
+
const parent = ctx.coerceId(raw, `${element}.data.parent`);
|
|
2882
|
+
if (parent !== null) {
|
|
2883
|
+
reading.parents.push({ index, parent, element });
|
|
2728
2884
|
}
|
|
2729
2885
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Write a Cytoscape node's `position` ({ x, y, z? }, y flipped to grow upward); any other value is
|
|
2889
|
+
* reported.
|
|
2890
|
+
* @param ctx - the context
|
|
2891
|
+
* @param reading - the read's state
|
|
2892
|
+
* @param index - the node index
|
|
2893
|
+
* @param position - the element's position
|
|
2894
|
+
* @param element - the element name
|
|
2895
|
+
*/
|
|
2896
|
+
function writeCytoscapePosition(ctx, reading, index, position, element) {
|
|
2897
|
+
if (position === undefined || position === null) {
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
2901
|
+
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
2902
|
+
const { point } = reading;
|
|
2903
|
+
point[0] = position.x;
|
|
2904
|
+
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
2905
|
+
point[1] = flipY(position.y);
|
|
2906
|
+
point[2] = z;
|
|
2907
|
+
ctx.nodes.set(reading.positionColumn, index, point);
|
|
2908
|
+
}
|
|
2909
|
+
else {
|
|
2910
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: position must be an object with numeric x and y (and z) that are finite numbers`, { element });
|
|
2911
|
+
}
|
|
2734
2912
|
}
|
|
2735
2913
|
/**
|
|
2736
2914
|
* Set the parent of each Cytoscape node once every node is known (a parent may come later): an
|
|
@@ -2819,28 +2997,43 @@ function importCytoscapeEdges(ctx, edges, edgeClassesColumn) {
|
|
|
2819
2997
|
ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
|
|
2820
2998
|
continue;
|
|
2821
2999
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3000
|
+
importCytoscapeEdge(ctx, { ids, kind, edgeClassesColumn }, record, data, element);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Read one Cytoscape edge element whose data object holds both endpoints.
|
|
3005
|
+
* @param ctx - the context
|
|
3006
|
+
* @param shared - the edge ids, the uniform kind and the classes column of the whole section
|
|
3007
|
+
* @param shared.ids - the declared edge ids
|
|
3008
|
+
* @param shared.kind - the uniform edge kind
|
|
3009
|
+
* @param shared.edgeClassesColumn - the edge classes column, or INVALID_INDEX
|
|
3010
|
+
* @param record - the edge element
|
|
3011
|
+
* @param data - its data object
|
|
3012
|
+
* @param element - the element's name in messages
|
|
3013
|
+
*/
|
|
3014
|
+
function importCytoscapeEdge(ctx, shared, record, data, element) {
|
|
3015
|
+
const { ids, kind, edgeClassesColumn } = shared;
|
|
3016
|
+
try {
|
|
3017
|
+
const u = ctx.coerceId(data.source, `${element}.data.source`);
|
|
3018
|
+
const v = ctx.coerceId(data.target, `${element}.data.target`);
|
|
3019
|
+
if (u === null || v === null) {
|
|
3020
|
+
ctx.countSkipped("edge");
|
|
3021
|
+
return;
|
|
2840
3022
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
3023
|
+
const idValue = ctx.edgeIdValue(ids, data.id);
|
|
3024
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
|
|
3025
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
3026
|
+
const { weightFrom } = ctx.options;
|
|
3027
|
+
for (const key of Object.keys(data)) {
|
|
3028
|
+
if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
|
|
3029
|
+
ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
|
|
3030
|
+
}
|
|
2843
3031
|
}
|
|
3032
|
+
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
3033
|
+
writeElementKeys(ctx.edges, edge, record, element);
|
|
3034
|
+
}
|
|
3035
|
+
catch (err) {
|
|
3036
|
+
ctx.skip(err, "edge", element);
|
|
2844
3037
|
}
|
|
2845
3038
|
}
|
|
2846
3039
|
/**
|