@graphty/graph-io 0.3.3 → 0.3.4
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 +17 -4
- package/dist/chunks/{importer-BELrICWM.js → importer-BOxwmef_.js} +352 -19
- package/dist/chunks/importer-BOxwmef_.js.map +1 -0
- package/dist/graph-io.js +2 -2
- package/dist/graph-io.js.map +1 -1
- package/dist/json.js +1 -1
- package/dist/src/formats/json/dialect.d.ts +21 -4
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +29 -4
- 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 +27 -7
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +17 -4
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +356 -8
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +1 -1
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +1 -1
- package/dist/src/formats/json/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/sniff.d.ts +3 -3
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js.map +1 -1
- package/package.json +1 -1
- package/src/common/escape.ts +5 -4
- package/src/formats/json/dialect.ts +40 -6
- package/src/formats/json/exporter.ts +28 -7
- package/src/formats/json/importer.ts +411 -21
- package/src/formats/json/index.ts +7 -1
- package/src/formats/neo4j/exporter.ts +43 -6
- package/src/formats/neo4j/importer.ts +56 -11
- package/src/formats/neo4j/index.ts +10 -3
- package/src/formats/pajek/exporter.ts +3 -1
- package/src/formats/pajek/importer.ts +284 -60
- package/src/formats/pajek/syntax.ts +98 -12
- package/src/index.ts +2 -0
- package/src/sniff.ts +3 -3
- package/dist/chunks/importer-BELrICWM.js.map +0 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* JSON Graph Format v2 (`graph.nodes` keyed by id, per-edge `directed`, hyperedges), Cytoscape.js
|
|
10
10
|
* elements (`data.id` / `data.source` / `data.target`, `position`, `classes`, `data.parent`),
|
|
11
11
|
* graphology serialisation (`key` / `attributes`, `undirected` edges, `options.type` / `multi`) and
|
|
12
|
-
* vis.js (`from` / `to`)
|
|
12
|
+
* vis.js (`from` / `to`); read only: NetworkX adjacency_data (`nodes` + `adjacency`) and tree_data
|
|
13
|
+
* (nested `id` / `children`).
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { type GraphMeta } from "@graphty/graph-format";
|
|
@@ -30,6 +31,20 @@ export const JSON_DIALECTS: readonly JsonDialect[] = Object.freeze([
|
|
|
30
31
|
"vis",
|
|
31
32
|
]);
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The dialects the importer reads: every JsonDialect plus two it only reads, NetworkX
|
|
36
|
+
* adjacency_data (`nodes` plus one neighbour list per node under `adjacency`) and tree_data (a
|
|
37
|
+
* nested `id` / `children` record). The exporter writes neither; write node-link instead.
|
|
38
|
+
*/
|
|
39
|
+
export type JsonImportDialect = JsonDialect | "adjacency" | "tree";
|
|
40
|
+
|
|
41
|
+
/** Every dialect the importer reads, for option checking and messages. */
|
|
42
|
+
export const JSON_IMPORT_DIALECTS: readonly JsonImportDialect[] = Object.freeze([
|
|
43
|
+
...JSON_DIALECTS,
|
|
44
|
+
"adjacency",
|
|
45
|
+
"tree",
|
|
46
|
+
]);
|
|
47
|
+
|
|
33
48
|
/** The key under `meta.extra` that holds the shape record (design section 8.5). */
|
|
34
49
|
export const META_KEY = "json";
|
|
35
50
|
|
|
@@ -90,17 +105,28 @@ export function isJsonDialect(value: unknown): value is JsonDialect {
|
|
|
90
105
|
return typeof value === "string" && (JSON_DIALECTS as readonly string[]).includes(value);
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Whether a text names a dialect the importer reads.
|
|
110
|
+
* @param value - any value
|
|
111
|
+
* @returns true for one of JSON_IMPORT_DIALECTS
|
|
112
|
+
*/
|
|
113
|
+
export function isJsonImportDialect(value: unknown): value is JsonImportDialect {
|
|
114
|
+
return typeof value === "string" && (JSON_IMPORT_DIALECTS as readonly string[]).includes(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
/**
|
|
94
118
|
* The dialect of a parsed JSON document, by the shape rules of design section 8.2 (Cytoscape:
|
|
95
119
|
* `elements` or a top-level array of `{ data }` elements; JGF: `graph.nodes` / `graph.edges` or
|
|
96
120
|
* `graphs[]`; graphology: `options.type` / `options.multi`, `key` nodes without `id`, edges with
|
|
97
121
|
* `undirected` or an `attributes` record; vis: edges with `from` / `to`; d3: `links` without
|
|
98
|
-
* `directed` / `multigraph` / `graph`;
|
|
122
|
+
* `directed` / `multigraph` / `graph`; NetworkX adjacency_data: `nodes` and `adjacency` without
|
|
123
|
+
* `links` / `edges`; NetworkX tree_data: `children` without `nodes` / `links` / `edges`; else
|
|
124
|
+
* node-link). Pure: the importer wraps it with its issue
|
|
99
125
|
* codes, the registry's sniff() uses it on a head that parses as a whole document.
|
|
100
126
|
* @param root - the parsed document
|
|
101
127
|
* @returns the dialect, or null when the document is not a graph document in any dialect
|
|
102
128
|
*/
|
|
103
|
-
export function sniffJsonDialect(root: unknown):
|
|
129
|
+
export function sniffJsonDialect(root: unknown): JsonImportDialect | null {
|
|
104
130
|
if (Array.isArray(root)) {
|
|
105
131
|
const first = firstJsonObject(root);
|
|
106
132
|
return root.length === 0 || (first !== null && isJsonObject(first.data)) ? "cytoscape" : null;
|
|
@@ -117,8 +143,13 @@ export function sniffJsonDialect(root: unknown): JsonDialect | null {
|
|
|
117
143
|
if (Array.isArray(root.graphs)) {
|
|
118
144
|
return "jgf";
|
|
119
145
|
}
|
|
120
|
-
if (!hasKey(root, "
|
|
121
|
-
|
|
146
|
+
if (!hasKey(root, "edges") && !hasKey(root, "links")) {
|
|
147
|
+
if (hasKey(root, "nodes") && hasKey(root, "adjacency")) {
|
|
148
|
+
return "adjacency";
|
|
149
|
+
}
|
|
150
|
+
if (!hasKey(root, "nodes")) {
|
|
151
|
+
return hasKey(root, "children") ? "tree" : null;
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
const firstNode = firstJsonObject(root.nodes);
|
|
124
155
|
const firstEdge = firstJsonObject(hasKey(root, "edges") ? root.edges : root.links);
|
|
@@ -182,13 +213,16 @@ export function shapeMetaOf(meta: GraphMeta): JsonShapeMeta {
|
|
|
182
213
|
}
|
|
183
214
|
|
|
184
215
|
/** The direction a dialect assumes when the file declares none (design section 8.4, `defaultDirected`). */
|
|
185
|
-
export const DIALECT_DEFAULT_DIRECTED: Readonly<Record<
|
|
216
|
+
export const DIALECT_DEFAULT_DIRECTED: Readonly<Record<JsonImportDialect, boolean>> = Object.freeze({
|
|
186
217
|
"node-link": false,
|
|
187
218
|
d3: false,
|
|
188
219
|
jgf: true,
|
|
189
220
|
cytoscape: true,
|
|
190
221
|
graphology: true,
|
|
191
222
|
vis: false,
|
|
223
|
+
// networkx adjacency_data declares `directed`; tree_graph always builds a DiGraph
|
|
224
|
+
adjacency: false,
|
|
225
|
+
tree: true,
|
|
192
226
|
});
|
|
193
227
|
|
|
194
228
|
/**
|
|
@@ -280,11 +280,26 @@ function numberText(value: number, f32: boolean, nonfinite: Counter): string {
|
|
|
280
280
|
nonfinite.count++;
|
|
281
281
|
return "null";
|
|
282
282
|
}
|
|
283
|
-
if (f32) {
|
|
284
|
-
return formatF32(value);
|
|
285
|
-
}
|
|
286
283
|
// -0 is a JSON number too ("-0"); JSON.parse reads it back as -0
|
|
287
|
-
return formatF64(value);
|
|
284
|
+
return exponentIfUnsafe(f32 ? formatF32(value) : formatF64(value));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* An integer text beyond 2^53 in exponent form (`100000000000000000000` -> `1e+20`, the same
|
|
289
|
+
* digits): the importer reads an integer literal that large as its exact digits (a string), so
|
|
290
|
+
* a number must not be written as one.
|
|
291
|
+
* @param text - the shortest decimal text of a finite number
|
|
292
|
+
* @returns the text, or its exponent form for an integer literal of 16 or more digits
|
|
293
|
+
*/
|
|
294
|
+
function exponentIfUnsafe(text: string): string {
|
|
295
|
+
const match = /^(-?)([0-9]{16,})$/.exec(text);
|
|
296
|
+
if (match === null || Number.isSafeInteger(Number(text))) {
|
|
297
|
+
return text;
|
|
298
|
+
}
|
|
299
|
+
const [, sign, digits] = match;
|
|
300
|
+
const mantissa = digits.replace(/0+$/, "");
|
|
301
|
+
const fraction = mantissa.length > 1 ? `.${mantissa.slice(1)}` : "";
|
|
302
|
+
return `${sign}${mantissa[0]}${fraction}e+${digits.length - 1}`;
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
/**
|
|
@@ -315,6 +330,8 @@ function valueText(value: unknown, f32: boolean, nonfinite: Counter): string {
|
|
|
315
330
|
const items = Array.from(value as ArrayLike<unknown>, (item) => valueText(item, f32, nonfinite));
|
|
316
331
|
return `[${items.join(",")}]`;
|
|
317
332
|
}
|
|
333
|
+
// ponytail: numbers nested in a json object are written by JSON.stringify, so an integral one
|
|
334
|
+
// beyond 2^53 re-imports as its digit text; walk the object here if that ever matters
|
|
318
335
|
const text = JSON.stringify(value);
|
|
319
336
|
return text === undefined ? "null" : text;
|
|
320
337
|
}
|
|
@@ -336,7 +353,7 @@ function cellText(column: Column, row: number, ids: GraphSnapshot["ids"], nonfin
|
|
|
336
353
|
case "u32":
|
|
337
354
|
if (column.meta.refersTo === "node" && column.meta.components === 1) {
|
|
338
355
|
const index = column.data[row];
|
|
339
|
-
return index === INVALID_INDEX ? "null" :
|
|
356
|
+
return index === INVALID_INDEX ? "null" : idText(ids.idOf(index));
|
|
340
357
|
}
|
|
341
358
|
return valueText(column.value(row), false, nonfinite);
|
|
342
359
|
case "f32":
|
|
@@ -839,7 +856,8 @@ function planWeights(ctx: PlanContext, edges: readonly ColumnPlan[]): (e: number
|
|
|
839
856
|
if (!weights.isExplicit(e)) {
|
|
840
857
|
return null;
|
|
841
858
|
}
|
|
842
|
-
|
|
859
|
+
const text = Number.isFinite(weights.value(e)) ? weights.text(e) : "null";
|
|
860
|
+
return text === null ? null : exponentIfUnsafe(text);
|
|
843
861
|
};
|
|
844
862
|
}
|
|
845
863
|
|
|
@@ -1093,7 +1111,10 @@ function anySet(plans: readonly ColumnPlan[], row: number, slots: ReadonlySet<Sl
|
|
|
1093
1111
|
* @returns the text
|
|
1094
1112
|
*/
|
|
1095
1113
|
function idText(id: NodeId): string {
|
|
1096
|
-
|
|
1114
|
+
if (typeof id === "number") {
|
|
1115
|
+
return Object.is(id, -0) ? "0" : exponentIfUnsafe(String(id));
|
|
1116
|
+
}
|
|
1117
|
+
return JSON.stringify(id);
|
|
1097
1118
|
}
|
|
1098
1119
|
|
|
1099
1120
|
/**
|