@graphty/cytoscape-extensions 0.0.0-placeholder.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +10 -0
- package/README.md +73 -7
- package/package.json +124 -14
- package/src/algorithm-options.ts +277 -0
- package/src/algorithms.ts +1911 -0
- package/src/elements.ts +357 -0
- package/src/gpu-platform-browser.ts +29 -0
- package/src/gpu-platform-node.ts +35 -0
- package/src/gpu.ts +407 -0
- package/src/graph-data.ts +165 -0
- package/src/index.ts +73 -0
- package/src/io.ts +205 -0
- package/src/layouts.ts +886 -0
- package/src/lazy.ts +35 -0
- package/src/samples.ts +273 -0
- package/src/snapshot.ts +281 -0
- package/src/webgpu-provider.ts +67 -0
package/src/io.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@graphty/cytoscape-extensions/io`: reading and writing Cytoscape graphs in the file formats of @graphty/graph-io.
|
|
3
|
+
* cy.graphtyImport() and cy.graphtyExport() load this module on their first call, so a page that never imports or
|
|
4
|
+
* exports a file never downloads the parsers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
checkExport,
|
|
9
|
+
type ExportGraphOptions,
|
|
10
|
+
exportGraphToString,
|
|
11
|
+
ImportError,
|
|
12
|
+
importGraph,
|
|
13
|
+
type ImportGraphOptions,
|
|
14
|
+
type ImportInput,
|
|
15
|
+
type ImportIssue,
|
|
16
|
+
type ImportReport,
|
|
17
|
+
type LossNote,
|
|
18
|
+
} from "@graphty/graph-io";
|
|
19
|
+
import type { Collection, ElementDefinition } from "cytoscape";
|
|
20
|
+
|
|
21
|
+
import { elementsToSnapshot, flipY, snapshotToElements } from "./elements.js";
|
|
22
|
+
|
|
23
|
+
/** One thing graphtyExport's format cannot hold: `code`, `message`, the data field (`column`) and how many values. */
|
|
24
|
+
export type { LossNote } from "@graphty/graph-io";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The formats graphtyImport reads; "auto" sniffs the format from the content (and `filename` when given). "cys" is a
|
|
28
|
+
* Cytoscape desktop session file, which must be passed as bytes.
|
|
29
|
+
*/
|
|
30
|
+
export type ImportFormat =
|
|
31
|
+
| "auto"
|
|
32
|
+
| "gexf"
|
|
33
|
+
| "graphml"
|
|
34
|
+
| "gml"
|
|
35
|
+
| "dot"
|
|
36
|
+
| "pajek"
|
|
37
|
+
| "csv"
|
|
38
|
+
| "json"
|
|
39
|
+
| "neo4j"
|
|
40
|
+
| "xgmml"
|
|
41
|
+
| "cx2"
|
|
42
|
+
| "cx"
|
|
43
|
+
| "obo"
|
|
44
|
+
| "cys";
|
|
45
|
+
|
|
46
|
+
/** The formats graphtyExport writes. XGMML is the one Cytoscape desktop opens as a network file. */
|
|
47
|
+
export type ExportFormat = "gexf" | "graphml" | "gml" | "dot" | "pajek" | "csv" | "json" | "neo4j" | "xgmml" | "cx2";
|
|
48
|
+
|
|
49
|
+
/** Options of graphtyImport: graph-io's import options (`filename`, `graphIndex`, format-specific ones, ...). */
|
|
50
|
+
export type ImportOptions = Omit<ImportGraphOptions, "format">;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options of graphtyExport: graph-io's export options for the format, plus whether to write a directed graph.
|
|
54
|
+
* `sanitizeIds` defaults to "mangle" here (graph-io's default is "error"): GML and CX2 take only integer ids, so
|
|
55
|
+
* when a node id is not a plain non-negative integer ("a", "-1", "007") the nodes are numbered and the original
|
|
56
|
+
* kept in an attribute graphtyImport restores. When every id is one ("0", "17"), the ids are written as they are,
|
|
57
|
+
* and "error" accepts them too.
|
|
58
|
+
* JSON is written as Cytoscape JSON (`dialect: "cytoscape"`, the shape `cy.json()` and `cy.add()` use), which keeps
|
|
59
|
+
* edge ids, compound parents and positions; pass `dialect: "node-link"` for the NetworkX shape.
|
|
60
|
+
*/
|
|
61
|
+
export interface ExportOptions extends ExportGraphOptions {
|
|
62
|
+
/** Write the graph as directed. Default false. */
|
|
63
|
+
readonly directed?: boolean | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Called before writing with what the format cannot hold (positions in GraphML, lists and objects in DOT,
|
|
66
|
+
* ...), one note per column and kind of loss; not called when nothing is lost. Without it a loss is silent.
|
|
67
|
+
*/
|
|
68
|
+
readonly onLoss?: ((notes: readonly LossNote[]) => void) | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A file read into Cytoscape element definitions. */
|
|
72
|
+
export interface ImportedElements {
|
|
73
|
+
/** The nodes, then the edges. */
|
|
74
|
+
readonly elements: ElementDefinition[];
|
|
75
|
+
/** Whether the file's graph is directed: pass it as `directed` to the algorithms. */
|
|
76
|
+
readonly directed: boolean;
|
|
77
|
+
/** The format the input was read as. */
|
|
78
|
+
readonly format: string;
|
|
79
|
+
/** graph-io's report: counts, warnings, and what could not be represented. */
|
|
80
|
+
readonly report: ImportReport;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads a graph file into Cytoscape element definitions.
|
|
85
|
+
* @param input - the file as text, bytes, a stream or chunks
|
|
86
|
+
* @param format - the format, or "auto" to sniff it
|
|
87
|
+
* @param options - graph-io's import options
|
|
88
|
+
* @returns the elements, the direction, the format and graph-io's report
|
|
89
|
+
* @throws ImportError (from graph-io) when the file cannot be read, or when it yields no node and reports an error
|
|
90
|
+
*/
|
|
91
|
+
export async function importElements(
|
|
92
|
+
input: ImportInput | ArrayBuffer,
|
|
93
|
+
format: ImportFormat = "auto",
|
|
94
|
+
options: ImportOptions = {},
|
|
95
|
+
): Promise<ImportedElements> {
|
|
96
|
+
// fetch().arrayBuffer() and File.arrayBuffer() give an ArrayBuffer, which graph-io does not take
|
|
97
|
+
const r = await importGraph(input instanceof ArrayBuffer ? new Uint8Array(input) : input, { ...options, format });
|
|
98
|
+
// most text sniffs as CSV, so a file that is not a graph at all reads as an empty one with errors
|
|
99
|
+
if (r.snapshot.nodeCount === 0 && r.report.errorCount > 0) {
|
|
100
|
+
const first = r.report.issues.find((i) => i.severity === "error");
|
|
101
|
+
throw new ImportError(
|
|
102
|
+
`nothing could be read from the input as ${r.format}: ${first?.code ?? "E_IMPORT"}: ${first?.message ?? ""}`,
|
|
103
|
+
r.report,
|
|
104
|
+
{ format: r.format },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const warnings: ImportIssue[] = [];
|
|
108
|
+
const elements = snapshotToElements(
|
|
109
|
+
r.snapshot,
|
|
110
|
+
({ domain, from, to }) =>
|
|
111
|
+
warnings.push({
|
|
112
|
+
category: "coercion",
|
|
113
|
+
severity: "warning",
|
|
114
|
+
code: "W_COLUMN_RENAMED",
|
|
115
|
+
message: `${domain} attribute "${from}" renamed to "${to}": Cytoscape reserves the data field "${from}"`,
|
|
116
|
+
line: null,
|
|
117
|
+
element: from,
|
|
118
|
+
}),
|
|
119
|
+
(id, takenBy) =>
|
|
120
|
+
warnings.push({
|
|
121
|
+
category: "coercion",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
code: "W_EDGE_ID_DROPPED",
|
|
124
|
+
message: `edge id "${id}" is already the id of ${takenBy === "node" ? "a node" : "an earlier edge"}; Cytoscape gives this edge a new id`,
|
|
125
|
+
line: null,
|
|
126
|
+
element: id,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
// graph-io stores positions with y growing upward; Cytoscape's y grows downward
|
|
130
|
+
for (const el of elements) {
|
|
131
|
+
if (el.position !== undefined) {
|
|
132
|
+
el.position.y = flipY(el.position.y);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const report =
|
|
136
|
+
warnings.length === 0
|
|
137
|
+
? r.report
|
|
138
|
+
: {
|
|
139
|
+
...r.report,
|
|
140
|
+
issues: [...r.report.issues, ...warnings],
|
|
141
|
+
warningCount: r.report.warningCount + warnings.length,
|
|
142
|
+
};
|
|
143
|
+
return { elements, directed: r.snapshot.directed, format: r.format, report };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether a graph-io loss note is a loss for a Cytoscape graph. graph-io's notes describe its own snapshot read back
|
|
148
|
+
* by itself; graphtyImport restores some of what they report:
|
|
149
|
+
* - W_ID_TEXT_TYPE: every id is turned back into a string;
|
|
150
|
+
* - W_COLUMN_NAME_CHANGED on the edge `id`, the `parent` or the `position` (DOT reads them back as "key",
|
|
151
|
+
* "graphty.parent" and "pos"): they are read back by their role, as the edge id, `data.parent` and the position;
|
|
152
|
+
* - W_ROLE_DROPPED on `label`: the values read back as `data.label`, and Cytoscape has no label role to lose.
|
|
153
|
+
* @param n - the note
|
|
154
|
+
* @returns false for a note about something graphtyImport gives back
|
|
155
|
+
*/
|
|
156
|
+
function lostForCytoscape(n: LossNote): boolean {
|
|
157
|
+
if (n.code === "W_ID_TEXT_TYPE") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (n.code === "W_COLUMN_NAME_CHANGED" && (n.column === "id" || n.column === "parent" || n.column === "position")) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return !(n.code === "W_ROLE_DROPPED" && n.column === "label");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The formats that hold only integer node ids: a graph whose ids are all integers is written with them as they are. */
|
|
167
|
+
const INTEGER_ID_FORMATS: ReadonlySet<ExportFormat> = new Set(["gml", "cx2"]);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Writes a collection as a graph file: every data field, each node's position and compound parent, and each
|
|
171
|
+
* edge's id, as far as the format holds them (`onLoss` hears about the rest).
|
|
172
|
+
* @param eles - the nodes and the edges between them
|
|
173
|
+
* @param format - the format
|
|
174
|
+
* @param options - graph-io's export options for the format, plus `directed`
|
|
175
|
+
* @returns the file's text; rejects (a TypeError) when `eles` is not a collection
|
|
176
|
+
*/
|
|
177
|
+
export async function exportElements(
|
|
178
|
+
eles: Collection,
|
|
179
|
+
format: ExportFormat,
|
|
180
|
+
options: ExportOptions = {},
|
|
181
|
+
): Promise<string> {
|
|
182
|
+
// importElements returns definitions, which this cannot read without a core to build them
|
|
183
|
+
if (typeof (eles as Partial<Collection> | null)?.nodes !== "function") {
|
|
184
|
+
throw new TypeError(
|
|
185
|
+
"exportElements needs a Cytoscape collection; wrap element definitions with cytoscape({ headless: true, elements }).elements()",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const { directed, onLoss, ...rest } = options;
|
|
189
|
+
const snapshot = elementsToSnapshot(eles, {
|
|
190
|
+
directed,
|
|
191
|
+
integerIds: INTEGER_ID_FORMATS.has(format),
|
|
192
|
+
});
|
|
193
|
+
const graphOptions = {
|
|
194
|
+
sanitizeIds: "mangle" as const,
|
|
195
|
+
...(format === "json" ? { dialect: "cytoscape" } : {}),
|
|
196
|
+
...rest,
|
|
197
|
+
};
|
|
198
|
+
if (onLoss !== undefined) {
|
|
199
|
+
const notes = checkExport(snapshot, format, graphOptions).filter(lostForCytoscape);
|
|
200
|
+
if (notes.length > 0) {
|
|
201
|
+
onLoss(notes);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return exportGraphToString(snapshot, format, graphOptions);
|
|
205
|
+
}
|