@graphty/graph-io 0.3.28 → 0.3.30
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-DrXtu_96.js → choose-BfOLq6OX.js} +2 -2
- package/dist/chunks/{choose-DrXtu_96.js.map → choose-BfOLq6OX.js.map} +1 -1
- package/dist/chunks/{constants-ikG85dT8.js → constants-M8Qsjz1C.js} +2 -2
- package/dist/chunks/{constants-ikG85dT8.js.map → constants-M8Qsjz1C.js.map} +1 -1
- package/dist/chunks/{escape-D1HPTUrP.js → escape-UMMpQZqs.js} +2 -2
- package/dist/chunks/{escape-D1HPTUrP.js.map → escape-UMMpQZqs.js.map} +1 -1
- package/dist/chunks/{export-CHd4tQE2.js → export-Dq4U8Ubq.js} +165 -148
- package/dist/chunks/{export-CHd4tQE2.js.map → export-Dq4U8Ubq.js.map} +1 -1
- package/dist/chunks/{exporter-DhtW6FxK.js → exporter-BUosqV0K.js} +34 -34
- package/dist/chunks/exporter-BUosqV0K.js.map +1 -0
- package/dist/chunks/{exporter-D23CKNqF.js → exporter-Bd_VZ9ph.js} +9 -10
- package/dist/chunks/exporter-Bd_VZ9ph.js.map +1 -0
- package/dist/chunks/{exporter-DfWTgrti.js → exporter-COgVm-ff.js} +8 -7
- package/dist/chunks/{exporter-DfWTgrti.js.map → exporter-COgVm-ff.js.map} +1 -1
- package/dist/chunks/{exporter-Cl021ZIE.js → exporter-DEk_XLLs.js} +4 -4
- package/dist/chunks/{exporter-Cl021ZIE.js.map → exporter-DEk_XLLs.js.map} +1 -1
- package/dist/chunks/{importer-BZB7iP9r.js → importer-AQ3KpNHz.js} +11 -20
- package/dist/chunks/importer-AQ3KpNHz.js.map +1 -0
- package/dist/chunks/{importer-BVRE--Un.js → importer-BsZa2Phx.js} +5 -5
- package/dist/chunks/importer-BsZa2Phx.js.map +1 -0
- package/dist/chunks/{importer-DpEW5zeG.js → importer-CV8au8gH.js} +4 -4
- package/dist/chunks/{importer-DpEW5zeG.js.map → importer-CV8au8gH.js.map} +1 -1
- package/dist/chunks/{importer-BSALvkui.js → importer-Cj4-lXlv.js} +10 -9
- package/dist/chunks/importer-Cj4-lXlv.js.map +1 -0
- package/dist/chunks/{importer-B2W_58pz.js → importer-DQsgmhxc.js} +5 -5
- package/dist/chunks/{importer-B2W_58pz.js.map → importer-DQsgmhxc.js.map} +1 -1
- package/dist/chunks/{importer-CIAzt-wo.js → importer-Dkn79TjB.js} +6 -6
- package/dist/chunks/importer-Dkn79TjB.js.map +1 -0
- package/dist/chunks/{importer-z_FL4VMb.js → importer-DwSZvMDq.js} +10 -19
- package/dist/chunks/importer-DwSZvMDq.js.map +1 -0
- package/dist/chunks/{json-elements-BAmNBZok.js → json-elements-CreeKoaH.js} +12 -16
- package/dist/chunks/{json-elements-BAmNBZok.js.map → json-elements-CreeKoaH.js.map} +1 -1
- package/dist/chunks/{ontology-export-Bab4eUA8.js → ontology-export-C5Xd3W-7.js} +2 -2
- package/dist/chunks/{ontology-export-Bab4eUA8.js.map → ontology-export-C5Xd3W-7.js.map} +1 -1
- package/dist/chunks/{records-CdlvL4RO.js → records-BwaH_2ll.js} +2 -2
- package/dist/chunks/{records-CdlvL4RO.js.map → records-BwaH_2ll.js.map} +1 -1
- package/dist/chunks/{text-DzXjAiTl.js → text-4HHe6YH3.js} +2 -2
- package/dist/chunks/{text-DzXjAiTl.js.map → text-4HHe6YH3.js.map} +1 -1
- package/dist/csv.js +22 -15
- 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 +9 -8
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +3 -3
- package/dist/graph-io.js +69 -69
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +3 -3
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/export.d.ts +6 -0
- package/dist/src/common/export.d.ts.map +1 -1
- package/dist/src/common/export.js +62 -28
- package/dist/src/common/export.js.map +1 -1
- package/dist/src/common/format.d.ts +0 -8
- package/dist/src/common/format.d.ts.map +1 -1
- package/dist/src/common/format.js +0 -10
- package/dist/src/common/format.js.map +1 -1
- package/dist/src/common/json-elements.d.ts +1 -9
- package/dist/src/common/json-elements.d.ts.map +1 -1
- package/dist/src/common/json-elements.js +2 -12
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +18 -7
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +3 -3
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx2/importer.d.ts.map +1 -1
- package/dist/src/formats/cx2/importer.js +3 -3
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cys/exporter.d.ts.map +1 -1
- package/dist/src/formats/cys/exporter.js +4 -3
- package/dist/src/formats/cys/exporter.js.map +1 -1
- package/dist/src/formats/dot/exporter.js +1 -1
- package/dist/src/formats/dot/importer.d.ts +2 -2
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +7 -11
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts +2 -2
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +7 -5
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/schema.js +1 -1
- package/dist/src/formats/json/exporter.js +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +4 -6
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/obo/syntax.d.ts +4 -0
- package/dist/src/formats/obo/syntax.d.ts.map +1 -1
- package/dist/src/formats/obo/syntax.js +35 -31
- package/dist/src/formats/obo/syntax.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +5 -4
- 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 +3 -7
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +1 -1
- package/dist/src/formats/pajek/syntax.js +1 -1
- package/dist/src/formats/xgmml/constants.d.ts +1 -1
- package/dist/src/formats/xgmml/constants.js +1 -1
- package/dist/src/formats/xgmml/emit.js +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +4 -3
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +1 -1
- package/src/common/export.ts +105 -62
- package/src/common/format.ts +0 -11
- package/src/common/json-elements.ts +2 -13
- package/src/formats/csv/exporter.ts +19 -15
- package/src/formats/cx/importer.ts +2 -3
- package/src/formats/cx2/importer.ts +2 -3
- package/src/formats/cys/exporter.ts +4 -3
- package/src/formats/dot/exporter.ts +1 -1
- package/src/formats/dot/importer.ts +7 -17
- package/src/formats/gexf/exporter.ts +7 -5
- package/src/formats/gexf/schema.ts +1 -1
- package/src/formats/json/exporter.ts +1 -1
- package/src/formats/json/importer.ts +4 -7
- package/src/formats/obo/syntax.ts +35 -31
- package/src/formats/pajek/exporter.ts +5 -4
- package/src/formats/pajek/importer.ts +3 -13
- package/src/formats/pajek/syntax.ts +1 -1
- package/src/formats/xgmml/constants.ts +1 -1
- package/src/formats/xgmml/emit.ts +1 -1
- package/src/formats/xgmml/exporter.ts +4 -3
- package/src/types.ts +6 -0
- package/dist/chunks/exporter-D23CKNqF.js.map +0 -1
- package/dist/chunks/exporter-DhtW6FxK.js.map +0 -1
- package/dist/chunks/importer-BSALvkui.js.map +0 -1
- package/dist/chunks/importer-BVRE--Un.js.map +0 -1
- package/dist/chunks/importer-BZB7iP9r.js.map +0 -1
- package/dist/chunks/importer-CIAzt-wo.js.map +0 -1
- package/dist/chunks/importer-z_FL4VMb.js.map +0 -1
package/src/common/export.ts
CHANGED
|
@@ -318,6 +318,12 @@ export interface CheckExtras {
|
|
|
318
318
|
* W_WEIGHTS_DROPPED note. Default true.
|
|
319
319
|
*/
|
|
320
320
|
readonly weights?: boolean | undefined;
|
|
321
|
+
/**
|
|
322
|
+
* The tables the export writes, of "node", "edge" and "graph"; a gap in a table it does not
|
|
323
|
+
* write is not a loss of this export and gets no note. Default all three. A CSV node table is
|
|
324
|
+
* `["node"]`, so its notes are about node ids and node columns only.
|
|
325
|
+
*/
|
|
326
|
+
readonly tables?: readonly ("node" | "edge" | "graph")[] | undefined;
|
|
321
327
|
/**
|
|
322
328
|
* How the format's importer turns id text back into ids ("canonical" for most text formats), so
|
|
323
329
|
* an id that comes back with another type (the text "7" as the number 7) is reported as
|
|
@@ -353,60 +359,11 @@ export function checkCapabilities(
|
|
|
353
359
|
const note = (code: string, message: string, column: string | null = null, count: number | null = null): void => {
|
|
354
360
|
notes.push(Object.freeze({ code, message, column, count }));
|
|
355
361
|
};
|
|
362
|
+
const tables = extras.tables ?? ["node", "edge", "graph"];
|
|
363
|
+
const edges = tables.includes("edge");
|
|
356
364
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (mixed > 0 && !caps.mixedDirection) {
|
|
360
|
-
if (options.onMixedDirection === "error") {
|
|
361
|
-
note(
|
|
362
|
-
LOSS.MIXED_DIRECTION_ERROR,
|
|
363
|
-
`${mixed} undirected edge${plural(mixed)} in a directed graph, and the format holds one direction per file, so the save fails unless onMixedDirection is "directed" or "undirected"`,
|
|
364
|
-
null,
|
|
365
|
-
mixed,
|
|
366
|
-
);
|
|
367
|
-
} else {
|
|
368
|
-
note(
|
|
369
|
-
LOSS.MIXED_DIRECTION,
|
|
370
|
-
`${mixed} undirected edge${plural(mixed)} in a directed graph ${agree(mixed, "is", "are")} written as ${options.onMixedDirection} (onMixedDirection), and the format holds one direction per file`,
|
|
371
|
-
null,
|
|
372
|
-
mixed,
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (snapshot.flags.multigraph && !caps.multiEdges) {
|
|
378
|
-
const parallel = countParallelArcs(snapshot);
|
|
379
|
-
note(
|
|
380
|
-
LOSS.MULTI_EDGES,
|
|
381
|
-
`${parallel} parallel edge${plural(parallel)}; the format has no parallel edges`,
|
|
382
|
-
null,
|
|
383
|
-
parallel,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
if (snapshot.selfLoopCount > 0 && !caps.selfLoops) {
|
|
387
|
-
note(
|
|
388
|
-
LOSS.SELF_LOOPS,
|
|
389
|
-
`${snapshot.selfLoopCount} self-loop${plural(snapshot.selfLoopCount)}; the format has no self-loops`,
|
|
390
|
-
null,
|
|
391
|
-
snapshot.selfLoopCount,
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const edgeIds = snapshot.edges.byRole("id");
|
|
396
|
-
if (caps.edgeIds === "required" && edgeIds === null && snapshot.edgeCount > 0) {
|
|
397
|
-
note(
|
|
398
|
-
LOSS.EDGE_IDS_GENERATED,
|
|
399
|
-
"the format requires edge ids, and the graph has none; the edges are written with generated ids, which read back as a new edge id column",
|
|
400
|
-
null,
|
|
401
|
-
snapshot.edgeCount,
|
|
402
|
-
);
|
|
403
|
-
} else if (caps.edgeIds === "none" && edgeIds !== null) {
|
|
404
|
-
note(
|
|
405
|
-
LOSS.EDGE_IDS_DROPPED,
|
|
406
|
-
`edge id column "${edgeIds.meta.name}" cannot be written`,
|
|
407
|
-
edgeIds.meta.name,
|
|
408
|
-
snapshot.edgeCount,
|
|
409
|
-
);
|
|
365
|
+
if (edges) {
|
|
366
|
+
checkEdgeStructure(snapshot, caps, options, note);
|
|
410
367
|
}
|
|
411
368
|
|
|
412
369
|
const unrepresentable = countUnrepresentableIds(snapshot, caps.idCharset);
|
|
@@ -455,24 +412,40 @@ export function checkCapabilities(
|
|
|
455
412
|
}
|
|
456
413
|
}
|
|
457
414
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
415
|
+
if (edges) {
|
|
416
|
+
checkWeights(snapshot, extras, note);
|
|
417
|
+
}
|
|
418
|
+
if (tables.includes("node")) {
|
|
419
|
+
checkColumns(snapshot.nodes, "node", caps, extras, note);
|
|
420
|
+
}
|
|
421
|
+
if (edges) {
|
|
422
|
+
checkColumns(snapshot.edges, "edge", caps, extras, note);
|
|
423
|
+
}
|
|
424
|
+
const graphColumns = tables.includes("graph") ? snapshot.graph.names() : [];
|
|
462
425
|
if (graphColumns.length > 0) {
|
|
463
426
|
if (caps.graphAttributes) {
|
|
464
427
|
checkColumns(snapshot.graph, "graph", caps, extras, note);
|
|
465
428
|
} else {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
429
|
+
notes.push(
|
|
430
|
+
Object.freeze({
|
|
431
|
+
code: LOSS.GRAPH_ATTRIBUTES,
|
|
432
|
+
message: `${graphColumns.length} graph attribute${plural(graphColumns.length)} cannot be written`,
|
|
433
|
+
column: null,
|
|
434
|
+
count: graphColumns.length,
|
|
435
|
+
columns: Object.freeze([...graphColumns]),
|
|
436
|
+
}),
|
|
471
437
|
);
|
|
472
438
|
}
|
|
473
439
|
}
|
|
474
440
|
|
|
475
441
|
for (const [name, table] of snapshot.extensions) {
|
|
442
|
+
// "temporal:node:<column>" is the node table's, "temporal:edge:<column>" the edge table's;
|
|
443
|
+
// any other extension table is the graph's.
|
|
444
|
+
const [, kind] = name.split(":");
|
|
445
|
+
const owner = name.startsWith("temporal:") && (kind === "node" || kind === "edge") ? kind : "graph";
|
|
446
|
+
if (!tables.includes(owner)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
476
449
|
if (name.startsWith("temporal:")) {
|
|
477
450
|
if (caps.temporal !== "dynamic-values") {
|
|
478
451
|
note(
|
|
@@ -494,6 +467,76 @@ export function checkCapabilities(
|
|
|
494
467
|
return notes;
|
|
495
468
|
}
|
|
496
469
|
|
|
470
|
+
/**
|
|
471
|
+
* The edge-structure checks of checkCapabilities(): direction, parallel edges, self-loops and
|
|
472
|
+
* edge ids. Skipped for an export that writes no edge table.
|
|
473
|
+
* @param snapshot - the snapshot about to be exported
|
|
474
|
+
* @param caps - the exporter's capabilities
|
|
475
|
+
* @param options - the resolved common export options
|
|
476
|
+
* @param note - the recorder
|
|
477
|
+
*/
|
|
478
|
+
function checkEdgeStructure(
|
|
479
|
+
snapshot: GraphSnapshot,
|
|
480
|
+
caps: ExportCapabilities,
|
|
481
|
+
options: ResolvedExportOptions,
|
|
482
|
+
note: NoteFn,
|
|
483
|
+
): void {
|
|
484
|
+
// mixed direction (design section 3.6): an expanded snapshot carries the pair / directed roles
|
|
485
|
+
const mixed = countMixedEdges(snapshot);
|
|
486
|
+
if (mixed > 0 && !caps.mixedDirection) {
|
|
487
|
+
if (options.onMixedDirection === "error") {
|
|
488
|
+
note(
|
|
489
|
+
LOSS.MIXED_DIRECTION_ERROR,
|
|
490
|
+
`${mixed} undirected edge${plural(mixed)} in a directed graph, and the format holds one direction per file, so the save fails unless onMixedDirection is "directed" or "undirected"`,
|
|
491
|
+
null,
|
|
492
|
+
mixed,
|
|
493
|
+
);
|
|
494
|
+
} else {
|
|
495
|
+
note(
|
|
496
|
+
LOSS.MIXED_DIRECTION,
|
|
497
|
+
`${mixed} undirected edge${plural(mixed)} in a directed graph ${agree(mixed, "is", "are")} written as ${options.onMixedDirection} (onMixedDirection), and the format holds one direction per file`,
|
|
498
|
+
null,
|
|
499
|
+
mixed,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (snapshot.flags.multigraph && !caps.multiEdges) {
|
|
505
|
+
const parallel = countParallelArcs(snapshot);
|
|
506
|
+
note(
|
|
507
|
+
LOSS.MULTI_EDGES,
|
|
508
|
+
`${parallel} parallel edge${plural(parallel)}; the format has no parallel edges`,
|
|
509
|
+
null,
|
|
510
|
+
parallel,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (snapshot.selfLoopCount > 0 && !caps.selfLoops) {
|
|
514
|
+
note(
|
|
515
|
+
LOSS.SELF_LOOPS,
|
|
516
|
+
`${snapshot.selfLoopCount} self-loop${plural(snapshot.selfLoopCount)}; the format has no self-loops`,
|
|
517
|
+
null,
|
|
518
|
+
snapshot.selfLoopCount,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const edgeIds = snapshot.edges.byRole("id");
|
|
523
|
+
if (caps.edgeIds === "required" && edgeIds === null && snapshot.edgeCount > 0) {
|
|
524
|
+
note(
|
|
525
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
526
|
+
"the format requires edge ids, and the graph has none; the edges are written with generated ids, which read back as a new edge id column",
|
|
527
|
+
null,
|
|
528
|
+
snapshot.edgeCount,
|
|
529
|
+
);
|
|
530
|
+
} else if (caps.edgeIds === "none" && edgeIds !== null) {
|
|
531
|
+
note(
|
|
532
|
+
LOSS.EDGE_IDS_DROPPED,
|
|
533
|
+
`edge id column "${edgeIds.meta.name}" cannot be written`,
|
|
534
|
+
edgeIds.meta.name,
|
|
535
|
+
snapshot.edgeCount,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
497
540
|
/**
|
|
498
541
|
* The ids a charset cannot hold, as a message phrase.
|
|
499
542
|
* @param charset - the format's id charset
|
package/src/common/format.ts
CHANGED
|
@@ -30,17 +30,6 @@ export function formatF32(value: number): string {
|
|
|
30
30
|
return String(value);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* Whether a number read from a file keeps its decimal value in an f32 column: the shortest decimal
|
|
35
|
-
* of its f32 rounding reads back as the same number (0.2 does; 123456789.123 and 0.123456789 do not).
|
|
36
|
-
* @param value - a finite number
|
|
37
|
-
* @returns true when nothing a reader would see is lost
|
|
38
|
-
* @category Plugin helpers
|
|
39
|
-
*/
|
|
40
|
-
export function survivesF32(value: number): boolean {
|
|
41
|
-
return Number(formatF32(Math.fround(value))) === value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
/**
|
|
45
34
|
* The shortest text of an f64 value: `String(x)`, which is already the shortest round-tripping
|
|
46
35
|
* decimal in JS, except for negative zero, which `String()` writes as "0".
|
|
@@ -1655,7 +1655,7 @@ export const POSITION_COLUMN = "position";
|
|
|
1655
1655
|
const Z_COLUMN = "z";
|
|
1656
1656
|
|
|
1657
1657
|
/**
|
|
1658
|
-
* The position column of a Cytoscape-family importer:
|
|
1658
|
+
* The position column of a Cytoscape-family importer: f64 x3, y-up, 2 source dimensions.
|
|
1659
1659
|
* @param format - the importer's format name
|
|
1660
1660
|
* @param sourceDims - 3 when z goes into the position (zAs "position")
|
|
1661
1661
|
* @returns the declaration
|
|
@@ -1664,7 +1664,7 @@ const Z_COLUMN = "z";
|
|
|
1664
1664
|
export function positionDecl(format: string, sourceDims = 2): ColumnDecl {
|
|
1665
1665
|
return {
|
|
1666
1666
|
name: POSITION_COLUMN,
|
|
1667
|
-
dtype: "
|
|
1667
|
+
dtype: "f64",
|
|
1668
1668
|
components: 3,
|
|
1669
1669
|
role: "position",
|
|
1670
1670
|
mutable: true,
|
|
@@ -1690,17 +1690,6 @@ export function zDecl(format: string): ColumnDecl {
|
|
|
1690
1690
|
};
|
|
1691
1691
|
}
|
|
1692
1692
|
|
|
1693
|
-
/**
|
|
1694
|
-
* Whether a coordinate fits the f32 position column: finite once rounded to f32 (1e39 and the
|
|
1695
|
-
* Infinity of a literal like 1e400 do not, nor does NaN).
|
|
1696
|
-
* @param value - the coordinate
|
|
1697
|
-
* @returns true when the column can hold it
|
|
1698
|
-
* @category Plugin helpers
|
|
1699
|
-
*/
|
|
1700
|
-
export function fitsF32(value: number): boolean {
|
|
1701
|
-
return Number.isFinite(Math.fround(value));
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
1693
|
/**
|
|
1705
1694
|
* The head of an input as text for a sniff: UTF-16 by its byte order mark, else UTF-8, never
|
|
1706
1695
|
* failing, without the BOM.
|
|
@@ -385,13 +385,9 @@ function cellText(column: Column, row: number): string | null {
|
|
|
385
385
|
* W_CSV_NODE_TABLE: the node columns an edge-table or adjacency export leaves for a second, node-table export.
|
|
386
386
|
* @param names - the node columns, label first
|
|
387
387
|
* @param table - the table being written ("edges" or "adjacency")
|
|
388
|
-
* @param
|
|
388
|
+
* @param notes - receives the note
|
|
389
389
|
*/
|
|
390
|
-
function nodeTableNote(
|
|
391
|
-
names: readonly string[],
|
|
392
|
-
table: string,
|
|
393
|
-
note: (code: string, message: string, column?: string | null, count?: number | null) => void,
|
|
394
|
-
): void {
|
|
390
|
+
function nodeTableNote(names: readonly string[], table: string, notes: LossNote[]): void {
|
|
395
391
|
const written = names.length;
|
|
396
392
|
if (written > 0) {
|
|
397
393
|
const shown =
|
|
@@ -399,11 +395,14 @@ function nodeTableNote(
|
|
|
399
395
|
.slice(0, 3)
|
|
400
396
|
.map((n) => JSON.stringify(n))
|
|
401
397
|
.join(", ") + (written > 3 ? ", ..." : "");
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
398
|
+
notes.push(
|
|
399
|
+
Object.freeze({
|
|
400
|
+
code: CSV_LOSS.NODE_TABLE,
|
|
401
|
+
message: `the ${table === "adjacency" ? "adjacency" : "edge"} table has no room for node attributes: ${written} node column${plural(written)} (${shown}) ${agree(written, "is", "are")} written only by a second export with table: "nodes"`,
|
|
402
|
+
column: null,
|
|
403
|
+
count: written,
|
|
404
|
+
columns: Object.freeze([...names]),
|
|
405
|
+
}),
|
|
407
406
|
);
|
|
408
407
|
}
|
|
409
408
|
}
|
|
@@ -483,7 +482,8 @@ function planExport(
|
|
|
483
482
|
edgeRows.push(e);
|
|
484
483
|
}
|
|
485
484
|
}
|
|
486
|
-
|
|
485
|
+
// A node table holds no edge: the direction and mutual notes are the edge table's.
|
|
486
|
+
if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
|
|
487
487
|
const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
|
|
488
488
|
const mixed = countMixedEdges(snapshot);
|
|
489
489
|
if (!snapshot.directed) {
|
|
@@ -509,7 +509,7 @@ function planExport(
|
|
|
509
509
|
0,
|
|
510
510
|
);
|
|
511
511
|
}
|
|
512
|
-
if (folding.mutualCount > 0) {
|
|
512
|
+
if (folding.mutualCount > 0 && csv.table !== "nodes") {
|
|
513
513
|
note(
|
|
514
514
|
CSV_LOSS.MUTUAL_EXPANDED,
|
|
515
515
|
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
|
|
@@ -586,7 +586,7 @@ function planExport(
|
|
|
586
586
|
nodeTableNote(
|
|
587
587
|
[...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
|
|
588
588
|
csv.table,
|
|
589
|
-
|
|
589
|
+
notes,
|
|
590
590
|
);
|
|
591
591
|
}
|
|
592
592
|
|
|
@@ -1116,6 +1116,7 @@ function headerlessNotes(plan: Plan): LossNote[] {
|
|
|
1116
1116
|
message: `without a header the file is read back by position (${csv.table === "nodes" ? "id" : "source, target, weight"}, then unnamed columns), so ${names.length === 1 ? "the column" : "the columns"} ${shown} ${agree(names.length, "reads", "read")} back unnamed or in another column's place; write the header, or for an edge list pass dialect: "generic" and write only source, target and weight`,
|
|
1117
1117
|
column: null,
|
|
1118
1118
|
count: names.length,
|
|
1119
|
+
columns: Object.freeze([...names]),
|
|
1119
1120
|
}),
|
|
1120
1121
|
];
|
|
1121
1122
|
}
|
|
@@ -1166,7 +1167,10 @@ function headerlessEdgeColumns(plan: Plan): string[] {
|
|
|
1166
1167
|
function check(snapshot: GraphSnapshot, options?: CsvExportOptions & CommonExportOptions): readonly LossNote[] {
|
|
1167
1168
|
const plan = planExport(snapshot, options, true);
|
|
1168
1169
|
return Object.freeze([
|
|
1169
|
-
...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, {
|
|
1170
|
+
...checkCapabilities(snapshot, CSV_CAPABILITIES, plan.common, {
|
|
1171
|
+
roles: KEPT_ROLES,
|
|
1172
|
+
...(plan.csv.table === "nodes" ? { tables: ["node"] as const } : {}),
|
|
1173
|
+
}),
|
|
1170
1174
|
...plan.notes,
|
|
1171
1175
|
...headerlessNotes(plan),
|
|
1172
1176
|
]);
|
|
@@ -86,7 +86,6 @@ import {
|
|
|
86
86
|
CxStructure,
|
|
87
87
|
declareFresh,
|
|
88
88
|
ExactInteger,
|
|
89
|
-
fitsF32,
|
|
90
89
|
flipY,
|
|
91
90
|
headText,
|
|
92
91
|
inexactLiteral,
|
|
@@ -2445,11 +2444,11 @@ class CxReader {
|
|
|
2445
2444
|
);
|
|
2446
2445
|
continue;
|
|
2447
2446
|
}
|
|
2448
|
-
if (!
|
|
2447
|
+
if (!Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
2449
2448
|
this.report.error(
|
|
2450
2449
|
"validation-error",
|
|
2451
2450
|
BAD_VALUE_CODE,
|
|
2452
|
-
`a cartesianLayout element for node ${shown(value.node)} has the coordinates ${String(value.x)}, ${String(value.y)},
|
|
2451
|
+
`a cartesianLayout element for node ${shown(value.node)} has the coordinates ${String(value.x)}, ${String(value.y)}, which are not both finite; skipped`,
|
|
2453
2452
|
{ line, element: "cartesianLayout" },
|
|
2454
2453
|
);
|
|
2455
2454
|
continue;
|
|
@@ -79,7 +79,6 @@ import {
|
|
|
79
79
|
CxStructure,
|
|
80
80
|
declareFresh,
|
|
81
81
|
ExactInteger,
|
|
82
|
-
fitsF32,
|
|
83
82
|
flipY,
|
|
84
83
|
headText,
|
|
85
84
|
inexactLiteral,
|
|
@@ -1638,11 +1637,11 @@ class Cx2Reader {
|
|
|
1638
1637
|
`${CX2_ISSUE.PARTIAL_LAYOUT}:z`,
|
|
1639
1638
|
);
|
|
1640
1639
|
}
|
|
1641
|
-
if (x !== null && y !== null && (!
|
|
1640
|
+
if (x !== null && y !== null && (!Number.isFinite(x) || !Number.isFinite(y))) {
|
|
1642
1641
|
this.report.error(
|
|
1643
1642
|
"validation-error",
|
|
1644
1643
|
BAD_VALUE_CODE,
|
|
1645
|
-
`${element}: the coordinates ${String(x)}, ${String(y)} are
|
|
1644
|
+
`${element}: the coordinates ${String(x)}, ${String(y)} are not both finite; the position is unset`,
|
|
1646
1645
|
{ line, element },
|
|
1647
1646
|
);
|
|
1648
1647
|
} else if (x !== null && y !== null) {
|
|
@@ -966,7 +966,7 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
|
966
966
|
for (const gen of checkCapabilities(snapshot, CYS_CAPABILITIES, common, {
|
|
967
967
|
roles: SLOT_ROLES,
|
|
968
968
|
roleNames: ROLE_NAMES,
|
|
969
|
-
positionDtype: "
|
|
969
|
+
positionDtype: "f64",
|
|
970
970
|
})) {
|
|
971
971
|
// json is reported per column below; ids by the SUID rule (positive integers)
|
|
972
972
|
if (gen.code !== LOSS.JSON && gen.code !== LOSS.ID_CHARSET && gen.code !== LOSS.ID_MANGLED) {
|
|
@@ -1224,8 +1224,9 @@ function viewCoords(p: Plan, i: number): string[] {
|
|
|
1224
1224
|
const coords: string[] = [];
|
|
1225
1225
|
if (p.position?.isSet(i) === true) {
|
|
1226
1226
|
const [x, y, z] = Array.from(p.position.value(i) as ArrayLike<number>);
|
|
1227
|
+
const fmt = p.position.dtype === "f32" ? formatF32 : formatF64;
|
|
1227
1228
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
1228
|
-
coords.push(`x="${
|
|
1229
|
+
coords.push(`x="${fmt(x)}"`, `y="${fmt(y === 0 ? 0 : -y)}"`);
|
|
1229
1230
|
}
|
|
1230
1231
|
if (
|
|
1231
1232
|
Number.isFinite(x) &&
|
|
@@ -1235,7 +1236,7 @@ function viewCoords(p: Plan, i: number): string[] {
|
|
|
1235
1236
|
z !== 0 &&
|
|
1236
1237
|
Number.isFinite(z)
|
|
1237
1238
|
) {
|
|
1238
|
-
coords.push(`z="${
|
|
1239
|
+
coords.push(`z="${fmt(z)}"`);
|
|
1239
1240
|
}
|
|
1240
1241
|
}
|
|
1241
1242
|
if (p.z?.isSet(i) === true) {
|
|
@@ -206,7 +206,7 @@ export const dotExporter: GraphExporter<DotExportOptions> = Object.freeze({
|
|
|
206
206
|
check(snapshot: GraphSnapshot, options?: DotExportOptions & CommonExportOptions): readonly LossNote[] {
|
|
207
207
|
const resolved = resolveExportOptions(options);
|
|
208
208
|
const notes = checkCapabilities(snapshot, CAPABILITIES, resolved, {
|
|
209
|
-
positionDtype: "
|
|
209
|
+
positionDtype: "f64",
|
|
210
210
|
roles: KEPT_ROLES,
|
|
211
211
|
roleNames: ROLE_NAMES,
|
|
212
212
|
});
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* name (fdp's cluster edges) merge into that one node with a warning. Other subgraphs are
|
|
22
22
|
* transparent grouping; their attributes are reported as dropped.
|
|
23
23
|
* - attribute values are ID strings inferred per column by the sink under the 5.1 grammar, except
|
|
24
|
-
* `label` (text, role label), `pos` on a node (the position role column,
|
|
24
|
+
* `label` (text, role label), `pos` on a node (the position role column, f64 x3, design section
|
|
25
25
|
* 5.2), `weight` (THE weight, `weightFrom`) and `key` (cgraph's edge identity, the edge id role).
|
|
26
26
|
* - ids are coerced with the common rule (`canonical` by default: `1` and `"1"` are the same node,
|
|
27
27
|
* as the DOT grammar says).
|
|
@@ -68,7 +68,6 @@ import {
|
|
|
68
68
|
UNKNOWN_ENCODING_CODE,
|
|
69
69
|
} from "../../common/codes.js";
|
|
70
70
|
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
71
|
-
import { survivesF32 } from "../../common/format.js";
|
|
72
71
|
import { IdCoercer } from "../../common/ids.js";
|
|
73
72
|
import { canonicalEncoding, readText, throwIfAborted } from "../../common/input.js";
|
|
74
73
|
import {
|
|
@@ -177,7 +176,7 @@ export const DOT_ISSUE = Object.freeze({
|
|
|
177
176
|
CLUSTER_NODE_MERGED: "W_DOT_CLUSTER_NODE_MERGED",
|
|
178
177
|
/** A node mentioned in two unrelated clusters keeps the first. */
|
|
179
178
|
CLUSTER_CONFLICT: "W_DOT_CLUSTER_CONFLICT",
|
|
180
|
-
/** A node's `pos` is not a point, or
|
|
179
|
+
/** A node's `pos` is not a point, or has a coordinate that is not finite; the value was dropped. */
|
|
181
180
|
BAD_POS: "W_DOT_BAD_POS",
|
|
182
181
|
/** Node `pos` values mix two and three coordinates; the position column records the first's. */
|
|
183
182
|
POS_DIMS: "W_DOT_POS_DIMS",
|
|
@@ -1672,25 +1671,16 @@ class DotParser {
|
|
|
1672
1671
|
const z = match[3] === undefined ? 0 : Number(match[3]);
|
|
1673
1672
|
const dims = match[3] === undefined ? 2 : 3;
|
|
1674
1673
|
const point = [x, y, z];
|
|
1675
|
-
if (point.some((v) => !Number.isFinite(
|
|
1676
|
-
//
|
|
1674
|
+
if (point.some((v) => !Number.isFinite(v))) {
|
|
1675
|
+
// a half-infinite point would be a half-valid position
|
|
1677
1676
|
this.report.warning(
|
|
1678
1677
|
"validation-error",
|
|
1679
1678
|
DOT_ISSUE.BAD_POS,
|
|
1680
|
-
`pos ${JSON.stringify(text)}
|
|
1679
|
+
`pos ${JSON.stringify(text)} has a coordinate that is not finite; dropped`,
|
|
1681
1680
|
{ line, element },
|
|
1682
1681
|
);
|
|
1683
1682
|
return;
|
|
1684
1683
|
}
|
|
1685
|
-
if (!point.every(survivesF32)) {
|
|
1686
|
-
this.report.warnOnce(
|
|
1687
|
-
"precision",
|
|
1688
|
-
DOT_ISSUE.PRECISION,
|
|
1689
|
-
`pos ${JSON.stringify(text)} has a coordinate the f32 position column rounds (warned once; positions=false keeps pos as written)`,
|
|
1690
|
-
{ line, element },
|
|
1691
|
-
`${DOT_ISSUE.PRECISION}:pos`,
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
1684
|
if (this.posDims === 0) {
|
|
1695
1685
|
this.posDims = dims;
|
|
1696
1686
|
} else if (dims !== this.posDims) {
|
|
@@ -1769,7 +1759,7 @@ class DotParser {
|
|
|
1769
1759
|
}
|
|
1770
1760
|
|
|
1771
1761
|
/**
|
|
1772
|
-
* The position column (
|
|
1762
|
+
* The position column (f64 x3, role position, design section 5.2), declared on the first `pos`.
|
|
1773
1763
|
* @param dims - the dimensions of the first value, recorded in extra.sourceDims
|
|
1774
1764
|
* @returns the handle
|
|
1775
1765
|
*/
|
|
@@ -1777,7 +1767,7 @@ class DotParser {
|
|
|
1777
1767
|
if (this.positionHandle === INVALID_INDEX) {
|
|
1778
1768
|
this.positionHandle = this.declare("node", {
|
|
1779
1769
|
name: POS_ATTRIBUTE,
|
|
1780
|
-
dtype: "
|
|
1770
|
+
dtype: "f64",
|
|
1781
1771
|
components: 3,
|
|
1782
1772
|
nullable: true,
|
|
1783
1773
|
mutable: true,
|
|
@@ -93,8 +93,8 @@ export const GEXF_LOSS = Object.freeze({
|
|
|
93
93
|
/** A numeric edge id column: GEXF edge ids read back as strings. */
|
|
94
94
|
EDGE_ID_TEXT: "W_GEXF_EDGE_ID_TEXT",
|
|
95
95
|
/**
|
|
96
|
-
* A visual attribute (position, color, size, thickness) is stored at
|
|
97
|
-
* as a 32-bit float.
|
|
96
|
+
* A visual attribute (position, color, size, thickness) is stored at another precision than the importer gives it;
|
|
97
|
+
* a position reads back as a 64-bit float, a color, size or thickness as a 32-bit float.
|
|
98
98
|
*/
|
|
99
99
|
VIZ_DTYPE: "W_GEXF_VIZ_DTYPE",
|
|
100
100
|
/**
|
|
@@ -271,7 +271,7 @@ function planExport(
|
|
|
271
271
|
...checkCapabilities(snapshot, caps, resolved, {
|
|
272
272
|
openIntervals: version === "1.2",
|
|
273
273
|
temporalText: true,
|
|
274
|
-
positionDtype: "
|
|
274
|
+
positionDtype: "f64",
|
|
275
275
|
roles: MAPPED_ROLES,
|
|
276
276
|
// GEXF 1.2 has no edge kind: the kind column is dropped (W_GEXF_KIND_DROPPED), not renamed
|
|
277
277
|
roleNames: version === "1.2" ? ROLE_NAMES_1_2 : ROLE_NAMES,
|
|
@@ -608,10 +608,12 @@ function collectRoles(
|
|
|
608
608
|
}
|
|
609
609
|
continue;
|
|
610
610
|
}
|
|
611
|
-
|
|
611
|
+
// the importer reads a position as f64 and every other viz value as f32
|
|
612
|
+
const vizDtype = role === "position" ? "f64" : "f32";
|
|
613
|
+
if (VIZ_NUMERIC_ROLES.has(role) && column.dtype !== vizDtype) {
|
|
612
614
|
note(
|
|
613
615
|
GEXF_LOSS.VIZ_DTYPE,
|
|
614
|
-
`${domain} column "${meta.name}" (${role}) is ${column.dtype}; viz values read back as
|
|
616
|
+
`${domain} column "${meta.name}" (${role}) is ${column.dtype}; viz values read back as ${vizDtype}`,
|
|
615
617
|
meta.name,
|
|
616
618
|
column.length - column.nullCount,
|
|
617
619
|
);
|
|
@@ -648,7 +648,7 @@ function plan(snapshot: GraphSnapshot, resolved: Resolved): Plan {
|
|
|
648
648
|
// plain attributes and read back without their role)
|
|
649
649
|
const notes = checkCapabilities(snapshot, caps, resolved.common, {
|
|
650
650
|
roles: SLOT_ROLES[dialect],
|
|
651
|
-
positionDtype: "
|
|
651
|
+
positionDtype: "f64",
|
|
652
652
|
}).filter((n) => n.code !== LOSS.POSITIONS && n.code !== LOSS.EDGE_IDS_DROPPED);
|
|
653
653
|
const note: NoteFn = (code, message, column = null, count = null): void => {
|
|
654
654
|
notes.push(Object.freeze({ code, message, column, count }));
|
|
@@ -3369,7 +3369,7 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
3369
3369
|
const positionColumn = ctx.nodes.declareIf(
|
|
3370
3370
|
{
|
|
3371
3371
|
name: POSITION_COLUMN,
|
|
3372
|
-
dtype: "
|
|
3372
|
+
dtype: "f64",
|
|
3373
3373
|
components: 3,
|
|
3374
3374
|
role: "position",
|
|
3375
3375
|
mutable: true,
|
|
@@ -3450,7 +3450,7 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
3450
3450
|
report.error(
|
|
3451
3451
|
"validation-error",
|
|
3452
3452
|
JSON_ISSUE.BAD_VALUE,
|
|
3453
|
-
`${element}: position must be an object with numeric x and y (and z) that are finite
|
|
3453
|
+
`${element}: position must be an object with numeric x and y (and z) that are finite numbers`,
|
|
3454
3454
|
{ element },
|
|
3455
3455
|
);
|
|
3456
3456
|
}
|
|
@@ -3656,16 +3656,13 @@ function hasPositionZ(element: unknown): boolean {
|
|
|
3656
3656
|
return isJsonObject(element) && isJsonObject(element.position) && typeof element.position.z === "number";
|
|
3657
3657
|
}
|
|
3658
3658
|
|
|
3659
|
-
/** The largest finite f32, the bound of a position coordinate. */
|
|
3660
|
-
const F32_MAX = 3.4028234663852886e38;
|
|
3661
|
-
|
|
3662
3659
|
/**
|
|
3663
|
-
* Whether a value is a coordinate the
|
|
3660
|
+
* Whether a value is a coordinate the position column holds: a finite number.
|
|
3664
3661
|
* @param value - the value
|
|
3665
3662
|
* @returns true for a storable coordinate
|
|
3666
3663
|
*/
|
|
3667
3664
|
function isCoordinate(value: unknown): value is number {
|
|
3668
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
3665
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3669
3666
|
}
|
|
3670
3667
|
|
|
3671
3668
|
/**
|