@loaders.gl/graphs 4.5.3 → 5.0.0-alpha.10

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 (77) hide show
  1. package/LICENSE +14 -3
  2. package/README.md +122 -10
  3. package/dist/dot-loader-types.d.ts +40 -3
  4. package/dist/dot-loader-types.d.ts.map +1 -1
  5. package/dist/dot-loader-types.js +8 -2
  6. package/dist/dot-loader-types.js.map +1 -1
  7. package/dist/dot-loader.cjs +1449 -108
  8. package/dist/dot-loader.cjs.map +3 -4
  9. package/dist/dot-loader.d.ts +11 -6
  10. package/dist/dot-loader.d.ts.map +1 -1
  11. package/dist/dot-loader.js +3 -3
  12. package/dist/dot-loader.js.map +1 -1
  13. package/dist/dot-types.d.ts +6 -1
  14. package/dist/dot-types.d.ts.map +1 -1
  15. package/dist/gexf-loader-types.d.ts +53 -0
  16. package/dist/gexf-loader-types.d.ts.map +1 -0
  17. package/dist/gexf-loader-types.js +26 -0
  18. package/dist/gexf-loader-types.js.map +1 -0
  19. package/dist/gexf-loader.cjs +1304 -0
  20. package/dist/gexf-loader.cjs.map +6 -0
  21. package/dist/gexf-loader.d.ts +24 -0
  22. package/dist/gexf-loader.d.ts.map +1 -0
  23. package/dist/gexf-loader.js +13 -0
  24. package/dist/gexf-loader.js.map +1 -0
  25. package/dist/gexf-types.d.ts +16 -0
  26. package/dist/gexf-types.d.ts.map +1 -0
  27. package/dist/gexf-types.js +5 -0
  28. package/dist/gexf-types.js.map +1 -0
  29. package/dist/graph-types.d.ts +28 -0
  30. package/dist/graph-types.d.ts.map +1 -1
  31. package/dist/graphml-loader-types.d.ts +39 -3
  32. package/dist/graphml-loader-types.d.ts.map +1 -1
  33. package/dist/graphml-loader-types.js +8 -2
  34. package/dist/graphml-loader-types.js.map +1 -1
  35. package/dist/graphml-loader.cjs +1006 -61
  36. package/dist/graphml-loader.cjs.map +3 -4
  37. package/dist/graphml-loader.d.ts +11 -6
  38. package/dist/graphml-loader.d.ts.map +1 -1
  39. package/dist/graphml-loader.js +3 -3
  40. package/dist/graphml-loader.js.map +1 -1
  41. package/dist/index.cjs +1452 -470
  42. package/dist/index.cjs.map +3 -4
  43. package/dist/index.d.ts +9 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/build-graph-tables.d.ts +4 -0
  48. package/dist/lib/build-graph-tables.d.ts.map +1 -0
  49. package/dist/lib/build-graph-tables.js +143 -0
  50. package/dist/lib/build-graph-tables.js.map +1 -0
  51. package/dist/lib/parse-dot.d.ts +3 -1
  52. package/dist/lib/parse-dot.d.ts.map +1 -1
  53. package/dist/lib/parse-dot.js +41 -24
  54. package/dist/lib/parse-dot.js.map +1 -1
  55. package/dist/lib/parse-gexf.d.ts +5 -0
  56. package/dist/lib/parse-gexf.d.ts.map +1 -0
  57. package/dist/lib/parse-gexf.js +380 -0
  58. package/dist/lib/parse-gexf.js.map +1 -0
  59. package/dist/lib/parse-graphml.d.ts +2 -1
  60. package/dist/lib/parse-graphml.d.ts.map +1 -1
  61. package/dist/lib/parse-graphml.js +50 -27
  62. package/dist/lib/parse-graphml.js.map +1 -1
  63. package/package.json +15 -6
  64. package/src/dot-loader-types.ts +21 -4
  65. package/src/dot-loader.ts +7 -4
  66. package/src/dot-types.ts +7 -1
  67. package/src/gexf-loader-types.ts +41 -0
  68. package/src/gexf-loader.ts +17 -0
  69. package/src/gexf-types.ts +21 -0
  70. package/src/graph-types.ts +35 -0
  71. package/src/graphml-loader-types.ts +20 -4
  72. package/src/graphml-loader.ts +7 -4
  73. package/src/index.ts +11 -2
  74. package/src/lib/build-graph-tables.ts +208 -0
  75. package/src/lib/parse-dot.ts +47 -24
  76. package/src/lib/parse-gexf.ts +388 -0
  77. package/src/lib/parse-graphml.ts +86 -34
