@graphty/graph-io 0.3.19 → 0.3.21
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 +49 -4
- package/dist/chunks/{escape-CWExcecC.js → escape-B-tkk_Cl.js} +74 -16
- package/dist/chunks/escape-B-tkk_Cl.js.map +1 -0
- package/dist/chunks/{writer-DQiKgQJc.js → export-Bh60Dv-n.js} +5 -79
- package/dist/chunks/export-Bh60Dv-n.js.map +1 -0
- package/dist/chunks/{exporter-CbMZyVt-.js → exporter-BGrsWamJ.js} +7 -6
- package/dist/chunks/{exporter-CbMZyVt-.js.map → exporter-BGrsWamJ.js.map} +1 -1
- package/dist/chunks/exporter-DIeGJXAZ.js +834 -0
- package/dist/chunks/exporter-DIeGJXAZ.js.map +1 -0
- package/dist/chunks/{importer-BNFuV1-K.js → importer-BWY2FFc5.js} +28 -7
- package/dist/chunks/importer-BWY2FFc5.js.map +1 -0
- package/dist/chunks/importer-DD-xv9_Y.js +1471 -0
- package/dist/chunks/importer-DD-xv9_Y.js.map +1 -0
- package/dist/chunks/{importer-BAP4PrxR.js → importer-DET1tg6s.js} +30 -20
- package/dist/chunks/importer-DET1tg6s.js.map +1 -0
- package/dist/chunks/{importer-ByPGO-09.js → importer-DEcKhsmQ.js} +2 -2
- package/dist/chunks/{importer-ByPGO-09.js.map → importer-DEcKhsmQ.js.map} +1 -1
- package/dist/chunks/{importer-BW-Ft2ps.js → importer-DIrbbnAf.js} +6 -5
- package/dist/chunks/{importer-BW-Ft2ps.js.map → importer-DIrbbnAf.js.map} +1 -1
- package/dist/chunks/importer-D_e7e7LX.js +3055 -0
- package/dist/chunks/importer-D_e7e7LX.js.map +1 -0
- package/dist/chunks/{importer-Bm1_5vPS.js → importer-eUREhBGj.js} +11 -8
- package/dist/chunks/importer-eUREhBGj.js.map +1 -0
- package/dist/chunks/{importer-DOepkgnG.js → importer-vi3bSdtw.js} +4 -4
- package/dist/chunks/{importer-DOepkgnG.js.map → importer-vi3bSdtw.js.map} +1 -1
- package/dist/chunks/{importer-BVGtU1NA.js → importer-zGLo8Dg_.js} +6 -5
- package/dist/chunks/{importer-BVGtU1NA.js.map → importer-zGLo8Dg_.js.map} +1 -1
- package/dist/chunks/{json-elements-CZY1wiZh.js → json-elements-DDS8N2Dd.js} +2 -2
- package/dist/chunks/{json-elements-CZY1wiZh.js.map → json-elements-DDS8N2Dd.js.map} +1 -1
- package/dist/chunks/{records-BzNicMsf.js → records-dbRkxwaq.js} +2 -2
- package/dist/chunks/{records-BzNicMsf.js.map → records-dbRkxwaq.js.map} +1 -1
- package/dist/chunks/{report-BcWboivV.js → report-B1z4WT9e.js} +143 -111
- package/dist/chunks/{report-BcWboivV.js.map → report-B1z4WT9e.js.map} +1 -1
- package/dist/chunks/{weights-CwISIpCP.js → weights-Dzba96G3.js} +5 -5
- package/dist/chunks/{weights-CwISIpCP.js.map → weights-Dzba96G3.js.map} +1 -1
- package/dist/chunks/writer-C7flM-Ih.js +77 -0
- package/dist/chunks/writer-C7flM-Ih.js.map +1 -0
- package/dist/csv.js +6 -5
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.d.ts +1 -0
- package/dist/cys.js +6 -0
- package/dist/cys.js.map +1 -0
- package/dist/dot.js +1 -1
- package/dist/gexf.js +5 -4
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +12 -14
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +288 -141
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +6 -5
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/cell-budget.d.ts +84 -0
- package/dist/src/common/cell-budget.d.ts.map +1 -0
- package/dist/src/common/cell-budget.js +138 -0
- package/dist/src/common/cell-budget.js.map +1 -0
- package/dist/src/common/input.d.ts +10 -0
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +39 -0
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/options.d.ts +6 -0
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +1 -1
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/xml.d.ts +37 -3
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +86 -6
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.d.ts +92 -0
- package/dist/src/common/zip.d.ts.map +1 -0
- package/dist/src/common/zip.js +399 -0
- package/dist/src/common/zip.js.map +1 -0
- package/dist/src/formats/cx/importer.d.ts.map +1 -1
- package/dist/src/formats/cx/importer.js +26 -17
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cys/constants.d.ts +68 -0
- package/dist/src/formats/cys/constants.d.ts.map +1 -0
- package/dist/src/formats/cys/constants.js +76 -0
- package/dist/src/formats/cys/constants.js.map +1 -0
- package/dist/src/formats/cys/importer.d.ts +38 -0
- package/dist/src/formats/cys/importer.d.ts.map +1 -0
- package/dist/src/formats/cys/importer.js +830 -0
- package/dist/src/formats/cys/importer.js.map +1 -0
- package/dist/src/formats/cys/index.d.ts +7 -0
- package/dist/src/formats/cys/index.d.ts.map +1 -0
- package/dist/src/formats/cys/index.js +7 -0
- package/dist/src/formats/cys/index.js.map +1 -0
- package/dist/src/formats/cys/session.d.ts +132 -0
- package/dist/src/formats/cys/session.d.ts.map +1 -0
- package/dist/src/formats/cys/session.js +315 -0
- package/dist/src/formats/cys/session.js.map +1 -0
- package/dist/src/formats/cys/tables.d.ts +90 -0
- package/dist/src/formats/cys/tables.d.ts.map +1 -0
- package/dist/src/formats/cys/tables.js +293 -0
- package/dist/src/formats/cys/tables.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +2 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +12 -0
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.js +6 -2
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts +2 -2
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +1 -1
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts +0 -8
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +7 -17
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +3 -1
- 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 +3 -2
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/xgmml/columns.d.ts +152 -0
- package/dist/src/formats/xgmml/columns.d.ts.map +1 -0
- package/dist/src/formats/xgmml/columns.js +593 -0
- package/dist/src/formats/xgmml/columns.js.map +1 -0
- package/dist/src/formats/xgmml/constants.d.ts +196 -0
- package/dist/src/formats/xgmml/constants.d.ts.map +1 -0
- package/dist/src/formats/xgmml/constants.js +197 -0
- package/dist/src/formats/xgmml/constants.js.map +1 -0
- package/dist/src/formats/xgmml/document.d.ts +361 -0
- package/dist/src/formats/xgmml/document.d.ts.map +1 -0
- package/dist/src/formats/xgmml/document.js +695 -0
- package/dist/src/formats/xgmml/document.js.map +1 -0
- package/dist/src/formats/xgmml/emit.d.ts +341 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -0
- package/dist/src/formats/xgmml/emit.js +1275 -0
- package/dist/src/formats/xgmml/emit.js.map +1 -0
- package/dist/src/formats/xgmml/exporter.d.ts +24 -0
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -0
- package/dist/src/formats/xgmml/exporter.js +999 -0
- package/dist/src/formats/xgmml/exporter.js.map +1 -0
- package/dist/src/formats/xgmml/importer.d.ts +58 -0
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -0
- package/dist/src/formats/xgmml/importer.js +360 -0
- package/dist/src/formats/xgmml/importer.js.map +1 -0
- package/dist/src/formats/xgmml/index.d.ts +8 -0
- package/dist/src/formats/xgmml/index.d.ts.map +1 -0
- package/dist/src/formats/xgmml/index.js +8 -0
- package/dist/src/formats/xgmml/index.js.map +1 -0
- package/dist/src/formats/xgmml/values.d.ts +60 -0
- package/dist/src/formats/xgmml/values.d.ts.map +1 -0
- package/dist/src/formats/xgmml/values.js +148 -0
- package/dist/src/formats/xgmml/values.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +7 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +25 -10
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +2 -0
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.d.ts +1 -0
- package/dist/xgmml.js +9 -0
- package/dist/xgmml.js.map +1 -0
- package/package.json +15 -2
- package/src/common/cell-budget.ts +170 -0
- package/src/common/input.ts +40 -0
- package/src/common/options.ts +1 -1
- package/src/common/xml.ts +127 -6
- package/src/common/zip.ts +472 -0
- package/src/formats/cx/importer.ts +27 -19
- package/src/formats/cys/constants.ts +99 -0
- package/src/formats/cys/importer.ts +1100 -0
- package/src/formats/cys/index.ts +7 -0
- package/src/formats/cys/session.ts +428 -0
- package/src/formats/cys/tables.ts +400 -0
- package/src/formats/dot/exporter.ts +17 -0
- package/src/formats/dot/importer.ts +6 -2
- package/src/formats/gexf/importer.ts +0 -1
- package/src/formats/gml/exporter.ts +7 -18
- package/src/formats/json/exporter.ts +3 -1
- package/src/formats/json/importer.ts +3 -2
- package/src/formats/xgmml/columns.ts +747 -0
- package/src/formats/xgmml/constants.ts +265 -0
- package/src/formats/xgmml/document.ts +975 -0
- package/src/formats/xgmml/emit.ts +1563 -0
- package/src/formats/xgmml/exporter.ts +1215 -0
- package/src/formats/xgmml/importer.ts +491 -0
- package/src/formats/xgmml/index.ts +8 -0
- package/src/formats/xgmml/values.ts +183 -0
- package/src/index.ts +9 -0
- package/src/registry.ts +41 -15
- package/src/sniff.ts +5 -1
- package/dist/chunks/escape-CWExcecC.js.map +0 -1
- package/dist/chunks/importer-BAP4PrxR.js.map +0 -1
- package/dist/chunks/importer-BNFuV1-K.js.map +0 -1
- package/dist/chunks/importer-Bm1_5vPS.js.map +0 -1
- package/dist/chunks/writer-DQiKgQJc.js.map +0 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tables of a Cytoscape 3 session: the CyCSV `.cytable` files (`research-session-and-style.md`
|
|
3
|
+
* section 2.4; Cytoscape's `CSVCyReader`) and the virtual columns `tables/cytables.xml` joins into
|
|
4
|
+
* them. Cells become XGMML att records, typed by the column's Java class, so the XGMML emitter
|
|
5
|
+
* applies the same column rules (types, widening, equations, hidden columns, renames) to a
|
|
6
|
+
* session's tables as to an XGMML file's atts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ResolvedImportOptions } from "../../common/options.js";
|
|
10
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
11
|
+
import { ImportError } from "../../types.js";
|
|
12
|
+
import { CsvRecordReader } from "../csv/records.js";
|
|
13
|
+
import { type AttRec } from "../xgmml/document.js";
|
|
14
|
+
import { CYS_ISSUE, FORMAT } from "./constants.js";
|
|
15
|
+
|
|
16
|
+
/** The Cytoscape type names of the Java classes a CyCSV column can hold. */
|
|
17
|
+
const JAVA_TYPES: Readonly<Record<string, string>> = {
|
|
18
|
+
"java.lang.String": "String",
|
|
19
|
+
"java.lang.Long": "Long",
|
|
20
|
+
"java.lang.Integer": "Integer",
|
|
21
|
+
"java.lang.Double": "Double",
|
|
22
|
+
"java.lang.Boolean": "Boolean",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A list column's class: `java.util.List<java.lang.X>`. */
|
|
26
|
+
const LIST_CLASS = /^java\.util\.List<\s*([\w.]+)\s*>$/;
|
|
27
|
+
|
|
28
|
+
/** One column of a table. */
|
|
29
|
+
interface CyColumn {
|
|
30
|
+
/** The name. */
|
|
31
|
+
readonly name: string;
|
|
32
|
+
/** The Cytoscape type of a scalar column, or of a list's items. */
|
|
33
|
+
readonly type: string;
|
|
34
|
+
/** A list column. */
|
|
35
|
+
readonly list: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One table, read. */
|
|
39
|
+
export interface CyTable {
|
|
40
|
+
/** The table's path under `tables/` as the archive spells it (what cytables.xml names). */
|
|
41
|
+
readonly path: string;
|
|
42
|
+
/** The entry name, for messages. */
|
|
43
|
+
readonly entry: string;
|
|
44
|
+
/** The columns; the first is the primary key. */
|
|
45
|
+
readonly columns: readonly CyColumn[];
|
|
46
|
+
/** The rows by key text, in file order (the first row of a repeated key). */
|
|
47
|
+
readonly rows: ReadonlyMap<string, readonly string[]>;
|
|
48
|
+
/** The line each row starts on, by key. */
|
|
49
|
+
readonly lines: ReadonlyMap<string, number>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What a table read reports besides its rows. */
|
|
53
|
+
interface RowProblems {
|
|
54
|
+
short: number;
|
|
55
|
+
long: number;
|
|
56
|
+
repeated: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read one CyCSV table. A table that cannot be read at all (an unknown CyCSV version or column
|
|
61
|
+
* class, a malformed header, broken CSV) is recorded as E_CYS_TABLE and skipped (null); rows with
|
|
62
|
+
* the wrong number of cells or a repeated key are counted in one W_CYS_TABLE_ROW.
|
|
63
|
+
* @param bytes - the entry's bytes
|
|
64
|
+
* @param path - the table's path under `tables/`
|
|
65
|
+
* @param entry - the entry name, for messages
|
|
66
|
+
* @param report - the import's report
|
|
67
|
+
* @param common - cancellation
|
|
68
|
+
* @returns the table, or null
|
|
69
|
+
*/
|
|
70
|
+
export async function readCyTable(
|
|
71
|
+
bytes: Uint8Array,
|
|
72
|
+
path: string,
|
|
73
|
+
entry: string,
|
|
74
|
+
report: ImportReportBuilder,
|
|
75
|
+
common: ResolvedImportOptions,
|
|
76
|
+
): Promise<CyTable | null> {
|
|
77
|
+
const scratch = new ImportReportBuilder(FORMAT, 0);
|
|
78
|
+
const reader = new CsvRecordReader(bytes, scratch, { delimiter: ",", encoding: "utf-8", signal: common.signal });
|
|
79
|
+
const records: { readonly cells: string[]; readonly line: number }[] = [];
|
|
80
|
+
try {
|
|
81
|
+
for await (const cells of reader) {
|
|
82
|
+
records.push({ cells, line: reader.line });
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err instanceof ImportError) {
|
|
86
|
+
return tableError(report, entry, scratch.issues.at(-1)?.message ?? err.message);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
let at = 0;
|
|
91
|
+
let version = 0;
|
|
92
|
+
if (records[0]?.cells.length === 2 && records[0].cells[0] === "CyCSV-Version") {
|
|
93
|
+
version = Number(records[0].cells[1]);
|
|
94
|
+
at = 1;
|
|
95
|
+
}
|
|
96
|
+
if (version !== 0 && version !== 1) {
|
|
97
|
+
return tableError(report, entry, `CyCSV version "${records[0].cells[1]}" is not 0 or 1`);
|
|
98
|
+
}
|
|
99
|
+
// version 1: names, types, column options, table title; version 0: names, types, table title
|
|
100
|
+
const headerLines = version === 1 ? 4 : 3;
|
|
101
|
+
if (records.length < at + headerLines) {
|
|
102
|
+
return tableError(report, entry, "the table header is incomplete");
|
|
103
|
+
}
|
|
104
|
+
const names = records[at].cells;
|
|
105
|
+
const classes = records[at + 1].cells;
|
|
106
|
+
if (names.length === 0 || classes.length !== names.length) {
|
|
107
|
+
return tableError(report, entry, `${names.length} column name(s) but ${classes.length} column class(es)`);
|
|
108
|
+
}
|
|
109
|
+
const columns: CyColumn[] = [];
|
|
110
|
+
for (let i = 0; i < names.length; i++) {
|
|
111
|
+
const column = columnOf(names[i], classes[i]);
|
|
112
|
+
if (column === null) {
|
|
113
|
+
return tableError(
|
|
114
|
+
report,
|
|
115
|
+
entry,
|
|
116
|
+
`column "${names[i]}" has the class "${classes[i]}", which graph-io cannot read`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
columns.push(column);
|
|
120
|
+
}
|
|
121
|
+
const rows = new Map<string, readonly string[]>();
|
|
122
|
+
const lines = new Map<string, number>();
|
|
123
|
+
const problems: RowProblems = { short: 0, long: 0, repeated: 0 };
|
|
124
|
+
for (const record of records.slice(at + headerLines)) {
|
|
125
|
+
const { cells } = record;
|
|
126
|
+
if (cells.length < columns.length) {
|
|
127
|
+
problems.short++;
|
|
128
|
+
} else if (cells.length > columns.length) {
|
|
129
|
+
problems.long++;
|
|
130
|
+
}
|
|
131
|
+
const key = cells[0];
|
|
132
|
+
if (rows.has(key)) {
|
|
133
|
+
problems.repeated++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
rows.set(key, cells.slice(0, columns.length));
|
|
137
|
+
lines.set(key, record.line);
|
|
138
|
+
}
|
|
139
|
+
if (problems.short + problems.long + problems.repeated > 0) {
|
|
140
|
+
report.warning(
|
|
141
|
+
"parse-error",
|
|
142
|
+
CYS_ISSUE.TABLE_ROW,
|
|
143
|
+
`${entry}: ${problems.short} row(s) with too few cells (the rest are unset), ${problems.long} with too many (the extra cells are ignored), ${problems.repeated} repeating a key (the first row is read)`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return { path, entry, columns, rows, lines };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The column of a name and a Java class.
|
|
151
|
+
* @param name - the name
|
|
152
|
+
* @param javaClass - the class text
|
|
153
|
+
* @returns the column, or null for a class graph-io cannot read
|
|
154
|
+
*/
|
|
155
|
+
function columnOf(name: string, javaClass: string): CyColumn | null {
|
|
156
|
+
const scalar = JAVA_TYPES[javaClass.trim()];
|
|
157
|
+
if (scalar !== undefined) {
|
|
158
|
+
return { name, type: scalar, list: false };
|
|
159
|
+
}
|
|
160
|
+
const list = LIST_CLASS.exec(javaClass.trim());
|
|
161
|
+
const item = list === null ? undefined : JAVA_TYPES[list[1]];
|
|
162
|
+
return item === undefined ? null : { name, type: item, list: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Record E_CYS_TABLE for a table that is skipped.
|
|
167
|
+
* @param report - the report
|
|
168
|
+
* @param entry - the entry name
|
|
169
|
+
* @param reason - why
|
|
170
|
+
* @returns null
|
|
171
|
+
*/
|
|
172
|
+
function tableError(report: ImportReportBuilder, entry: string, reason: string): null {
|
|
173
|
+
report.error("parse-error", CYS_ISSUE.TABLE, `${entry}: ${reason}; the table is not read`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The att record of one cell, or null for an unset cell. CyCSV's rules: an empty cell is unset
|
|
179
|
+
* except in a String column, where it is the empty string; a list cell holds its items separated
|
|
180
|
+
* by newlines (Java's `split`, so trailing empty items vanish; an empty String-list cell is one
|
|
181
|
+
* empty item, any other empty list cell unset); a cell starting with `=` is an equation.
|
|
182
|
+
* @param column - the column
|
|
183
|
+
* @param text - the cell text
|
|
184
|
+
* @param line - the row's line
|
|
185
|
+
* @param namespace - the table namespace the column belongs to (null for the network's own table)
|
|
186
|
+
* @param hidden - whether the column is hidden (HIDDEN and app tables)
|
|
187
|
+
* @returns the att, or null
|
|
188
|
+
*/
|
|
189
|
+
export function cellAtt(
|
|
190
|
+
column: CyColumn,
|
|
191
|
+
text: string,
|
|
192
|
+
line: number,
|
|
193
|
+
namespace: string | null,
|
|
194
|
+
hidden: boolean,
|
|
195
|
+
): AttRec | null {
|
|
196
|
+
if (text.startsWith("=")) {
|
|
197
|
+
return att(column.name, column.list ? "List" : column.type, null, text, line, namespace, hidden, true);
|
|
198
|
+
}
|
|
199
|
+
if (!column.list) {
|
|
200
|
+
if (text.length === 0 && column.type !== "String") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return att(column.name, column.type, null, text, line, namespace, hidden, false);
|
|
204
|
+
}
|
|
205
|
+
if (text.length === 0 && column.type !== "String") {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const list = att(column.name, "List", column.type, null, line, namespace, hidden, false);
|
|
209
|
+
for (const item of javaSplitLines(text)) {
|
|
210
|
+
if (item.length === 0 && column.type !== "String") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
list.children.push(att(column.name, column.type, null, item, line, namespace, hidden, false));
|
|
214
|
+
}
|
|
215
|
+
return list;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Java's `String.split("\n")`: trailing empty strings are removed, and a text without a newline
|
|
220
|
+
* is itself (so `""` is one empty item).
|
|
221
|
+
* @param text - the cell
|
|
222
|
+
* @returns the items
|
|
223
|
+
*/
|
|
224
|
+
function javaSplitLines(text: string): string[] {
|
|
225
|
+
if (!text.includes("\n")) {
|
|
226
|
+
return [text];
|
|
227
|
+
}
|
|
228
|
+
const items = text.split("\n");
|
|
229
|
+
while (items.length > 0 && items[items.length - 1].length === 0) {
|
|
230
|
+
items.pop();
|
|
231
|
+
}
|
|
232
|
+
return items;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* An att record.
|
|
237
|
+
* @param name - the name
|
|
238
|
+
* @param cyType - the Cytoscape type
|
|
239
|
+
* @param elementType - a list's item type, or null
|
|
240
|
+
* @param value - the value, or null
|
|
241
|
+
* @param line - the line
|
|
242
|
+
* @param namespace - the table namespace, or null
|
|
243
|
+
* @param hidden - a hidden column
|
|
244
|
+
* @param equation - the value is a formula
|
|
245
|
+
* @returns the record
|
|
246
|
+
*/
|
|
247
|
+
function att(
|
|
248
|
+
name: string,
|
|
249
|
+
cyType: string,
|
|
250
|
+
elementType: string | null,
|
|
251
|
+
value: string | null,
|
|
252
|
+
line: number,
|
|
253
|
+
namespace: string | null,
|
|
254
|
+
hidden: boolean,
|
|
255
|
+
equation: boolean,
|
|
256
|
+
): AttRec {
|
|
257
|
+
return {
|
|
258
|
+
name,
|
|
259
|
+
type: null,
|
|
260
|
+
cyType,
|
|
261
|
+
elementType,
|
|
262
|
+
value,
|
|
263
|
+
hidden,
|
|
264
|
+
equation,
|
|
265
|
+
children: [],
|
|
266
|
+
extra: {},
|
|
267
|
+
xml: null,
|
|
268
|
+
hasGraph: false,
|
|
269
|
+
text: "",
|
|
270
|
+
line,
|
|
271
|
+
namespace,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** One virtual column of `tables/cytables.xml`. */
|
|
276
|
+
export interface VirtualColumn {
|
|
277
|
+
/** The column name in the target table. */
|
|
278
|
+
readonly name: string;
|
|
279
|
+
/** The target table's path under `tables/`. */
|
|
280
|
+
readonly targetTable: string;
|
|
281
|
+
/** The source table's path. */
|
|
282
|
+
readonly sourceTable: string;
|
|
283
|
+
/** The source column's name. */
|
|
284
|
+
readonly sourceColumn: string;
|
|
285
|
+
/** The join column of the source (normally SUID). */
|
|
286
|
+
readonly sourceJoinKey: string;
|
|
287
|
+
/** The join column of the target (normally SUID). */
|
|
288
|
+
readonly targetJoinKey: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** A resolved virtual column: its type and its value per target row key. */
|
|
292
|
+
interface VirtualValues {
|
|
293
|
+
readonly column: CyColumn;
|
|
294
|
+
readonly values: ReadonlyMap<string, string>;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Resolve the virtual columns of one table: each one's values joined from its source table,
|
|
299
|
+
* following a source that is itself a virtual column. A column whose source table or column is
|
|
300
|
+
* missing, or whose sources form a cycle, is E_CYS_TABLE and skipped.
|
|
301
|
+
* @param target - the table
|
|
302
|
+
* @param virtuals - every virtual column of the session
|
|
303
|
+
* @param tableOf - reads (and caches) a table by path, null when it cannot be read or is absent
|
|
304
|
+
* @param report - the report
|
|
305
|
+
* @returns the virtual columns of the table, in cytables.xml order
|
|
306
|
+
*/
|
|
307
|
+
export async function virtualColumnsOf(
|
|
308
|
+
target: CyTable,
|
|
309
|
+
virtuals: readonly VirtualColumn[],
|
|
310
|
+
tableOf: (path: string) => Promise<CyTable | null>,
|
|
311
|
+
report: ImportReportBuilder,
|
|
312
|
+
): Promise<VirtualValues[]> {
|
|
313
|
+
const out: VirtualValues[] = [];
|
|
314
|
+
for (const virtual of virtuals) {
|
|
315
|
+
if (virtual.targetTable !== target.path) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const resolved = await resolve(virtual, virtuals, tableOf, new Set());
|
|
319
|
+
if (typeof resolved === "string") {
|
|
320
|
+
report.error(
|
|
321
|
+
"parse-error",
|
|
322
|
+
CYS_ISSUE.TABLE,
|
|
323
|
+
`${target.entry}: the virtual column "${virtual.name}" ${resolved}; the column is not read`,
|
|
324
|
+
);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const targetKey = target.columns.findIndex((c) => c.name === virtual.targetJoinKey);
|
|
328
|
+
const values = new Map<string, string>();
|
|
329
|
+
for (const [key, cells] of target.rows) {
|
|
330
|
+
const join = targetKey <= 0 ? key : cells[targetKey];
|
|
331
|
+
const value = join === undefined ? undefined : resolved.values.get(join);
|
|
332
|
+
if (value !== undefined) {
|
|
333
|
+
values.set(key, value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
out.push({ column: { ...resolved.column, name: virtual.name }, values });
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The values of a virtual column's source, by the source's join key.
|
|
343
|
+
* @param virtual - the virtual column
|
|
344
|
+
* @param virtuals - every virtual column
|
|
345
|
+
* @param tableOf - the table reader
|
|
346
|
+
* @param seen - the virtual columns on the current chain (a cycle check)
|
|
347
|
+
* @returns the values, or the reason they cannot be read
|
|
348
|
+
*/
|
|
349
|
+
async function resolve(
|
|
350
|
+
virtual: VirtualColumn,
|
|
351
|
+
virtuals: readonly VirtualColumn[],
|
|
352
|
+
tableOf: (path: string) => Promise<CyTable | null>,
|
|
353
|
+
seen: Set<VirtualColumn>,
|
|
354
|
+
): Promise<VirtualValues | string> {
|
|
355
|
+
if (seen.has(virtual)) {
|
|
356
|
+
return "is part of a cycle of virtual columns";
|
|
357
|
+
}
|
|
358
|
+
seen.add(virtual);
|
|
359
|
+
const source = await tableOf(virtual.sourceTable);
|
|
360
|
+
if (source === null) {
|
|
361
|
+
return `names the source table "${virtual.sourceTable}", which the session does not hold or cannot read`;
|
|
362
|
+
}
|
|
363
|
+
const joinIndex = source.columns.findIndex((c) => c.name === virtual.sourceJoinKey);
|
|
364
|
+
if (joinIndex < 0) {
|
|
365
|
+
return `joins on the column "${virtual.sourceJoinKey}", which its source table does not have`;
|
|
366
|
+
}
|
|
367
|
+
const joinOf = (key: string, cells: readonly string[]): string | undefined =>
|
|
368
|
+
joinIndex === 0 ? key : cells[joinIndex];
|
|
369
|
+
const own = source.columns.findIndex((c) => c.name === virtual.sourceColumn);
|
|
370
|
+
if (own >= 0) {
|
|
371
|
+
const values = new Map<string, string>();
|
|
372
|
+
for (const [key, cells] of source.rows) {
|
|
373
|
+
const join = joinOf(key, cells);
|
|
374
|
+
if (join !== undefined && cells[own] !== undefined) {
|
|
375
|
+
values.set(join, cells[own]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { column: source.columns[own], values };
|
|
379
|
+
}
|
|
380
|
+
const chained = virtuals.find((v) => v.targetTable === source.path && v.name === virtual.sourceColumn);
|
|
381
|
+
if (chained === undefined) {
|
|
382
|
+
return `names the source column "${virtual.sourceColumn}", which its source table does not have`;
|
|
383
|
+
}
|
|
384
|
+
const inner = await resolve(chained, virtuals, tableOf, seen);
|
|
385
|
+
if (typeof inner === "string") {
|
|
386
|
+
return inner;
|
|
387
|
+
}
|
|
388
|
+
// re-key the chained values (by the chained source's join key) to this source's join key
|
|
389
|
+
const values = new Map<string, string>();
|
|
390
|
+
const chainedJoin = source.columns.findIndex((c) => c.name === chained.targetJoinKey);
|
|
391
|
+
for (const [key, cells] of source.rows) {
|
|
392
|
+
const lookup = chainedJoin <= 0 ? key : cells[chainedJoin];
|
|
393
|
+
const value = lookup === undefined ? undefined : inner.values.get(lookup);
|
|
394
|
+
const join = joinOf(key, cells);
|
|
395
|
+
if (value !== undefined && join !== undefined) {
|
|
396
|
+
values.set(join, value);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { column: inner.column, values };
|
|
400
|
+
}
|
|
@@ -70,6 +70,8 @@ export const DOT_LOSS = Object.freeze({
|
|
|
70
70
|
EMPTY_COLUMN_DROPPED: LOSS.EMPTY_COLUMN,
|
|
71
71
|
/** A role-less column named `label` reads back with the label role. */
|
|
72
72
|
ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
|
|
73
|
+
/** A parent that is a plain node is written as a node and a cluster of one name; it reads back marked as a cluster. */
|
|
74
|
+
CLUSTER_MARKED: "W_DOT_CLUSTER_MARKED",
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
/** The roles DOT has a slot for (the label attribute, key, ports, clusters); every other role is reported. */
|
|
@@ -510,6 +512,21 @@ class ExportPlan {
|
|
|
510
512
|
}
|
|
511
513
|
}
|
|
512
514
|
}
|
|
515
|
+
let unmarked = 0;
|
|
516
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
517
|
+
const marked = this.cluster !== null && this.cluster.isSet(i) && this.cluster.value(i) === true;
|
|
518
|
+
if (!marked && this.children.childrenOf(i).length > 0) {
|
|
519
|
+
unmarked++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (unmarked > 0) {
|
|
523
|
+
note(
|
|
524
|
+
DOT_LOSS.CLUSTER_MARKED,
|
|
525
|
+
`${unmarked} parent node(s) are written as a node and a cluster of the same name and read back with "${CLUSTER_COLUMN}" true`,
|
|
526
|
+
CLUSTER_COLUMN,
|
|
527
|
+
unmarked,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
513
530
|
const mutual = this.folding.mutualCount;
|
|
514
531
|
if (mutual > 0) {
|
|
515
532
|
note(
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
resolveImportOptions,
|
|
70
70
|
} from "../../common/options.js";
|
|
71
71
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
72
|
-
import { parseTextCell, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
|
|
72
|
+
import { inferTextDtype, parseTextCell, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
|
|
73
73
|
import { parseWeightText } from "../../common/weights.js";
|
|
74
74
|
import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
|
|
75
75
|
import {
|
|
@@ -1474,7 +1474,11 @@ class DotParser {
|
|
|
1474
1474
|
if (attribute.name === LABEL_ATTRIBUTE) {
|
|
1475
1475
|
this.sink.setGraphValue(attribute.name, attribute.value, { dtype: "string", origin: DOT_ORIGIN });
|
|
1476
1476
|
} else {
|
|
1477
|
-
|
|
1477
|
+
// the dtype comes from the text's grammar, so "20.0" stays f64 as it does in a node column
|
|
1478
|
+
this.sink.setGraphValue(attribute.name, parseTextCell(attribute.value), {
|
|
1479
|
+
dtype: inferTextDtype(attribute.value),
|
|
1480
|
+
origin: DOT_ORIGIN,
|
|
1481
|
+
});
|
|
1478
1482
|
}
|
|
1479
1483
|
} catch (err) {
|
|
1480
1484
|
this.report.recordError(err, { line: attribute.line, element: attribute.name });
|
|
@@ -830,17 +830,6 @@ function countMangled(snapshot: GraphSnapshot): number {
|
|
|
830
830
|
|
|
831
831
|
// ============================================================ writing
|
|
832
832
|
|
|
833
|
-
/**
|
|
834
|
-
* The GML text of a real: the shortest text of the dtype with a decimal point guaranteed
|
|
835
|
-
* (`2.0`, `1.0e-7`), the NetworkX spellings `+INF` / `-INF` / `NAN` for the non-finite values.
|
|
836
|
-
* @param value - the value
|
|
837
|
-
* @param dtype - the dtype the value came from (f32 uses the fround-shortest text)
|
|
838
|
-
* @returns the text
|
|
839
|
-
*/
|
|
840
|
-
export function gmlRealText(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
|
|
841
|
-
return formatGmlReal(value, dtype);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
833
|
/**
|
|
845
834
|
* The GML text of a number by the dtype and origin of its column: an integer text for the integer
|
|
846
835
|
* dtypes and for an f64 column that came from GML `int` values, a real text otherwise.
|
|
@@ -855,12 +844,12 @@ function numberText(value: number, meta: ColumnMeta): string {
|
|
|
855
844
|
case "u8":
|
|
856
845
|
return formatInteger(value);
|
|
857
846
|
case "f32":
|
|
858
|
-
return
|
|
847
|
+
return formatGmlReal(value, "f32");
|
|
859
848
|
default:
|
|
860
849
|
if (meta.origin?.type === "int" && Number.isInteger(value)) {
|
|
861
850
|
return formatInteger(value);
|
|
862
851
|
}
|
|
863
|
-
return
|
|
852
|
+
return formatGmlReal(value, "f64");
|
|
864
853
|
}
|
|
865
854
|
}
|
|
866
855
|
|
|
@@ -873,7 +862,7 @@ function numberText(value: number, meta: ColumnMeta): string {
|
|
|
873
862
|
function jsonScalarText(value: number | string | boolean): string {
|
|
874
863
|
switch (typeof value) {
|
|
875
864
|
case "number":
|
|
876
|
-
return Number.isInteger(value) ? formatInteger(value) :
|
|
865
|
+
return Number.isInteger(value) ? formatInteger(value) : formatGmlReal(value, "f64");
|
|
877
866
|
case "boolean":
|
|
878
867
|
return value ? "1" : "0";
|
|
879
868
|
default:
|
|
@@ -897,13 +886,13 @@ function itemText(item: unknown, itemDtype: string, meta: ColumnMeta): string {
|
|
|
897
886
|
case "u8":
|
|
898
887
|
return formatInteger(item);
|
|
899
888
|
case "f32":
|
|
900
|
-
return
|
|
889
|
+
return formatGmlReal(item, "f32");
|
|
901
890
|
case "json":
|
|
902
891
|
return jsonScalarText(item);
|
|
903
892
|
default:
|
|
904
893
|
return meta.origin?.type === "int" && Number.isInteger(item)
|
|
905
894
|
? formatInteger(item)
|
|
906
|
-
:
|
|
895
|
+
: formatGmlReal(item, "f64");
|
|
907
896
|
}
|
|
908
897
|
case "boolean":
|
|
909
898
|
return item ? "1" : "0";
|
|
@@ -1234,7 +1223,7 @@ function originalIdText(id: NodeId): string {
|
|
|
1234
1223
|
if (typeof id === "string") {
|
|
1235
1224
|
return quoteGmlString(id);
|
|
1236
1225
|
}
|
|
1237
|
-
return Number.isSafeInteger(id) ? formatInteger(id) :
|
|
1226
|
+
return Number.isSafeInteger(id) ? formatInteger(id) : formatGmlReal(id, "f64");
|
|
1238
1227
|
}
|
|
1239
1228
|
|
|
1240
1229
|
/**
|
|
@@ -1308,7 +1297,7 @@ function* gmlParts(context: WriteContext): Generator<string, void, undefined> {
|
|
|
1308
1297
|
const text =
|
|
1309
1298
|
integerWeights && Number.isInteger(weight)
|
|
1310
1299
|
? formatInteger(weight)
|
|
1311
|
-
:
|
|
1300
|
+
: formatGmlReal(weight, context.weights.dtype);
|
|
1312
1301
|
w.line(" ", plan.weightKey, text);
|
|
1313
1302
|
}
|
|
1314
1303
|
for (const { column, key } of context.edgeColumns) {
|
|
@@ -23,6 +23,7 @@ import { type Column, GraphFormatError, type GraphSnapshot, INVALID_INDEX, type
|
|
|
23
23
|
import { type PairFolding, pairFolding } from "../../common/direction.js";
|
|
24
24
|
import { checkCapabilities, countMixedEdges, LOSS } from "../../common/export.js";
|
|
25
25
|
import { formatF32, formatF64 } from "../../common/format.js";
|
|
26
|
+
import { flipY } from "../../common/json-elements.js";
|
|
26
27
|
import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
|
|
27
28
|
import { explicitWeights } from "../../common/weights.js";
|
|
28
29
|
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
@@ -1520,7 +1521,8 @@ function* writeCytoscape(snapshot: GraphSnapshot, p: Plan): Generator<string, vo
|
|
|
1520
1521
|
w.key("position");
|
|
1521
1522
|
w.open("{");
|
|
1522
1523
|
w.member("x", numberText(value[0], f32, c.nonfinite));
|
|
1523
|
-
|
|
1524
|
+
// stored y-up; Cytoscape's y grows downward
|
|
1525
|
+
w.member("y", numberText(flipY(value[1]), f32, c.nonfinite));
|
|
1524
1526
|
w.close("}");
|
|
1525
1527
|
}
|
|
1526
1528
|
}
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
67
67
|
import { ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
68
68
|
import { readText, textChunks, throwIfAborted } from "../../common/input.js";
|
|
69
|
-
import { MAYBE_UNSAFE_INTEGER, reviveNonstandard, rewriteNumbers } from "../../common/json-elements.js";
|
|
69
|
+
import { flipY, MAYBE_UNSAFE_INTEGER, reviveNonstandard, rewriteNumbers } from "../../common/json-elements.js";
|
|
70
70
|
import {
|
|
71
71
|
chooseGraph,
|
|
72
72
|
type ImportFormatDefaults,
|
|
@@ -2669,7 +2669,8 @@ function importCytoscape(ctx: ImportContext, root: unknown): void {
|
|
|
2669
2669
|
if (position !== undefined && position !== null) {
|
|
2670
2670
|
if (isJsonObject(position) && typeof position.x === "number" && typeof position.y === "number") {
|
|
2671
2671
|
point[0] = position.x;
|
|
2672
|
-
|
|
2672
|
+
// Cytoscape's y grows downward; positions are stored y-up (the exporter flips back)
|
|
2673
|
+
point[1] = flipY(position.y);
|
|
2673
2674
|
ctx.nodes.set(positionColumn, index, point);
|
|
2674
2675
|
} else {
|
|
2675
2676
|
report.error(
|