@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.
Files changed (42) hide show
  1. package/README.md +17 -4
  2. package/dist/chunks/{importer-BELrICWM.js → importer-BOxwmef_.js} +352 -19
  3. package/dist/chunks/importer-BOxwmef_.js.map +1 -0
  4. package/dist/graph-io.js +2 -2
  5. package/dist/graph-io.js.map +1 -1
  6. package/dist/json.js +1 -1
  7. package/dist/src/formats/json/dialect.d.ts +21 -4
  8. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  9. package/dist/src/formats/json/dialect.js +29 -4
  10. package/dist/src/formats/json/dialect.js.map +1 -1
  11. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  12. package/dist/src/formats/json/exporter.js +27 -7
  13. package/dist/src/formats/json/exporter.js.map +1 -1
  14. package/dist/src/formats/json/importer.d.ts +17 -4
  15. package/dist/src/formats/json/importer.d.ts.map +1 -1
  16. package/dist/src/formats/json/importer.js +356 -8
  17. package/dist/src/formats/json/importer.js.map +1 -1
  18. package/dist/src/formats/json/index.d.ts +1 -1
  19. package/dist/src/formats/json/index.d.ts.map +1 -1
  20. package/dist/src/formats/json/index.js +1 -1
  21. package/dist/src/formats/json/index.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/sniff.d.ts +3 -3
  26. package/dist/src/sniff.d.ts.map +1 -1
  27. package/dist/src/sniff.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/common/escape.ts +5 -4
  30. package/src/formats/json/dialect.ts +40 -6
  31. package/src/formats/json/exporter.ts +28 -7
  32. package/src/formats/json/importer.ts +411 -21
  33. package/src/formats/json/index.ts +7 -1
  34. package/src/formats/neo4j/exporter.ts +43 -6
  35. package/src/formats/neo4j/importer.ts +56 -11
  36. package/src/formats/neo4j/index.ts +10 -3
  37. package/src/formats/pajek/exporter.ts +3 -1
  38. package/src/formats/pajek/importer.ts +284 -60
  39. package/src/formats/pajek/syntax.ts +98 -12
  40. package/src/index.ts +2 -0
  41. package/src/sniff.ts +3 -3
  42. 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`; else node-link). Pure: the importer wraps it with its issue
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): JsonDialect | null {
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, "nodes") && !hasKey(root, "edges") && !hasKey(root, "links")) {
121
- return null;
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<JsonDialect, boolean>> = Object.freeze({
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" : JSON.stringify(ids.idOf(index));
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
- return Number.isFinite(weights.value(e)) ? weights.text(e) : "null";
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
- return typeof id === "number" && Object.is(id, -0) ? "0" : JSON.stringify(id);
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
  /**