@@ -0,0 +1,208 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {
6
+ Bool,
7
+ Float32,
8
+ Float64,
9
+ Int32,
10
+ Int64,
11
+ Utf8,
12
+ Struct,
13
+ List,
14
+ Field,
15
+ Table,
16
+ vectorFromArray
17
+ } from 'apache-arrow';
18
+ import type {DataType, Vector} from 'apache-arrow';
19
+ import {convertArrowToSchema} from '@loaders.gl/schema-utils';
20
+ import type {ArrowTable, ObjectRowTable} from '@loaders.gl/schema';
21
+ import type {
22
+ GraphNode,
23
+ GraphEdge,
24
+ GraphOutput,
25
+ GraphShape,
26
+ GraphAttributeSchemas,
27
+ GraphAttributeType
28
+ } from '../graph-types';
29
+
30
+ /** Constructs named tables without materializing an intermediate plain graph. */
31
+ export function buildGraphTables(
32
+ nodes: Iterable<GraphNode>,
33
+ edges: Iterable<GraphEdge>,
34
+ shape: GraphShape,
35
+ declarations?: GraphAttributeSchemas
36
+ ): GraphOutput {
37
+ if (shape === 'plain-graph-data') {
38
+ return {shape, nodes: Array.from(nodes), edges: Array.from(edges)};
39
+ }
40
+ if (shape !== 'arrow-table' && shape !== 'object-row-table') {
41
+ throw new Error(`Unsupported graph shape: ${shape}`);
42
+ }
43
+ const nodeTable = buildGraphTable(nodes, false, shape, declarations?.nodes);
44
+ const edgeTable = buildGraphTable(edges, true, shape, declarations?.edges);
45
+ // Both tables always have the same representation.
46
+ return {
47
+ shape: 'tables',
48
+ tables: [
49
+ {name: 'nodes', table: nodeTable},
50
+ {name: 'edges', table: edgeTable}
51
+ ]
52
+ } as GraphOutput;
53
+ }
54
+
55
+ /** Builds structural columns and a collision-free application attribute struct. */
56
+ function buildGraphTable(
57
+ records: Iterable<GraphNode | GraphEdge>,
58
+ isEdge: boolean,
59
+ shape: 'arrow-table' | 'object-row-table',
60
+ declarations?: Map<string, GraphAttributeType>
61
+ ): ArrowTable | ObjectRowTable {
62
+ const columnNames = isEdge
63
+ ? ['id', 'sourceId', 'targetId', 'directed', 'label', 'attributes']
64
+ : ['id', 'label', 'attributes'];
65
+ const columns = new Map<string, unknown[]>(columnNames.map(name => [name, []]));
66
+ for (const record of records) {
67
+ for (const name of columnNames) {
68
+ const value = (record as unknown as Record<string, unknown>)[name];
69
+ columns
70
+ .get(name)!
71
+ .push(
72
+ name === 'id' || name === 'sourceId' || name === 'targetId'
73
+ ? String(value)
74
+ : (value ?? null)
75
+ );
76
+ }
77
+ }
78
+ if (shape === 'object-row-table') {
79
+ return {
80
+ shape,
81
+ data: columns
82
+ .get('id')!
83
+ .map((_, index) =>
84
+ Object.fromEntries(columnNames.map(name => [name, columns.get(name)![index]]))
85
+ )
86
+ };
87
+ }
88
+ const vectors: Record<string, Vector> = Object.create(null);
89
+ for (const [name, values] of columns) {
90
+ const type =
91
+ name === 'attributes'
92
+ ? inferAttributeType(values, declarations)
93
+ : name === 'directed'
94
+ ? new Bool()
95
+ : new Utf8();
96
+ vectors[name] = vectorFromArray(
97
+ values.map(value => normalizeValue(value, type)),
98
+ type
99
+ );
100
+ }
101
+ const data = new Table(vectors);
102
+ return {shape, schema: convertArrowToSchema(data.schema), data};
103
+ }
104
+
105
+ /** Infers a struct using the union of declared and observed application keys. */
106
+ function inferAttributeType(
107
+ values: unknown[],
108
+ declarations?: Map<string, GraphAttributeType>
109
+ ): Struct {
110
+ const names = new Set(declarations?.keys());
111
+ for (const value of values) {
112
+ if (value && typeof value === 'object') {
113
+ for (const name of Object.keys(value)) {
114
+ names.add(name);
115
+ }
116
+ }
117
+ }
118
+ return new Struct(
119
+ Array.from(names, name => {
120
+ const fieldValues = values.map(
121
+ value => (value as Record<string, unknown> | null)?.[name] ?? null
122
+ );
123
+ return new Field(name, inferValueType(fieldValues, declarations?.get(name)), true);
124
+ })
125
+ );
126
+ }
127
+
128
+ /** Preserves declared scalar types where valid and infers nested DOT attributes. */
129
+ function inferValueType(values: unknown[], declaration?: GraphAttributeType): DataType {
130
+ const present = values.filter(value => value !== null && value !== undefined);
131
+ if (declaration && typeof declaration === 'object') {
132
+ if (!present.every(value => Array.isArray(value))) return new Utf8();
133
+ return new List(
134
+ new Field(
135
+ 'item',
136
+ inferValueType(
137
+ present.flatMap(value => value as unknown[]),
138
+ declaration.list
139
+ ),
140
+ true
141
+ )
142
+ );
143
+ }
144
+ if (declaration) {
145
+ const declaredTypes = {
146
+ boolean: new Bool(),
147
+ int: new Int32(),
148
+ long: new Int64(),
149
+ float: new Float32(),
150
+ double: new Float64(),
151
+ string: new Utf8()
152
+ };
153
+ const valid = present.every(value => {
154
+ switch (declaration) {
155
+ case 'boolean':
156
+ return typeof value === 'boolean';
157
+ case 'int':
158
+ return (
159
+ typeof value === 'number' &&
160
+ Number.isInteger(value) &&
161
+ value >= -2147483648 &&
162
+ value <= 2147483647
163
+ );
164
+ case 'long':
165
+ return typeof value === 'bigint' && value >= -(2n ** 63n) && value < 2n ** 63n;
166
+ case 'float':
167
+ case 'double':
168
+ return typeof value === 'number';
169
+ case 'string':
170
+ return typeof value === 'string';
171
+ }
172
+ });
173
+ return valid ? declaredTypes[declaration] : new Utf8();
174
+ }
175
+ if (present.length && present.every(value => typeof value === 'boolean')) return new Bool();
176
+ if (present.length && present.every(value => typeof value === 'number')) return new Float64();
177
+ if (present.length && present.every(value => Array.isArray(value))) {
178
+ return new List(
179
+ new Field('item', inferValueType(present.flatMap(value => value as unknown[])), true)
180
+ );
181
+ }
182
+ if (
183
+ present.length &&
184
+ present.every(value => typeof value === 'object' && !Array.isArray(value))
185
+ ) {
186
+ return inferAttributeType(present);
187
+ }
188
+ return new Utf8();
189
+ }
190
+
191
+ /** Normalizes missing fields and mixed values for the selected Arrow type. */
192
+ function normalizeValue(value: unknown, type: DataType): unknown {
193
+ if (value === null || value === undefined) return null;
194
+ if (type instanceof Struct) {
195
+ return Object.fromEntries(
196
+ type.children.map(field => [
197
+ field.name,
198
+ normalizeValue((value as Record<string, unknown>)[field.name], field.type)
199
+ ])
200
+ );
201
+ }
202
+ if (type instanceof List)
203
+ return (value as unknown[]).map(item => normalizeValue(item, type.valueType));
204
+ if (type instanceof Utf8 && typeof value !== 'string') {
205
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
206
+ }
207
+ return value;
208
+ }
@@ -2,7 +2,9 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {DOTGraphData} from '../dot-types';
5
+ import type {DOTGraphData, DOTOutput} from '../dot-types';
6
+ import type {GraphShape, GraphNode, GraphEdge} from '../graph-types';
7
+ import {buildGraphTables} from './build-graph-tables';
6
8
 
