@graphty/graph-io 0.3.40 → 0.3.42
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/README.md +2 -0
- package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
- package/dist/chunks/children-CA4coBi0.js.map +1 -0
- package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
- package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
- package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
- package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
- package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
- package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
- package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
- package/dist/chunks/text-DHOJXnED.js.map +1 -0
- package/dist/csv.js +323 -241
- 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 +27 -27
- 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/children.d.ts.map +1 -1
- package/dist/src/children.js +50 -27
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +86 -42
- 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 +358 -184
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/lists.js +30 -15
- package/dist/src/common/lists.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 +288 -146
- 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 +129 -80
- 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 +78 -36
- 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/dot/tokenizer.d.ts +39 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +112 -66
- package/dist/src/formats/dot/tokenizer.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/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +54 -20
- package/dist/src/formats/json/dialect.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/children.ts +54 -27
- package/src/common/attributes.ts +118 -49
- package/src/common/input.ts +439 -197
- package/src/common/lists.ts +29 -15
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +377 -175
- package/src/formats/csv/importer.ts +155 -84
- package/src/formats/csv/records.ts +90 -35
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/dot/tokenizer.ts +118 -68
- 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/dialect.ts +61 -22
- 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/children-CL3Cy0ez.js.map +0 -1
- 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/importer-DcKB97kW.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
|
@@ -2310,18 +2310,7 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2310
2310
|
ctx.setHeader(directed);
|
|
2311
2311
|
// adjacency_data writes the graph dict as a list of [key, value] pairs
|
|
2312
2312
|
ctx.writeGraphDict(isPairList(root.graph) ? Object.fromEntries(root.graph) : root.graph, "graph");
|
|
2313
|
-
|
|
2314
|
-
if (key !== "nodes" && key !== "adjacency" && key !== "directed" && key !== "multigraph" && key !== "graph") {
|
|
2315
|
-
report.warning(
|
|
2316
|
-
"unsupported",
|
|
2317
|
-
JSON_ISSUE.UNREAD_KEY,
|
|
2318
|
-
`top-level key ${key} (${describe(root[key])}) is not read; dropped`,
|
|
2319
|
-
{
|
|
2320
|
-
element: key,
|
|
2321
|
-
},
|
|
2322
|
-
);
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2313
|
+
reportUnreadAdjacencyKeys(ctx, root);
|
|
2325
2314
|
const lists = adjacency ?? [];
|
|
2326
2315
|
ctx.sink.reserve(
|
|
2327
2316
|
nodes.length,
|
|
@@ -2329,91 +2318,179 @@ function importAdjacency(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2329
2318
|
);
|
|
2330
2319
|
const idKey = ctx.json.nodeIdKey ?? "id";
|
|
2331
2320
|
ctx.reportNodeIdFrom("adjacency", `the ${JSON.stringify(idKey)} key`);
|
|
2332
|
-
const owners
|
|
2333
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
2334
|
-
const element = `nodes[${i}]`;
|
|
2335
|
-
const record = nodes[i];
|
|
2336
|
-
let pushed: NodeId | null = null;
|
|
2337
|
-
if (!isJsonObject(record)) {
|
|
2338
|
-
ctx.badElement("node", element);
|
|
2339
|
-
} else {
|
|
2340
|
-
const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
|
|
2341
|
-
if (id === null) {
|
|
2342
|
-
ctx.countSkipped("node");
|
|
2343
|
-
} else {
|
|
2344
|
-
const index = ctx.pushNode(id, element);
|
|
2345
|
-
if (index >= 0) {
|
|
2346
|
-
pushed = id;
|
|
2347
|
-
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
owners.push(pushed);
|
|
2352
|
-
}
|
|
2321
|
+
const owners = nodes.map((record, i) => pushAdjacencyNode(ctx, record, `nodes[${i}]`, idKey));
|
|
2353
2322
|
throwIfAborted(ctx.options.signal);
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2323
|
+
const reading: AdjacencyReading = {
|
|
2324
|
+
owners,
|
|
2325
|
+
nodeCount: nodes.length,
|
|
2326
|
+
directed,
|
|
2327
|
+
kind: ctx.uniformKind(),
|
|
2328
|
+
idKey,
|
|
2329
|
+
// undirected: entries read once whose mirror is still to come, by pair and key, with their attributes
|
|
2330
|
+
pending: new Map<string, string[]>(),
|
|
2331
|
+
disagreeing: 0,
|
|
2332
|
+
};
|
|
2359
2333
|
for (let i = 0; i < lists.length; i++) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2334
|
+
readAdjacencyList(ctx, reading, i, lists[i]);
|
|
2335
|
+
}
|
|
2336
|
+
reportAdjacencyShape(
|
|
2337
|
+
ctx,
|
|
2338
|
+
nodes.length,
|
|
2339
|
+
adjacency === null ? null : lists.length,
|
|
2340
|
+
reading.disagreeing,
|
|
2341
|
+
reading.pending,
|
|
2342
|
+
);
|
|
2343
|
+
ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/** The top-level keys of an adjacency_data document that are read. */
|
|
2347
|
+
const ADJACENCY_DOCUMENT_KEYS: ReadonlySet<string> = new Set(["nodes", "adjacency", "directed", "multigraph", "graph"]);
|
|
2348
|
+
|
|
2349
|
+
/** The state of an adjacency_data read, shared by its lists. */
|
|
2350
|
+
interface AdjacencyReading {
|
|
2351
|
+
/** The id of each node record, or null when it was skipped. */
|
|
2352
|
+
readonly owners: readonly (NodeId | null)[];
|
|
2353
|
+
/** The number of node records. */
|
|
2354
|
+
readonly nodeCount: number;
|
|
2355
|
+
readonly directed: boolean;
|
|
2356
|
+
readonly kind: EdgeKind;
|
|
2357
|
+
/** The entries' id key. */
|
|
2358
|
+
readonly idKey: string;
|
|
2359
|
+
/** Undirected entries awaiting their mirror (mirroredEntry()). */
|
|
2360
|
+
readonly pending: Map<string, string[]>;
|
|
2361
|
+
/** The mirrors whose attributes differ. */
|
|
2362
|
+
disagreeing: number;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* Report the top-level keys of an adjacency_data document that are not read.
|
|
2367
|
+
* @param ctx - the context
|
|
2368
|
+
* @param root - the document
|
|
2369
|
+
*/
|
|
2370
|
+
function reportUnreadAdjacencyKeys(ctx: ImportContext, root: JsonRecord): void {
|
|
2371
|
+
for (const key of Object.keys(root)) {
|
|
2372
|
+
if (!ADJACENCY_DOCUMENT_KEYS.has(key)) {
|
|
2373
|
+
ctx.report.warning(
|
|
2374
|
+
"unsupported",
|
|
2375
|
+
JSON_ISSUE.UNREAD_KEY,
|
|
2376
|
+
`top-level key ${key} (${describe(root[key])}) is not read; dropped`,
|
|
2373
2377
|
{
|
|
2374
|
-
element,
|
|
2378
|
+
element: key,
|
|
2375
2379
|
},
|
|
2376
2380
|
);
|
|
2377
|
-
report.counts.skippedEdges += list.length;
|
|
2378
|
-
continue;
|
|
2379
2381
|
}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Push one node record of an adjacency_data document.
|
|
2387
|
+
* @param ctx - the context
|
|
2388
|
+
* @param record - the node record
|
|
2389
|
+
* @param element - the element name
|
|
2390
|
+
* @param idKey - the key that holds the id
|
|
2391
|
+
* @returns the node's id, or null when it was skipped
|
|
2392
|
+
*/
|
|
2393
|
+
function pushAdjacencyNode(ctx: ImportContext, record: unknown, element: string, idKey: string): NodeId | null {
|
|
2394
|
+
if (!isJsonObject(record)) {
|
|
2395
|
+
ctx.badElement("node", element);
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
const id = ctx.coerceId(hasKey(record, idKey) ? record[idKey] : undefined, element);
|
|
2399
|
+
if (id === null) {
|
|
2400
|
+
ctx.countSkipped("node");
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
const index = ctx.pushNode(id, element);
|
|
2404
|
+
if (index < 0) {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
writeFlat(ctx.nodes, index, record, id, (key) => key !== idKey);
|
|
2408
|
+
return id;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Read one adjacency list: the edges of nodes[i]. A list whose owner was skipped is skipped whole.
|
|
2413
|
+
* @param ctx - the context
|
|
2414
|
+
* @param reading - the read's state
|
|
2415
|
+
* @param i - the list's position
|
|
2416
|
+
* @param list - the list
|
|
2417
|
+
*/
|
|
2418
|
+
function readAdjacencyList(ctx: ImportContext, reading: AdjacencyReading, i: number, list: unknown): void {
|
|
2419
|
+
const { report } = ctx;
|
|
2420
|
+
const element = `adjacency[${i}]`;
|
|
2421
|
+
if (!Array.isArray(list)) {
|
|
2422
|
+
report.error("validation-error", JSON_ISSUE.BAD_ELEMENT, `${element} is not an array`, { element });
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const source = i < reading.owners.length ? reading.owners[i] : null;
|
|
2426
|
+
if (source === null) {
|
|
2427
|
+
const why = i < reading.nodeCount ? `nodes[${i}] was skipped` : `there is no nodes[${i}]`;
|
|
2428
|
+
report.error(
|
|
2429
|
+
"missing-value",
|
|
2430
|
+
JSON_ISSUE.BAD_INDEX,
|
|
2431
|
+
`${element}: ${why}; its ${list.length} edge${plural(list.length)} ${agree(list.length, "is", "are")} skipped`,
|
|
2432
|
+
{
|
|
2433
|
+
element,
|
|
2434
|
+
},
|
|
2435
|
+
);
|
|
2436
|
+
report.counts.skippedEdges += list.length;
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
for (let j = 0; j < list.length; j++) {
|
|
2440
|
+
readAdjacencyEntry(ctx, reading, source, list[j], element, `${element}[${j}]`);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/**
|
|
2445
|
+
* Read one adjacency entry as an edge from the list's owner, unless it is the mirror of an
|
|
2446
|
+
* undirected edge already read.
|
|
2447
|
+
* @param ctx - the context
|
|
2448
|
+
* @param reading - the read's state
|
|
2449
|
+
* @param source - the list's owner
|
|
2450
|
+
* @param record - the entry
|
|
2451
|
+
* @param element - the list's element name (attribute issues name it)
|
|
2452
|
+
* @param entry - the entry's element name
|
|
2453
|
+
*/
|
|
2454
|
+
function readAdjacencyEntry(
|
|
2455
|
+
ctx: ImportContext,
|
|
2456
|
+
reading: AdjacencyReading,
|
|
2457
|
+
source: NodeId,
|
|
2458
|
+
record: unknown,
|
|
2459
|
+
element: string,
|
|
2460
|
+
entry: string,
|
|
2461
|
+
): void {
|
|
2462
|
+
const { idKey } = reading;
|
|
2463
|
+
if (!isJsonObject(record)) {
|
|
2464
|
+
ctx.badElement("edge", entry);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (!hasKey(record, idKey)) {
|
|
2468
|
+
ctx.missingEndpoint(entry, idKey);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
try {
|
|
2472
|
+
const target = ctx.coerceId(record[idKey], `${entry}.${idKey}`);
|
|
2473
|
+
if (target === null) {
|
|
2474
|
+
ctx.countSkipped("edge");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (!reading.directed) {
|
|
2478
|
+
const mirror = mirroredEntry(reading.pending, source, target, record, idKey);
|
|
2479
|
+
reading.disagreeing += mirror === "disagrees" ? 1 : 0;
|
|
2480
|
+
if (mirror !== "new") {
|
|
2481
|
+
return;
|
|
2390
2482
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
if (!directed) {
|
|
2398
|
-
const mirror = mirroredEntry(pending, source, target, record, idKey);
|
|
2399
|
-
disagreeing += mirror === "disagrees" ? 1 : 0;
|
|
2400
|
-
if (mirror !== "new") {
|
|
2401
|
-
continue;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
const edge = ctx.pushEdge(source, target, kind, ctx.weightOf(record), entry);
|
|
2405
|
-
for (const key of Object.keys(record)) {
|
|
2406
|
-
if (key !== idKey && key !== weightFrom) {
|
|
2407
|
-
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
} catch (err) {
|
|
2411
|
-
ctx.skip(err, "edge", entry);
|
|
2483
|
+
}
|
|
2484
|
+
const edge = ctx.pushEdge(source, target, reading.kind, ctx.weightOf(record), entry);
|
|
2485
|
+
const { weightFrom } = ctx.options;
|
|
2486
|
+
for (const key of Object.keys(record)) {
|
|
2487
|
+
if (key !== idKey && key !== weightFrom) {
|
|
2488
|
+
ctx.edges.write(edge, key, record[key], SUFFIX.data, element);
|
|
2412
2489
|
}
|
|
2413
2490
|
}
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
ctx.skip(err, "edge", entry);
|
|
2414
2493
|
}
|
|
2415
|
-
reportAdjacencyShape(ctx, nodes.length, adjacency === null ? null : lists.length, disagreeing, pending);
|
|
2416
|
-
ctx.setMeta({}, { declaredMultigraph: multigraph, ...ctx.weightOriginPatch() });
|
|
2417
2494
|
}
|
|
2418
2495
|
|
|
2419
2496
|
/**
|
|
@@ -2603,29 +2680,8 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2603
2680
|
element: "options",
|
|
2604
2681
|
});
|
|
2605
2682
|
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
({ type } = options);
|
|
2609
|
-
} else {
|
|
2610
|
-
if (options.type !== undefined && options.type !== null) {
|
|
2611
|
-
report.warning(
|
|
2612
|
-
"validation-error",
|
|
2613
|
-
JSON_ISSUE.BAD_FLAG,
|
|
2614
|
-
`options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`,
|
|
2615
|
-
{ element: "options.type" },
|
|
2616
|
-
);
|
|
2617
|
-
}
|
|
2618
|
-
type = "mixed";
|
|
2619
|
-
}
|
|
2620
|
-
let directed: boolean;
|
|
2621
|
-
if (type === "mixed") {
|
|
2622
|
-
const objects = edges.filter((edge) => isJsonObject(edge)) as JsonRecord[];
|
|
2623
|
-
const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
|
|
2624
|
-
directed = objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
|
|
2625
|
-
} else {
|
|
2626
|
-
directed = type === "directed";
|
|
2627
|
-
}
|
|
2628
|
-
ctx.setHeader(directed);
|
|
2683
|
+
const type = graphologyType(ctx, options);
|
|
2684
|
+
ctx.setHeader(graphologyDirected(ctx, type, edges));
|
|
2629
2685
|
const multi = hasKey(options, "multi") ? flagOf(options.multi, "options.multi", false, report) : null;
|
|
2630
2686
|
const allowSelfLoops = hasKey(options, "allowSelfLoops")
|
|
2631
2687
|
? flagOf(options.allowSelfLoops, "options.allowSelfLoops", true, report)
|
|
@@ -2635,80 +2691,184 @@ function importGraphology(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2635
2691
|
ctx.sink.reserve(nodes.length, edges.length);
|
|
2636
2692
|
|
|
2637
2693
|
for (let i = 0; i < nodes.length; i++) {
|
|
2638
|
-
|
|
2639
|
-
const record = nodes[i];
|
|
2640
|
-
if (!isJsonObject(record)) {
|
|
2641
|
-
ctx.badElement("node", element);
|
|
2642
|
-
continue;
|
|
2643
|
-
}
|
|
2644
|
-
const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
|
|
2645
|
-
if (id === null) {
|
|
2646
|
-
ctx.countSkipped("node");
|
|
2647
|
-
continue;
|
|
2648
|
-
}
|
|
2649
|
-
pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
|
|
2694
|
+
pushGraphologyNode(ctx, nodes[i], `nodes[${i}]`);
|
|
2650
2695
|
}
|
|
2651
2696
|
throwIfAborted(ctx.options.signal);
|
|
2652
2697
|
|
|
2653
|
-
const
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2698
|
+
const reading: GraphologyReading = {
|
|
2699
|
+
ids: ctx.declareEdgeIds(edges, (edge) => edge.key, "key", true),
|
|
2700
|
+
type,
|
|
2701
|
+
allowSelfLoops,
|
|
2702
|
+
// what the edges do against the declared options: graphology itself refuses such a document
|
|
2703
|
+
violations: { selfLoops: 0, parallels: 0, flags: 0 },
|
|
2704
|
+
pairs: multi === false ? new Set<string>() : null,
|
|
2705
|
+
};
|
|
2657
2706
|
for (let i = 0; i < edges.length; i++) {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2707
|
+
readGraphologyEdge(ctx, reading, edges[i], `edges[${i}]`);
|
|
2708
|
+
}
|
|
2709
|
+
reportGraphologyViolations(ctx, reading.violations, type);
|
|
2710
|
+
ctx.setMeta({ dialect: "graphology", allowSelfLoops }, { declaredMultigraph: multi, ...ctx.weightOriginPatch() });
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/** The graph types a graphology document declares in `options.type`. */
|
|
2714
|
+
type GraphologyType = "directed" | "undirected" | "mixed";
|
|
2715
|
+
|
|
2716
|
+
/** The state of a graphology read, shared by its edges. */
|
|
2717
|
+
interface GraphologyReading {
|
|
2718
|
+
/** The edge id column, from declareEdgeIds(). */
|
|
2719
|
+
readonly ids: EdgeIdColumn | null;
|
|
2720
|
+
readonly type: GraphologyType;
|
|
2721
|
+
/** options.allowSelfLoops, or undefined when absent. */
|
|
2722
|
+
readonly allowSelfLoops: boolean | undefined;
|
|
2723
|
+
/** The edges that contradict the declared options (reportGraphologyViolations()). */
|
|
2724
|
+
readonly violations: { selfLoops: number; parallels: number; flags: number };
|
|
2725
|
+
/** The pairs seen so far under multi false, or null. */
|
|
2726
|
+
readonly pairs: Set<string> | null;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* A graphology document's `options.type`; anything else is reported and read as "mixed".
|
|
2731
|
+
* @param ctx - the context
|
|
2732
|
+
* @param options - the options object
|
|
2733
|
+
* @returns the type
|
|
2734
|
+
*/
|
|
2735
|
+
function graphologyType(ctx: ImportContext, options: JsonRecord): GraphologyType {
|
|
2736
|
+
if (options.type === "directed" || options.type === "undirected" || options.type === "mixed") {
|
|
2737
|
+
return options.type;
|
|
2738
|
+
}
|
|
2739
|
+
if (options.type !== undefined && options.type !== null) {
|
|
2740
|
+
ctx.report.warning(
|
|
2741
|
+
"validation-error",
|
|
2742
|
+
JSON_ISSUE.BAD_FLAG,
|
|
2743
|
+
`options.type ${describe(options.type)} is not directed, undirected or mixed; mixed assumed`,
|
|
2744
|
+
{ element: "options.type" },
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
return "mixed";
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
/**
|
|
2751
|
+
* The header direction of a graphology document: its type, or for "mixed" directed unless every
|
|
2752
|
+
* edge object says undirected (the dialect default when there is none).
|
|
2753
|
+
* @param ctx - the context
|
|
2754
|
+
* @param type - options.type
|
|
2755
|
+
* @param edges - the edge records
|
|
2756
|
+
* @returns whether the header is directed
|
|
2757
|
+
*/
|
|
2758
|
+
function graphologyDirected(ctx: ImportContext, type: GraphologyType, edges: readonly unknown[]): boolean {
|
|
2759
|
+
if (type !== "mixed") {
|
|
2760
|
+
return type === "directed";
|
|
2761
|
+
}
|
|
2762
|
+
const objects = edges.filter((edge) => isJsonObject(edge)) as JsonRecord[];
|
|
2763
|
+
const undirectedEdges = objects.filter((edge) => edge.undirected === true).length;
|
|
2764
|
+
return objects.length === 0 ? ctx.defaultDirected("graphology") : undirectedEdges < objects.length;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/**
|
|
2768
|
+
* Push one graphology node record: `key` the id, `attributes` the columns.
|
|
2769
|
+
* @param ctx - the context
|
|
2770
|
+
* @param record - the node record
|
|
2771
|
+
* @param element - the element name
|
|
2772
|
+
*/
|
|
2773
|
+
function pushGraphologyNode(ctx: ImportContext, record: unknown, element: string): void {
|
|
2774
|
+
if (!isJsonObject(record)) {
|
|
2775
|
+
ctx.badElement("node", element);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
const id = ctx.coerceId(hasKey(record, "key") ? record.key : undefined, element);
|
|
2779
|
+
if (id === null) {
|
|
2780
|
+
ctx.countSkipped("node");
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
pushNestedNode(ctx, id, record, "attributes", GRAPHOLOGY_NODE_KEYS, element);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/**
|
|
2787
|
+
* Read one graphology edge record, counting what it does against the declared options.
|
|
2788
|
+
* @param ctx - the context
|
|
2789
|
+
* @param reading - the read's state
|
|
2790
|
+
* @param record - the edge record
|
|
2791
|
+
* @param element - the element name
|
|
2792
|
+
*/
|
|
2793
|
+
function readGraphologyEdge(ctx: ImportContext, reading: GraphologyReading, record: unknown, element: string): void {
|
|
2794
|
+
if (!isJsonObject(record)) {
|
|
2795
|
+
ctx.badElement("edge", element);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
2799
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
try {
|
|
2803
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
2804
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
2805
|
+
if (u === null || v === null) {
|
|
2806
|
+
ctx.countSkipped("edge");
|
|
2807
|
+
return;
|
|
2663
2808
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2809
|
+
const kind = graphologyEdgeKind(ctx, reading, record, element);
|
|
2810
|
+
const attributes = graphologyEdgeAttributes(ctx, record, element);
|
|
2811
|
+
const idValue = ctx.edgeIdValue(reading.ids, record.key);
|
|
2812
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2813
|
+
ctx.setEdgeId(reading.ids, edge, idValue);
|
|
2814
|
+
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2815
|
+
if (reading.allowSelfLoops === false && u === v) {
|
|
2816
|
+
reading.violations.selfLoops++;
|
|
2667
2817
|
}
|
|
2668
|
-
|
|
2669
|
-
const
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
ctx.countSkipped("edge");
|
|
2673
|
-
continue;
|
|
2674
|
-
}
|
|
2675
|
-
let kind: EdgeKind;
|
|
2676
|
-
if (type === "mixed") {
|
|
2677
|
-
kind = flagOf(record.undirected, `${element}.undirected`, false, report) ? "undirected" : "directed";
|
|
2678
|
-
} else {
|
|
2679
|
-
kind = type;
|
|
2680
|
-
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2681
|
-
violations.flags++;
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
const { attributes: rawAttributes } = record;
|
|
2685
|
-
if (rawAttributes !== undefined && rawAttributes !== null && !isJsonObject(rawAttributes)) {
|
|
2686
|
-
report.error(
|
|
2687
|
-
"validation-error",
|
|
2688
|
-
JSON_ISSUE.BAD_VALUE,
|
|
2689
|
-
`${element}: attributes must be an object, found ${describe(rawAttributes)}`,
|
|
2690
|
-
{ element },
|
|
2691
|
-
);
|
|
2692
|
-
}
|
|
2693
|
-
const attributes = isJsonObject(rawAttributes) ? rawAttributes : {};
|
|
2694
|
-
const idValue = ctx.edgeIdValue(ids, record.key);
|
|
2695
|
-
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(attributes), element);
|
|
2696
|
-
ctx.setEdgeId(ids, edge, idValue);
|
|
2697
|
-
ctx.writeNested(ctx.edges, edge, record, attributes, GRAPHOLOGY_EDGE_KEYS, ctx.options.weightFrom, element);
|
|
2698
|
-
if (allowSelfLoops === false && u === v) {
|
|
2699
|
-
violations.selfLoops++;
|
|
2700
|
-
}
|
|
2701
|
-
if (pairs !== null) {
|
|
2702
|
-
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2703
|
-
violations.parallels += pairs.has(pair) ? 1 : 0;
|
|
2704
|
-
pairs.add(pair);
|
|
2705
|
-
}
|
|
2706
|
-
} catch (err) {
|
|
2707
|
-
ctx.skip(err, "edge", element);
|
|
2818
|
+
if (reading.pairs !== null) {
|
|
2819
|
+
const pair = `${kind} ${pairKey(u, v, kind === "directed")}`;
|
|
2820
|
+
reading.violations.parallels += reading.pairs.has(pair) ? 1 : 0;
|
|
2821
|
+
reading.pairs.add(pair);
|
|
2708
2822
|
}
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
ctx.skip(err, "edge", element);
|
|
2709
2825
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* The direction of one graphology edge: its `undirected` flag under type "mixed", else the type
|
|
2830
|
+
* (a contrary flag is counted as a violation).
|
|
2831
|
+
* @param ctx - the context
|
|
2832
|
+
* @param reading - the read's state
|
|
2833
|
+
* @param record - the edge record
|
|
2834
|
+
* @param element - the element name
|
|
2835
|
+
* @returns the edge's kind
|
|
2836
|
+
*/
|
|
2837
|
+
function graphologyEdgeKind(
|
|
2838
|
+
ctx: ImportContext,
|
|
2839
|
+
reading: GraphologyReading,
|
|
2840
|
+
record: JsonRecord,
|
|
2841
|
+
element: string,
|
|
2842
|
+
): EdgeKind {
|
|
2843
|
+
const { type } = reading;
|
|
2844
|
+
if (type === "mixed") {
|
|
2845
|
+
return flagOf(record.undirected, `${element}.undirected`, false, ctx.report) ? "undirected" : "directed";
|
|
2846
|
+
}
|
|
2847
|
+
if (typeof record.undirected === "boolean" && record.undirected !== (type === "undirected")) {
|
|
2848
|
+
reading.violations.flags++;
|
|
2849
|
+
}
|
|
2850
|
+
return type;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/**
|
|
2854
|
+
* The `attributes` object of a graphology edge; any other non-null value is reported and read as
|
|
2855
|
+
* no attributes.
|
|
2856
|
+
* @param ctx - the context
|
|
2857
|
+
* @param record - the edge record
|
|
2858
|
+
* @param element - the element name
|
|
2859
|
+
* @returns the attributes
|
|
2860
|
+
*/
|
|
2861
|
+
function graphologyEdgeAttributes(ctx: ImportContext, record: JsonRecord, element: string): JsonRecord {
|
|
2862
|
+
const { attributes } = record;
|
|
2863
|
+
if (attributes !== undefined && attributes !== null && !isJsonObject(attributes)) {
|
|
2864
|
+
ctx.report.error(
|
|
2865
|
+
"validation-error",
|
|
2866
|
+
JSON_ISSUE.BAD_VALUE,
|
|
2867
|
+
`${element}: attributes must be an object, found ${describe(attributes)}`,
|
|
2868
|
+
{ element },
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
return isJsonObject(attributes) ? attributes : {};
|
|
2712
2872
|
}
|
|
2713
2873
|
|
|
2714
2874
|
/**
|
|
@@ -2812,24 +2972,7 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2812
2972
|
}
|
|
2813
2973
|
const nodeRecords: readonly unknown[] = Array.isArray(nodesRaw) ? nodesRaw : Object.values(nodesRaw ?? {});
|
|
2814
2974
|
|
|
2815
|
-
|
|
2816
|
-
if (typeof graph.directed === "boolean") {
|
|
2817
|
-
({ directed } = graph);
|
|
2818
|
-
} else if (ctx.explicitDefaultDirected) {
|
|
2819
|
-
directed = ctx.options.defaultDirected;
|
|
2820
|
-
} else {
|
|
2821
|
-
if (graph.directed !== undefined && graph.directed !== null) {
|
|
2822
|
-
report.warning(
|
|
2823
|
-
"validation-error",
|
|
2824
|
-
JSON_ISSUE.BAD_FLAG,
|
|
2825
|
-
`graph.directed is ${describe(graph.directed)}, not a boolean`,
|
|
2826
|
-
{ element: "graph.directed" },
|
|
2827
|
-
);
|
|
2828
|
-
}
|
|
2829
|
-
// the spec default is true; a file whose every edge says directed: false is read as undirected
|
|
2830
|
-
const objects = edges.filter((edge) => isJsonObject(edge)) as JsonRecord[];
|
|
2831
|
-
directed = objects.length === 0 || !objects.every((edge) => edge.directed === false);
|
|
2832
|
-
}
|
|
2975
|
+
const directed = jgfDirected(ctx, graph, edges);
|
|
2833
2976
|
ctx.setHeader(directed);
|
|
2834
2977
|
ctx.writeGraphDict(graph.metadata, "graph.metadata");
|
|
2835
2978
|
const shape: JsonShapeMeta = {
|
|
@@ -2842,50 +2985,125 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2842
2985
|
ctx.reportNodeIdFrom("jgf", "the node keys");
|
|
2843
2986
|
ctx.sink.reserve(nodeRecords.length, edges.length);
|
|
2844
2987
|
const labelColumn = ctx.nodes.declareIf(
|
|
2845
|
-
{ name: "label", dtype: "string", role: "label", nullable: true },
|
|
2846
|
-
anyHas(nodeRecords, "label"),
|
|
2847
|
-
);
|
|
2848
|
-
if (isJsonObject(nodesRaw)) {
|
|
2849
|
-
for (const key of Object.keys(nodesRaw)) {
|
|
2850
|
-
|
|
2851
|
-
const record = nodesRaw[key];
|
|
2852
|
-
if (record !== null && !isJsonObject(record)) {
|
|
2853
|
-
ctx.badElement("node", element);
|
|
2854
|
-
continue;
|
|
2855
|
-
}
|
|
2856
|
-
const id = ctx.coerceId(key, element);
|
|
2857
|
-
if (id === null) {
|
|
2858
|
-
ctx.countSkipped("node");
|
|
2859
|
-
continue;
|
|
2860
|
-
}
|
|
2861
|
-
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
2862
|
-
report.warning(
|
|
2863
|
-
"validation-error",
|
|
2864
|
-
JSON_ISSUE.INCONSISTENT,
|
|
2865
|
-
`${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}`,
|
|
2866
|
-
{ element },
|
|
2867
|
-
);
|
|
2868
|
-
}
|
|
2869
|
-
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
2988
|
+
{ name: "label", dtype: "string", role: "label", nullable: true },
|
|
2989
|
+
anyHas(nodeRecords, "label"),
|
|
2990
|
+
);
|
|
2991
|
+
if (isJsonObject(nodesRaw)) {
|
|
2992
|
+
for (const key of Object.keys(nodesRaw)) {
|
|
2993
|
+
pushJgfKeyedNode(ctx, key, nodesRaw[key], labelColumn);
|
|
2870
2994
|
}
|
|
2871
2995
|
} else {
|
|
2872
2996
|
for (let i = 0; i < nodeRecords.length; i++) {
|
|
2873
|
-
|
|
2874
|
-
const record = nodeRecords[i];
|
|
2875
|
-
if (!isJsonObject(record)) {
|
|
2876
|
-
ctx.badElement("node", element);
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2879
|
-
const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
|
|
2880
|
-
if (id === null) {
|
|
2881
|
-
ctx.countSkipped("node");
|
|
2882
|
-
continue;
|
|
2883
|
-
}
|
|
2884
|
-
pushJgfNode(ctx, id, record, labelColumn, element);
|
|
2997
|
+
pushJgfListedNode(ctx, nodeRecords[i], labelColumn, `nodes[${i}]`);
|
|
2885
2998
|
}
|
|
2886
2999
|
}
|
|
2887
3000
|
throwIfAborted(ctx.options.signal);
|
|
2888
3001
|
|
|
3002
|
+
const writeJgfEdge = jgfEdgeWriter(ctx, edges, hyperedges);
|
|
3003
|
+
for (let i = 0; i < edges.length; i++) {
|
|
3004
|
+
readJgfEdge(ctx, edges[i], `edges[${i}]`, directed, writeJgfEdge);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
|
|
3008
|
+
ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
/** Push one JGF edge (or one edge of a hyperedge) with its id, relation, label and metadata. */
|
|
3012
|
+
type JgfEdgeWriter = (
|
|
3013
|
+
record: JsonRecord,
|
|
3014
|
+
u: NodeId,
|
|
3015
|
+
v: NodeId,
|
|
3016
|
+
kind: EdgeKind,
|
|
3017
|
+
element: string,
|
|
3018
|
+
structural?: ReadonlySet<string>,
|
|
3019
|
+
) => void;
|
|
3020
|
+
|
|
3021
|
+
/**
|
|
3022
|
+
* The header direction of a JGF graph: `directed`, else the defaultDirected option when given,
|
|
3023
|
+
* else the spec default true unless every edge says directed: false.
|
|
3024
|
+
* @param ctx - the context
|
|
3025
|
+
* @param graph - the graph object
|
|
3026
|
+
* @param edges - the edge records
|
|
3027
|
+
* @returns whether the header is directed
|
|
3028
|
+
*/
|
|
3029
|
+
function jgfDirected(ctx: ImportContext, graph: JsonRecord, edges: readonly unknown[]): boolean {
|
|
3030
|
+
if (typeof graph.directed === "boolean") {
|
|
3031
|
+
return graph.directed;
|
|
3032
|
+
}
|
|
3033
|
+
if (ctx.explicitDefaultDirected) {
|
|
3034
|
+
return ctx.options.defaultDirected;
|
|
3035
|
+
}
|
|
3036
|
+
if (graph.directed !== undefined && graph.directed !== null) {
|
|
3037
|
+
ctx.report.warning(
|
|
3038
|
+
"validation-error",
|
|
3039
|
+
JSON_ISSUE.BAD_FLAG,
|
|
3040
|
+
`graph.directed is ${describe(graph.directed)}, not a boolean`,
|
|
3041
|
+
{ element: "graph.directed" },
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
// the spec default is true; a file whose every edge says directed: false is read as undirected
|
|
3045
|
+
const objects = edges.filter((edge) => isJsonObject(edge)) as JsonRecord[];
|
|
3046
|
+
return objects.length === 0 || !objects.every((edge) => edge.directed === false);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Push one node of a JGF v2 `nodes` object, keyed by its id (a null record is a node with nothing).
|
|
3051
|
+
* @param ctx - the context
|
|
3052
|
+
* @param key - the node's key
|
|
3053
|
+
* @param record - the node record
|
|
3054
|
+
* @param labelColumn - the label column
|
|
3055
|
+
*/
|
|
3056
|
+
function pushJgfKeyedNode(ctx: ImportContext, key: string, record: unknown, labelColumn: ColumnHandle): void {
|
|
3057
|
+
const element = `nodes[${JSON.stringify(key)}]`;
|
|
3058
|
+
if (record !== null && !isJsonObject(record)) {
|
|
3059
|
+
ctx.badElement("node", element);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
const id = ctx.coerceId(key, element);
|
|
3063
|
+
if (id === null) {
|
|
3064
|
+
ctx.countSkipped("node");
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
if (record !== null && hasKey(record, "id") && record.id !== key) {
|
|
3068
|
+
ctx.report.warning(
|
|
3069
|
+
"validation-error",
|
|
3070
|
+
JSON_ISSUE.INCONSISTENT,
|
|
3071
|
+
`${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}`,
|
|
3072
|
+
{ element },
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
pushJgfNode(ctx, id, record ?? {}, labelColumn, element);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
/**
|
|
3079
|
+
* Push one node of a JGF v1 `nodes` array, with its `id`.
|
|
3080
|
+
* @param ctx - the context
|
|
3081
|
+
* @param record - the node record
|
|
3082
|
+
* @param labelColumn - the label column
|
|
3083
|
+
* @param element - the element name
|
|
3084
|
+
*/
|
|
3085
|
+
function pushJgfListedNode(ctx: ImportContext, record: unknown, labelColumn: ColumnHandle, element: string): void {
|
|
3086
|
+
if (!isJsonObject(record)) {
|
|
3087
|
+
ctx.badElement("node", element);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const id = ctx.coerceId(hasKey(record, "id") ? record.id : undefined, element);
|
|
3091
|
+
if (id === null) {
|
|
3092
|
+
ctx.countSkipped("node");
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
pushJgfNode(ctx, id, record, labelColumn, element);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* The writer of JGF edges: declares the edge id, relation and label columns the edges and
|
|
3100
|
+
* hyperedges need, and returns the function that pushes one edge into them.
|
|
3101
|
+
* @param ctx - the context
|
|
3102
|
+
* @param edges - the edge records
|
|
3103
|
+
* @param hyperedges - the hyperedge records
|
|
3104
|
+
* @returns the edge writer
|
|
3105
|
+
*/
|
|
3106
|
+
function jgfEdgeWriter(ctx: ImportContext, edges: readonly unknown[], hyperedges: readonly unknown[]): JgfEdgeWriter {
|
|
2889
3107
|
const ids = ctx.declareEdgeIds([...edges, ...hyperedges], (edge) => edge.id, "id", false);
|
|
2890
3108
|
const relationColumn = ctx.edges.declareIf(
|
|
2891
3109
|
{ name: "relation", dtype: "string", role: "kind", nullable: true },
|
|
@@ -2895,14 +3113,7 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2895
3113
|
{ name: "label", dtype: "string", role: "label", nullable: true },
|
|
2896
3114
|
anyHas(edges, "label") || anyHas(hyperedges, "label"),
|
|
2897
3115
|
);
|
|
2898
|
-
|
|
2899
|
-
record: JsonRecord,
|
|
2900
|
-
u: NodeId,
|
|
2901
|
-
v: NodeId,
|
|
2902
|
-
kind: EdgeKind,
|
|
2903
|
-
element: string,
|
|
2904
|
-
structural: ReadonlySet<string> = JGF_EDGE_KEYS,
|
|
2905
|
-
): void => {
|
|
3116
|
+
return (record, u, v, kind, element, structural = JGF_EDGE_KEYS): void => {
|
|
2906
3117
|
const { metadata } = record;
|
|
2907
3118
|
const dict = isJsonObject(metadata) ? metadata : {};
|
|
2908
3119
|
const idValue = ctx.edgeIdValue(ids, record.id);
|
|
@@ -2911,40 +3122,49 @@ function importJgf(ctx: ImportContext, root: JsonRecord): void {
|
|
|
2911
3122
|
ctx.writeStringField(ctx.edges, relationColumn, edge, record.relation, "relation", element);
|
|
2912
3123
|
ctx.writeStringField(ctx.edges, edgeLabelColumn, edge, record.label, "label", element);
|
|
2913
3124
|
if (metadata !== undefined && metadata !== null && !isJsonObject(metadata)) {
|
|
2914
|
-
report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
3125
|
+
ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, `${element}: metadata must be an object`, {
|
|
2915
3126
|
element,
|
|
2916
3127
|
});
|
|
2917
3128
|
}
|
|
2918
3129
|
ctx.writeNested(ctx.edges, edge, record, dict, structural, ctx.options.weightFrom, element);
|
|
2919
3130
|
};
|
|
3131
|
+
}
|
|
2920
3132
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3133
|
+
/**
|
|
3134
|
+
* Read one JGF edge record: its ends and its own `directed` flag (the header's by default).
|
|
3135
|
+
* @param ctx - the context
|
|
3136
|
+
* @param record - the edge record
|
|
3137
|
+
* @param element - the element name
|
|
3138
|
+
* @param directed - the header direction
|
|
3139
|
+
* @param writeJgfEdge - the edge writer
|
|
3140
|
+
*/
|
|
3141
|
+
function readJgfEdge(
|
|
3142
|
+
ctx: ImportContext,
|
|
3143
|
+
record: unknown,
|
|
3144
|
+
element: string,
|
|
3145
|
+
directed: boolean,
|
|
3146
|
+
writeJgfEdge: JgfEdgeWriter,
|
|
3147
|
+
): void {
|
|
3148
|
+
if (!isJsonObject(record)) {
|
|
3149
|
+
ctx.badElement("edge", element);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (!present(record, "source") || !present(record, "target")) {
|
|
3153
|
+
ctx.missingEndpoint(element, present(record, "source") ? "target" : "source");
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
try {
|
|
3157
|
+
const u = ctx.coerceId(record.source, `${element}.source`);
|
|
3158
|
+
const v = ctx.coerceId(record.target, `${element}.target`);
|
|
3159
|
+
if (u === null || v === null) {
|
|
3160
|
+
ctx.countSkipped("edge");
|
|
3161
|
+
return;
|
|
2943
3162
|
}
|
|
3163
|
+
const edgeDirected = flagOf(record.directed, `${element}.directed`, directed, ctx.report);
|
|
3164
|
+
writeJgfEdge(record, u, v, edgeDirected ? "directed" : "undirected", element);
|
|
3165
|
+
} catch (err) {
|
|
3166
|
+
ctx.skip(err, "edge", element);
|
|
2944
3167
|
}
|
|
2945
|
-
|
|
2946
|
-
importHyperedges(ctx, hyperedges, directed, writeJgfEdge);
|
|
2947
|
-
ctx.setMeta(shape, { name: label, ...ctx.weightOriginPatch() });
|
|
2948
3168
|
}
|
|
2949
3169
|
|
|
2950
3170
|
/**
|
|
@@ -3339,26 +3559,7 @@ function distinctMembers(ctx: ImportContext, members: readonly NodeId[], element
|
|
|
3339
3559
|
* @param root - the document
|
|
3340
3560
|
*/
|
|
3341
3561
|
function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
3342
|
-
const {
|
|
3343
|
-
let elements: unknown;
|
|
3344
|
-
let extra: Record<string, unknown> | undefined;
|
|
3345
|
-
if (Array.isArray(root)) {
|
|
3346
|
-
elements = root;
|
|
3347
|
-
} else if (isJsonObject(root)) {
|
|
3348
|
-
({ elements } = root);
|
|
3349
|
-
ctx.writeGraphDict(root.data, "data");
|
|
3350
|
-
const rest: Record<string, unknown> = {};
|
|
3351
|
-
for (const key of Object.keys(root)) {
|
|
3352
|
-
if (key !== "elements" && key !== "data") {
|
|
3353
|
-
rest[key] = root[key];
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
if (Object.keys(rest).length > 0) {
|
|
3357
|
-
extra = rest;
|
|
3358
|
-
}
|
|
3359
|
-
} else {
|
|
3360
|
-
report.fail(JSON_ISSUE.SHAPE, `a Cytoscape document must be an object or an array, found ${describe(root)}`);
|
|
3361
|
-
}
|
|
3562
|
+
const { elements, extra } = cytoscapeRoot(ctx, root);
|
|
3362
3563
|
const { nodes, edges } = cytoscapeSections(ctx, elements);
|
|
3363
3564
|
ctx.setHeader(ctx.defaultDirected("cytoscape"));
|
|
3364
3565
|
ctx.reportNodeIdFrom("cytoscape", "data.id");
|
|
@@ -3366,24 +3567,28 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
3366
3567
|
|
|
3367
3568
|
const dataHas = (items: readonly unknown[], key: string): boolean =>
|
|
3368
3569
|
items.some((item) => isJsonObject(item) && isJsonObject(item.data) && item.data[key] !== undefined);
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3570
|
+
const reading: CytoscapeNodeReading = {
|
|
3571
|
+
positionColumn: ctx.nodes.declareIf(
|
|
3572
|
+
{
|
|
3573
|
+
name: POSITION_COLUMN,
|
|
3574
|
+
dtype: "f64",
|
|
3575
|
+
components: 3,
|
|
3576
|
+
role: "position",
|
|
3577
|
+
mutable: true,
|
|
3578
|
+
nullable: true,
|
|
3579
|
+
// 3D layouts written by Cytoscape-compatible tools carry a z
|
|
3580
|
+
extra: { sourceDims: nodes.some(hasPositionZ) ? 3 : 2, units: "file" },
|
|
3581
|
+
origin: { format: "json", namespace: "cytoscape" },
|
|
3582
|
+
},
|
|
3583
|
+
anyHas(nodes, "position"),
|
|
3584
|
+
),
|
|
3585
|
+
classesColumn: ctx.nodes.declareIf(
|
|
3586
|
+
{ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true },
|
|
3587
|
+
anyHas(nodes, "classes"),
|
|
3588
|
+
),
|
|
3589
|
+
parents: [],
|
|
3590
|
+
point: [0, 0, 0],
|
|
3591
|
+
};
|
|
3387
3592
|
const parentColumn = ctx.nodes.declareIf(
|
|
3388
3593
|
{ name: PARENT_COLUMN, dtype: "u32", role: "parent", refersTo: "node", nullable: true },
|
|
3389
3594
|
dataHas(nodes, "parent"),
|
|
@@ -3392,80 +3597,165 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
3392
3597
|
{ name: CLASSES_COLUMN, dtype: "list", itemDtype: "string", role: "classes", nullable: true },
|
|
3393
3598
|
anyHas(edges, "classes"),
|
|
3394
3599
|
);
|
|
3395
|
-
const parents: { index: number; parent: NodeId; element: string }[] = [];
|
|
3396
|
-
const point: [number, number, number] = [0, 0, 0];
|
|
3397
3600
|
|
|
3398
3601
|
for (let i = 0; i < nodes.length; i++) {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3602
|
+
readCytoscapeNode(ctx, reading, nodes[i], `nodes[${i}]`);
|
|
3603
|
+
}
|
|
3604
|
+
resolveCytoscapeParents(ctx, reading.parents, parentColumn);
|
|
3605
|
+
throwIfAborted(ctx.options.signal);
|
|
3606
|
+
|
|
3607
|
+
importCytoscapeEdges(ctx, edges, edgeClassesColumn);
|
|
3608
|
+
ctx.setMeta({ dialect: "cytoscape", cytoscape: extra }, ctx.weightOriginPatch());
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
/** The state of a read of Cytoscape node elements. */
|
|
3612
|
+
interface CytoscapeNodeReading {
|
|
3613
|
+
readonly positionColumn: ColumnHandle;
|
|
3614
|
+
readonly classesColumn: ColumnHandle;
|
|
3615
|
+
/** The parent links, resolved once every node is known. */
|
|
3616
|
+
readonly parents: { index: number; parent: NodeId; element: string }[];
|
|
3617
|
+
/** A scratch position. */
|
|
3618
|
+
readonly point: [number, number, number];
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
/**
|
|
3622
|
+
* The elements of a Cytoscape document (the document itself when it is an array) and, for an
|
|
3623
|
+
* object, its graph `data` written and its other top-level keys kept.
|
|
3624
|
+
* @param ctx - the context
|
|
3625
|
+
* @param root - the document
|
|
3626
|
+
* @returns the elements and the other top-level keys, if any
|
|
3627
|
+
*/
|
|
3628
|
+
function cytoscapeRoot(ctx: ImportContext, root: unknown): { elements: unknown; extra?: Record<string, unknown> } {
|
|
3629
|
+
if (Array.isArray(root)) {
|
|
3630
|
+
return { elements: root };
|
|
3631
|
+
}
|
|
3632
|
+
if (!isJsonObject(root)) {
|
|
3633
|
+
ctx.report.fail(
|
|
3634
|
+
JSON_ISSUE.SHAPE,
|
|
3635
|
+
`a Cytoscape document must be an object or an array, found ${describe(root)}`,
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
ctx.writeGraphDict(root.data, "data");
|
|
3639
|
+
const rest: Record<string, unknown> = {};
|
|
3640
|
+
for (const key of Object.keys(root)) {
|
|
3641
|
+
if (key !== "elements" && key !== "data") {
|
|
3642
|
+
rest[key] = root[key];
|
|
3404
3643
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3644
|
+
}
|
|
3645
|
+
return Object.keys(rest).length > 0 ? { elements: root.elements, extra: rest } : { elements: root.elements };
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
/**
|
|
3649
|
+
* Read one Cytoscape node element: `data.id` the id, the other `data` keys attributes (`parent`
|
|
3650
|
+
* kept for later), `position`, `classes` and the element-level keys.
|
|
3651
|
+
* @param ctx - the context
|
|
3652
|
+
* @param reading - the read's state
|
|
3653
|
+
* @param record - the element
|
|
3654
|
+
* @param element - the element name
|
|
3655
|
+
*/
|
|
3656
|
+
function readCytoscapeNode(ctx: ImportContext, reading: CytoscapeNodeReading, record: unknown, element: string): void {
|
|
3657
|
+
const { report } = ctx;
|
|
3658
|
+
if (!isJsonObject(record) || !isJsonObject(record.data)) {
|
|
3659
|
+
ctx.badElement("node", element, "an element with a data object");
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
const { data } = record;
|
|
3663
|
+
const id = ctx.coerceId(hasKey(data, "id") ? data.id : undefined, element);
|
|
3664
|
+
if (id === null) {
|
|
3665
|
+
ctx.countSkipped("node");
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
const index = ctx.pushNode(id, element);
|
|
3669
|
+
if (index < 0) {
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
if (present(data, "source") && present(data, "target")) {
|
|
3673
|
+
report.warning(
|
|
3674
|
+
"validation-error",
|
|
3675
|
+
JSON_ISSUE.INCONSISTENT,
|
|
3676
|
+
`${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`,
|
|
3677
|
+
{ element },
|
|
3678
|
+
);
|
|
3679
|
+
}
|
|
3680
|
+
try {
|
|
3681
|
+
writeCytoscapeData(ctx, reading, index, data, element);
|
|
3682
|
+
writeCytoscapePosition(ctx, reading, index, record.position, element);
|
|
3683
|
+
writeClasses(ctx, ctx.nodes, reading.classesColumn, index, record.classes, element);
|
|
3684
|
+
writeElementKeys(ctx.nodes, index, record, element);
|
|
3685
|
+
} catch (err) {
|
|
3686
|
+
report.recordError(err, { element: String(id) });
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
/**
|
|
3691
|
+
* Write a Cytoscape node's `data` keys as attributes; `parent` is kept as a link to resolve later.
|
|
3692
|
+
* @param ctx - the context
|
|
3693
|
+
* @param reading - the read's state
|
|
3694
|
+
* @param index - the node index
|
|
3695
|
+
* @param data - the element's data object
|
|
3696
|
+
* @param element - the element name
|
|
3697
|
+
*/
|
|
3698
|
+
function writeCytoscapeData(
|
|
3699
|
+
ctx: ImportContext,
|
|
3700
|
+
reading: CytoscapeNodeReading,
|
|
3701
|
+
index: number,
|
|
3702
|
+
data: JsonRecord,
|
|
3703
|
+
element: string,
|
|
3704
|
+
): void {
|
|
3705
|
+
for (const key of Object.keys(data)) {
|
|
3706
|
+
if (key === "id") {
|
|
3409
3707
|
continue;
|
|
3410
3708
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3709
|
+
if (key !== "parent") {
|
|
3710
|
+
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
3413
3711
|
continue;
|
|
3414
3712
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
JSON_ISSUE.INCONSISTENT,
|
|
3419
|
-
`${element} has data.source and data.target, the shape of an edge, but its section or group says node; read as a node`,
|
|
3420
|
-
{ element },
|
|
3421
|
-
);
|
|
3713
|
+
const raw = data.parent;
|
|
3714
|
+
if (raw === undefined || raw === null) {
|
|
3715
|
+
continue;
|
|
3422
3716
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
continue;
|
|
3427
|
-
}
|
|
3428
|
-
if (key === "parent") {
|
|
3429
|
-
const raw = data.parent;
|
|
3430
|
-
if (raw !== undefined && raw !== null) {
|
|
3431
|
-
const parent = ctx.coerceId(raw, `${element}.data.parent`);
|
|
3432
|
-
if (parent !== null) {
|
|
3433
|
-
parents.push({ index, parent, element });
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
continue;
|
|
3437
|
-
}
|
|
3438
|
-
ctx.nodes.write(index, key, data[key], SUFFIX.data, element);
|
|
3439
|
-
}
|
|
3440
|
-
const { position } = record;
|
|
3441
|
-
if (position !== undefined && position !== null) {
|
|
3442
|
-
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
3443
|
-
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
3444
|
-
point[0] = position.x;
|
|
3445
|
-
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
3446
|
-
point[1] = flipY(position.y);
|
|
3447
|
-
point[2] = z;
|
|
3448
|
-
ctx.nodes.set(positionColumn, index, point);
|
|
3449
|
-
} else {
|
|
3450
|
-
report.error(
|
|
3451
|
-
"validation-error",
|
|
3452
|
-
JSON_ISSUE.BAD_VALUE,
|
|
3453
|
-
`${element}: position must be an object with numeric x and y (and z) that are finite numbers`,
|
|
3454
|
-
{ element },
|
|
3455
|
-
);
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
writeClasses(ctx, ctx.nodes, classesColumn, index, record.classes, element);
|
|
3459
|
-
writeElementKeys(ctx.nodes, index, record, element);
|
|
3460
|
-
} catch (err) {
|
|
3461
|
-
report.recordError(err, { element: String(id) });
|
|
3717
|
+
const parent = ctx.coerceId(raw, `${element}.data.parent`);
|
|
3718
|
+
if (parent !== null) {
|
|
3719
|
+
reading.parents.push({ index, parent, element });
|
|
3462
3720
|
}
|
|
3463
3721
|
}
|
|
3464
|
-
|
|
3465
|
-
throwIfAborted(ctx.options.signal);
|
|
3722
|
+
}
|
|
3466
3723
|
|
|
3467
|
-
|
|
3468
|
-
|
|
3724
|
+
/**
|
|
3725
|
+
* Write a Cytoscape node's `position` ({ x, y, z? }, y flipped to grow upward); any other value is
|
|
3726
|
+
* reported.
|
|
3727
|
+
* @param ctx - the context
|
|
3728
|
+
* @param reading - the read's state
|
|
3729
|
+
* @param index - the node index
|
|
3730
|
+
* @param position - the element's position
|
|
3731
|
+
* @param element - the element name
|
|
3732
|
+
*/
|
|
3733
|
+
function writeCytoscapePosition(
|
|
3734
|
+
ctx: ImportContext,
|
|
3735
|
+
reading: CytoscapeNodeReading,
|
|
3736
|
+
index: number,
|
|
3737
|
+
position: unknown,
|
|
3738
|
+
element: string,
|
|
3739
|
+
): void {
|
|
3740
|
+
if (position === undefined || position === null) {
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const z = isJsonObject(position) ? (position.z ?? 0) : 0;
|
|
3744
|
+
if (isJsonObject(position) && isCoordinate(position.x) && isCoordinate(position.y) && isCoordinate(z)) {
|
|
3745
|
+
const { point } = reading;
|
|
3746
|
+
point[0] = position.x;
|
|
3747
|
+
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
3748
|
+
point[1] = flipY(position.y);
|
|
3749
|
+
point[2] = z;
|
|
3750
|
+
ctx.nodes.set(reading.positionColumn, index, point);
|
|
3751
|
+
} else {
|
|
3752
|
+
ctx.report.error(
|
|
3753
|
+
"validation-error",
|
|
3754
|
+
JSON_ISSUE.BAD_VALUE,
|
|
3755
|
+
`${element}: position must be an object with numeric x and y (and z) that are finite numbers`,
|
|
3756
|
+
{ element },
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3469
3759
|
}
|
|
3470
3760
|
|
|
3471
3761
|
/** One Cytoscape parent link: the parent's index and id, its document order and element name. */
|
|
@@ -3579,27 +3869,53 @@ function importCytoscapeEdges(ctx: ImportContext, edges: readonly unknown[], edg
|
|
|
3579
3869
|
ctx.missingEndpoint(element, `data.${present(data, "source") ? "target" : "source"}`);
|
|
3580
3870
|
continue;
|
|
3581
3871
|
}
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3872
|
+
importCytoscapeEdge(ctx, { ids, kind, edgeClassesColumn }, record, data, element);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
/**
|
|
3877
|
+
* Read one Cytoscape edge element whose data object holds both endpoints.
|
|
3878
|
+
* @param ctx - the context
|
|
3879
|
+
* @param shared - the edge ids, the uniform kind and the classes column of the whole section
|
|
3880
|
+
* @param shared.ids - the declared edge ids
|
|
3881
|
+
* @param shared.kind - the uniform edge kind
|
|
3882
|
+
* @param shared.edgeClassesColumn - the edge classes column, or INVALID_INDEX
|
|
3883
|
+
* @param record - the edge element
|
|
3884
|
+
* @param data - its data object
|
|
3885
|
+
* @param element - the element's name in messages
|
|
3886
|
+
*/
|
|
3887
|
+
function importCytoscapeEdge(
|
|
3888
|
+
ctx: ImportContext,
|
|
3889
|
+
shared: {
|
|
3890
|
+
readonly ids: ReturnType<ImportContext["declareEdgeIds"]>;
|
|
3891
|
+
readonly kind: ReturnType<ImportContext["uniformKind"]>;
|
|
3892
|
+
readonly edgeClassesColumn: ColumnHandle;
|
|
3893
|
+
},
|
|
3894
|
+
record: JsonRecord,
|
|
3895
|
+
data: JsonRecord,
|
|
3896
|
+
element: string,
|
|
3897
|
+
): void {
|
|
3898
|
+
const { ids, kind, edgeClassesColumn } = shared;
|
|
3899
|
+
try {
|
|
3900
|
+
const u = ctx.coerceId(data.source, `${element}.data.source`);
|
|
3901
|
+
const v = ctx.coerceId(data.target, `${element}.data.target`);
|
|
3902
|
+
if (u === null || v === null) {
|
|
3903
|
+
ctx.countSkipped("edge");
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
const idValue = ctx.edgeIdValue(ids, data.id);
|
|
3907
|
+
const edge = ctx.pushEdge(u, v, kind, ctx.weightOf(data), element);
|
|
3908
|
+
ctx.setEdgeId(ids, edge, idValue);
|
|
3909
|
+
const { weightFrom } = ctx.options;
|
|
3910
|
+
for (const key of Object.keys(data)) {
|
|
3911
|
+
if (key !== "id" && key !== "source" && key !== "target" && key !== weightFrom) {
|
|
3912
|
+
ctx.edges.write(edge, key, data[key], SUFFIX.data, element);
|
|
3597
3913
|
}
|
|
3598
|
-
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
3599
|
-
writeElementKeys(ctx.edges, edge, record, element);
|
|
3600
|
-
} catch (err) {
|
|
3601
|
-
ctx.skip(err, "edge", element);
|
|
3602
3914
|
}
|
|
3915
|
+
writeClasses(ctx, ctx.edges, edgeClassesColumn, edge, record.classes, element);
|
|
3916
|
+
writeElementKeys(ctx.edges, edge, record, element);
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
ctx.skip(err, "edge", element);
|
|
3603
3919
|
}
|
|
3604
3920
|
}
|
|
3605
3921
|
|