@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.
- package/LICENSE +14 -3
- package/README.md +122 -10
- package/dist/dot-loader-types.d.ts +40 -3
- package/dist/dot-loader-types.d.ts.map +1 -1
- package/dist/dot-loader-types.js +8 -2
- package/dist/dot-loader-types.js.map +1 -1
- package/dist/dot-loader.cjs +1449 -108
- package/dist/dot-loader.cjs.map +3 -4
- package/dist/dot-loader.d.ts +11 -6
- package/dist/dot-loader.d.ts.map +1 -1
- package/dist/dot-loader.js +3 -3
- package/dist/dot-loader.js.map +1 -1
- package/dist/dot-types.d.ts +6 -1
- package/dist/dot-types.d.ts.map +1 -1
- package/dist/gexf-loader-types.d.ts +53 -0
- package/dist/gexf-loader-types.d.ts.map +1 -0
- package/dist/gexf-loader-types.js +26 -0
- package/dist/gexf-loader-types.js.map +1 -0
- package/dist/gexf-loader.cjs +1304 -0
- package/dist/gexf-loader.cjs.map +6 -0
- package/dist/gexf-loader.d.ts +24 -0
- package/dist/gexf-loader.d.ts.map +1 -0
- package/dist/gexf-loader.js +13 -0
- package/dist/gexf-loader.js.map +1 -0
- package/dist/gexf-types.d.ts +16 -0
- package/dist/gexf-types.d.ts.map +1 -0
- package/dist/gexf-types.js +5 -0
- package/dist/gexf-types.js.map +1 -0
- package/dist/graph-types.d.ts +28 -0
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/graphml-loader-types.d.ts +39 -3
- package/dist/graphml-loader-types.d.ts.map +1 -1
- package/dist/graphml-loader-types.js +8 -2
- package/dist/graphml-loader-types.js.map +1 -1
- package/dist/graphml-loader.cjs +1006 -61
- package/dist/graphml-loader.cjs.map +3 -4
- package/dist/graphml-loader.d.ts +11 -6
- package/dist/graphml-loader.d.ts.map +1 -1
- package/dist/graphml-loader.js +3 -3
- package/dist/graphml-loader.js.map +1 -1
- package/dist/index.cjs +1452 -470
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/build-graph-tables.d.ts +4 -0
- package/dist/lib/build-graph-tables.d.ts.map +1 -0
- package/dist/lib/build-graph-tables.js +143 -0
- package/dist/lib/build-graph-tables.js.map +1 -0
- package/dist/lib/parse-dot.d.ts +3 -1
- package/dist/lib/parse-dot.d.ts.map +1 -1
- package/dist/lib/parse-dot.js +41 -24
- package/dist/lib/parse-dot.js.map +1 -1
- package/dist/lib/parse-gexf.d.ts +5 -0
- package/dist/lib/parse-gexf.d.ts.map +1 -0
- package/dist/lib/parse-gexf.js +380 -0
- package/dist/lib/parse-gexf.js.map +1 -0
- package/dist/lib/parse-graphml.d.ts +2 -1
- package/dist/lib/parse-graphml.d.ts.map +1 -1
- package/dist/lib/parse-graphml.js +50 -27
- package/dist/lib/parse-graphml.js.map +1 -1
- package/package.json +15 -6
- package/src/dot-loader-types.ts +21 -4
- package/src/dot-loader.ts +7 -4
- package/src/dot-types.ts +7 -1
- package/src/gexf-loader-types.ts +41 -0
- package/src/gexf-loader.ts +17 -0
- package/src/gexf-types.ts +21 -0
- package/src/graph-types.ts +35 -0
- package/src/graphml-loader-types.ts +20 -4
- package/src/graphml-loader.ts +7 -4
- package/src/index.ts +11 -2
- package/src/lib/build-graph-tables.ts +208 -0
- package/src/lib/parse-dot.ts +47 -24
- package/src/lib/parse-gexf.ts +388 -0
- 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
|
+
}
|
package/src/lib/parse-dot.ts
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
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,
|
|
95
|
+
? {subgraphs: Array.from(node.subgraphs, id => describeSubgraph(id, parsed.subgraphs))}
|
|
80
96
|
: {})
|
|
81
97
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
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(
|
|
114
|
+
? {subgraphs: edge.subgraphs.map(id => describeSubgraph(id, parsed.subgraphs))}
|
|
93
115
|
: {})
|
|
94
116
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
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(
|
|
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
|
|
791
|
+
return {value: `\\${next}`, nextIndex: startIndex + 1};
|
|
769
792
|
}
|
|
770
793
|
}
|
|
771
794
|
}
|