7
9
  /** Internal DOT parsing representation. */
8
10
  type DOTAttributeMap = Record<string, unknown>;
@@ -62,25 +64,45 @@ type DOTParseResult = {
62
64
  };
63
65
 
64
66
  /** Parses DOT source into plain graph records without a visualization dependency. */
65
- export function parseDOT(input: string): DOTGraphData {
67
+ export function parseDOT(input: string): DOTGraphData;
68
+ export function parseDOT(input: string, shape: GraphShape): DOTOutput;
69
+ export function parseDOT(input: string, shape: GraphShape = 'plain-graph-data'): DOTOutput {
66
70
  const parsed = parseDOTDocument(input);
67
- const subgraphs = Array.from(parsed.subgraphs.values(), (subgraph) => ({
68
- ...subgraph,
69
- attributes: {...subgraph.attributes}
70
- }));
71
71
  return {
72
- shape: 'plain-graph-data',
73
- nodes: Array.from(parsed.nodes.values(), (node) => ({
72
+ ...buildGraphTables(iterateNodes(parsed), iterateEdges(parsed), shape),
73
+ metadata: {
74
+ id: parsed.id,
75
+ directed: parsed.directed,
76
+ strict: parsed.strict,
77
+ attributes: {...parsed.graphAttributes},
78
+ subgraphs: Array.from(parsed.subgraphs.values(), subgraph => ({
79
+ ...subgraph,
80
+ attributes: {...subgraph.attributes}
81
+ }))
82
+ }
83
+ };
84
+ }
85
+
86
+ /** Emits finalized nodes without creating an intermediate record array. */
87
+ function* iterateNodes(parsed: DOTParseResult): Iterable<GraphNode> {
88
+ for (const node of parsed.nodes.values()) {
89
+ yield {
74
90
  id: node.id,
75
91
  ...(typeof node.attributes.label === 'string' ? {label: node.attributes.label} : {}),
76
92
  attributes: {
77
93
  ...node.attributes,
78
94
  ...(node.subgraphs.size
79
- ? {subgraphs: Array.from(node.subgraphs, (id) => describeSubgraph(id, parsed.subgraphs))}
95
+ ? {subgraphs: Array.from(node.subgraphs, id => describeSubgraph(id, parsed.subgraphs))}
80
96
  : {})
81
97
  }
82
- })),
83
- edges: parsed.edges.map((edge) => ({
98
+ };
99
+ }
100
+ }
101
+
102
+ /** Emits coalesced edges in document order. */
103
+ function* iterateEdges(parsed: DOTParseResult): Iterable<GraphEdge> {
104
+ for (const edge of parsed.edges) {
105
+ yield {
84
106
  id: edge.id,
85
107
  sourceId: edge.sourceId,
86
108
  targetId: edge.targetId,
@@ -89,18 +111,11 @@ export function parseDOT(input: string): DOTGraphData {
89
111
  attributes: {
90
112
  ...edge.attributes,
91
113
  ...(edge.subgraphs.length
92
- ? {subgraphs: edge.subgraphs.map((id) => describeSubgraph(id, parsed.subgraphs))}
114
+ ? {subgraphs: edge.subgraphs.map(id => describeSubgraph(id, parsed.subgraphs))}
93
115
  : {})
94
116
  }
95
- })),
96
- metadata: {
97
- id: parsed.id,
98
- directed: parsed.directed,
99
- strict: parsed.strict,
100
- attributes: {...parsed.graphAttributes},
101
- subgraphs
102
- }
103
- };
117
+ };
118
+ }
104
119
  }
105
120
 
106
121
  /** Copies a subgraph descriptor into node or edge attributes. */
@@ -393,6 +408,10 @@ class DOTParser {
393
408
  const existingEdge = this.result.strict ? this.strictEdges.get(strictKey) : undefined;
394
409
  if (existingEdge) {
395
410
  existingEdge.attributes = {...existingEdge.attributes, ...explicitAttributes};
411
+ const explicitId = explicitAttributes.id ?? explicitAttributes.Id ?? explicitAttributes.ID;
412
+ if (typeof explicitId === 'string' || typeof explicitId === 'number') {
413
+ existingEdge.id = String(explicitId);
414
+ }
396
415
  existingEdge.directed = deriveDirectedFlag(existingEdge.attributes, directed);
397
416
  existingEdge.subgraphs = Array.from(new Set([...existingEdge.subgraphs, ...membership]));
398
417
  continue;
@@ -496,7 +515,7 @@ class DOTParser {
496
515
  node = {id, attributes: {...defaults}, subgraphs: new Set()};
497
516
  this.result.nodes.set(id, node);
498
517
  }
499
- membership.forEach((subgraphId) => node.subgraphs.add(subgraphId));
518
+ membership.forEach(subgraphId => node.subgraphs.add(subgraphId));
500
519
  return node;
501
520
  }
502
521
 
@@ -742,13 +761,17 @@ function readQuotedString(input: string, startIndex: number): {value: string; ne
742
761
  throw new Error('Unterminated string literal in DOT source.');
743
762
  }
744
763
 
745
- /** Decodes one quoted-string escape. */
764
+ /** Decodes one quoted-string escape or removes a physical line continuation. */
746
765
  function readEscapedCharacter(
747
766
  input: string,
748
767
  startIndex: number
749
768
  ): {value: string; nextIndex: number} {
750
769
  const next = input[startIndex];
751
770
  switch (next) {
771
+ case '\n':
772
+ return {value: '', nextIndex: startIndex + 1};
773
+ case '\r':
774
+ return {value: '', nextIndex: startIndex + (input[startIndex + 1] === '\n' ? 2 : 1)};
752
775
  case 'n':
753
776
  case 'l':
754
777
  case 'L':
@@ -765,7 +788,7 @@ function readEscapedCharacter(
765
788
  if (typeof next === 'undefined') {
766
789
  throw new Error('Unterminated escape sequence in DOT source.');
767
790
  }
768
- return {value: next, nextIndex: startIndex + 1};
791
+ return {value: `\\${next}`, nextIndex: startIndex + 1};
769
792
  }
770
793
  }
771
794
  }