@loaders.gl/graphs 5.0.0-alpha.9 → 5.0.0-beta.1

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 (78) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +121 -4
  3. package/THIRD-PARTY-NOTICES.md +17 -0
  4. package/dist/dot-loader-types.d.ts +25 -8
  5. package/dist/dot-loader-types.d.ts.map +1 -1
  6. package/dist/dot-loader-types.js +2 -2
  7. package/dist/dot-loader-types.js.map +1 -1
  8. package/dist/dot-loader.cjs +638 -47
  9. package/dist/dot-loader.cjs.map +3 -3
  10. package/dist/dot-loader.d.ts +10 -6
  11. package/dist/dot-loader.d.ts.map +1 -1
  12. package/dist/dot-loader.js +3 -3
  13. package/dist/dot-loader.js.map +1 -1
  14. package/dist/dot-types.d.ts +6 -1
  15. package/dist/dot-types.d.ts.map +1 -1
  16. package/dist/gexf-loader-types.d.ts +53 -0
  17. package/dist/gexf-loader-types.d.ts.map +1 -0
  18. package/dist/gexf-loader-types.js +26 -0
  19. package/dist/gexf-loader-types.js.map +1 -0
  20. package/dist/gexf-loader.cjs +1304 -0
  21. package/dist/gexf-loader.cjs.map +6 -0
  22. package/dist/gexf-loader.d.ts +24 -0
  23. package/dist/gexf-loader.d.ts.map +1 -0
  24. package/dist/gexf-loader.js +13 -0
  25. package/dist/gexf-loader.js.map +1 -0
  26. package/dist/gexf-types.d.ts +16 -0
  27. package/dist/gexf-types.d.ts.map +1 -0
  28. package/dist/gexf-types.js +5 -0
  29. package/dist/gexf-types.js.map +1 -0
  30. package/dist/graph-types.d.ts +28 -0
  31. package/dist/graph-types.d.ts.map +1 -1
  32. package/dist/graphml-loader-types.d.ts +24 -8
  33. package/dist/graphml-loader-types.d.ts.map +1 -1
  34. package/dist/graphml-loader-types.js +2 -2
  35. package/dist/graphml-loader-types.js.map +1 -1
  36. package/dist/graphml-loader.cjs +670 -60
  37. package/dist/graphml-loader.cjs.map +3 -3
  38. package/dist/graphml-loader.d.ts +10 -6
  39. package/dist/graphml-loader.d.ts.map +1 -1
  40. package/dist/graphml-loader.js +3 -3
  41. package/dist/graphml-loader.js.map +1 -1
  42. package/dist/index.cjs +927 -59
  43. package/dist/index.cjs.map +3 -3
  44. package/dist/index.d.ts +7 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/build-graph-tables.d.ts +4 -0
  49. package/dist/lib/build-graph-tables.d.ts.map +1 -0
  50. package/dist/lib/build-graph-tables.js +143 -0
  51. package/dist/lib/build-graph-tables.js.map +1 -0
  52. package/dist/lib/parse-dot.d.ts +3 -1
  53. package/dist/lib/parse-dot.d.ts.map +1 -1
  54. package/dist/lib/parse-dot.js +38 -21
  55. package/dist/lib/parse-dot.js.map +1 -1
  56. package/dist/lib/parse-gexf.d.ts +5 -0
  57. package/dist/lib/parse-gexf.d.ts.map +1 -0
  58. package/dist/lib/parse-gexf.js +380 -0
  59. package/dist/lib/parse-gexf.js.map +1 -0
  60. package/dist/lib/parse-graphml.d.ts +2 -1
  61. package/dist/lib/parse-graphml.d.ts.map +1 -1
  62. package/dist/lib/parse-graphml.js +50 -27
  63. package/dist/lib/parse-graphml.js.map +1 -1
  64. package/package.json +13 -5
  65. package/src/dot-loader-types.ts +15 -5
  66. package/src/dot-loader.ts +7 -4
  67. package/src/dot-types.ts +7 -1
  68. package/src/gexf-loader-types.ts +41 -0
  69. package/src/gexf-loader.ts +17 -0
  70. package/src/gexf-types.ts +21 -0
  71. package/src/graph-types.ts +35 -0
  72. package/src/graphml-loader-types.ts +14 -5
  73. package/src/graphml-loader.ts +7 -4
  74. package/src/index.ts +9 -0
  75. package/src/lib/build-graph-tables.ts +208 -0
  76. package/src/lib/parse-dot.ts +44 -21
  77. package/src/lib/parse-gexf.ts +388 -0
  78. package/src/lib/parse-graphml.ts +86 -34
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loaders.gl/graphs",
3
- "version": "5.0.0-alpha.9",
4
- "description": "Graph format parsers for DOT, GraphML, and related formats",
3
+ "version": "5.0.0-beta.1",
4
+ "description": "Graph format parsers for DOT, GraphML, and GEXF",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -12,6 +12,7 @@
12
12
  "graph",
13
13
  "dot",
14
14
  "graphml",
15
+ "gexf",
15
16
  "parser",
16
17
  "loader"
17
18
  ],
@@ -33,13 +34,20 @@
33
34
  "types": "./dist/dot-loader.d.ts",
34
35
  "import": "./dist/dot-loader.js",
35
36
  "require": "./dist/dot-loader.cjs"
37
+ },
38
+ "./gexf-loader": {
39
+ "types": "./dist/gexf-loader.d.ts",
40
+ "import": "./dist/gexf-loader.js",
41
+ "require": "./dist/gexf-loader.cjs"
36
42
  }
37
43
  },
38
44
  "sideEffects": false,
39
45
  "files": [
40
46
  "src",
41
47
  "dist",
42
- "README.md"
48
+ "README.md",
49
+ "LICENSE",
50
+ "THIRD-PARTY-NOTICES.md"
43
51
  ],
44
52
  "scripts": {
45
53
  "pre-build": "echo \"Nothing to build in @loaders.gl/graphs\""
@@ -47,12 +55,12 @@
47
55
  "peerDependencies": {
48
56
  "@loaders.gl/core": "~5.0.0-alpha.0"
49
57
  },
50
- "gitHead": "f1a6da2c65e4ea06b59c61d1884819b6be577fe3",
58
+ "gitHead": "89312301ed9e8ec89a58501c052a38284d1570c1",
51
59
  "publishConfig": {
52
60
  "access": "public"
53
61
  },
54
62
  "dependencies": {
55
- "@loaders.gl/loader-utils": "5.0.0-alpha.9",
63
+ "apache-arrow": ">= 17.0.0",
56
64
  "fast-xml-parser": "^5.5.3"
57
65
  }
58
66
  }
@@ -2,8 +2,18 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {Loader} from '@loaders.gl/loader-utils';
6
- import type {DOTGraphData} from './dot-types';
5
+ import type {Loader} from '@loaders.gl/core/loader-utils';
6
+ import type {DOTOutput} from './dot-types';
7
+ import type {GraphShape} from './graph-types';
8
+
9
+ /** Options for selecting the DOT graph output representation. */
10
+ export type DOTLoaderOptions = {
11
+ /** Format-specific graph options. */
12
+ dot?: {
13
+ /** Representation of each named table; defaults to Arrow. */
14
+ shape?: GraphShape;
15
+ };
16
+ };
7
17
 
8
18
  // @ts-ignore __VERSION__ is injected by the build.
9
19
  const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
@@ -24,8 +34,8 @@ export const DOTLoader = {
24
34
  mimeTypes: ['text/vnd.graphviz', 'text/x-graphviz', 'application/vnd.graphviz'],
25
35
  text: true,
26
36
  worker: false,
27
- dataType: null as unknown as DOTGraphData,
37
+ dataType: null as unknown as DOTOutput,
28
38
  batchType: null as never,
29
- options: {},
39
+ options: {dot: {shape: 'arrow-table'}},
30
40
  preload
31
- } as const satisfies Loader<DOTGraphData, never>;
41
+ } as const satisfies Loader<DOTOutput, never, DOTLoaderOptions>;
package/src/dot-loader.ts CHANGED
@@ -2,13 +2,16 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {DOTLoader} from './dot-loader-types';
5
+ import {DOTLoader, type DOTLoaderOptions} from './dot-loader-types';
6
6
  import {parseDOT} from './lib/parse-dot';
7
7
 
8
8
  /** Parser-bearing loader for synchronous and asynchronous DOT parsing. */
9
9
  export const DOTLoaderWithParser = {
10
10
  ...DOTLoader,
11
- parse: async (data: ArrayBuffer) => parseDOT(new TextDecoder().decode(data)),
12
- parseSync: (data: ArrayBuffer) => parseDOT(new TextDecoder().decode(data)),
13
- parseTextSync: parseDOT
11
+ parse: async (data: ArrayBuffer, options?: DOTLoaderOptions) =>
12
+ parseDOT(new TextDecoder().decode(data), options?.dot?.shape ?? 'arrow-table'),
13
+ parseSync: (data: ArrayBuffer, options?: DOTLoaderOptions) =>
14
+ parseDOT(new TextDecoder().decode(data), options?.dot?.shape ?? 'arrow-table'),
15
+ parseTextSync: (data: string, options?: DOTLoaderOptions) =>
16
+ parseDOT(data, options?.dot?.shape ?? 'arrow-table')
14
17
  };
package/src/dot-types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {GraphData} from './graph-types';
5
+ import type {GraphData, GraphOutput} from './graph-types';
6
6
 
7
7
  /** Application attributes parsed from DOT. */
8
8
  type DOTAttributeMap = Record<string, unknown>;
@@ -36,3 +36,9 @@ export type DOTGraphData = GraphData & {
36
36
  /** DOT graph identity, direction, strictness, attributes, and subgraphs. */
37
37
  metadata: DOTMetadata;
38
38
  };
39
+
40
+ /** DOT tables or compatibility records with graph metadata. */
41
+ export type DOTOutput = GraphOutput & {
42
+ /** Graph identity, defaults, and subgraph descriptors. */
43
+ metadata: DOTMetadata;
44
+ };
@@ -0,0 +1,41 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Loader} from '@loaders.gl/core/loader-utils';
6
+ import type {GraphShape} from './graph-types';
7
+ import type {GEXFOutput} from './gexf-types';
8
+
9
+ /** Options for selecting the GEXF graph output representation. */
10
+ export type GEXFLoaderOptions = {
11
+ /** GEXF-specific graph options. */
12
+ gexf?: {
13
+ /** Representation of each named table; defaults to Arrow. */
14
+ shape?: GraphShape;
15
+ };
16
+ };
17
+
18
+ // @ts-ignore __VERSION__ is injected by the build.
19
+ const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
20
+
21
+ /** Preloads the GEXF parser implementation. */
22
+ async function preload() {
23
+ const {GEXFLoaderWithParser} = await import('@loaders.gl/graphs/gexf-loader');
24
+ return GEXFLoaderWithParser;
25
+ }
26
+
27
+ /** Metadata-only loader for static GEXF 1.2 and 1.3 documents. */
28
+ export const GEXFLoader = {
29
+ name: 'GEXF',
30
+ id: 'gexf',
31
+ module: 'graphs',
32
+ version: VERSION,
33
+ extensions: ['gexf'],
34
+ mimeTypes: ['application/gexf+xml'],
35
+ text: true,
36
+ worker: false,
37
+ dataType: null as unknown as GEXFOutput,
38
+ batchType: null as never,
39
+ options: {gexf: {shape: 'arrow-table'}},
40
+ preload
41
+ } as const satisfies Loader<GEXFOutput, never, GEXFLoaderOptions>;
@@ -0,0 +1,17 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {GEXFLoader, type GEXFLoaderOptions} from './gexf-loader-types';
6
+ import {parseGEXF} from './lib/parse-gexf';
7
+
8
+ /** Parser-bearing loader for synchronous and asynchronous GEXF parsing. */
9
+ export const GEXFLoaderWithParser = {
10
+ ...GEXFLoader,
11
+ parse: async (data: ArrayBuffer, options?: GEXFLoaderOptions) =>
12
+ parseGEXF(data, options?.gexf?.shape ?? 'arrow-table'),
13
+ parseSync: (data: ArrayBuffer, options?: GEXFLoaderOptions) =>
14
+ parseGEXF(data, options?.gexf?.shape ?? 'arrow-table'),
15
+ parseTextSync: (data: string, options?: GEXFLoaderOptions) =>
16
+ parseGEXF(data, options?.gexf?.shape ?? 'arrow-table')
17
+ };
@@ -0,0 +1,21 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {GraphOutput} from './graph-types';
6
+
7
+ /** Static GEXF document metadata, retained in every output shape. */
8
+ export type GEXFMetadata = {
9
+ /** Document format version, when supplied. */
10
+ version?: string;
11
+ /** Default direction used by edges without a type override. */
12
+ directed: boolean;
13
+ /** Document author, description, keywords, and modification date. */
14
+ attributes: Record<string, string>;
15
+ };
16
+
17
+ /** Standard graph tables or plain records with GEXF document metadata. */
18
+ export type GEXFOutput = GraphOutput & {
19
+ /** Document metadata and graph-wide direction. */
20
+ metadata: GEXFMetadata;
21
+ };
@@ -2,6 +2,8 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ import type {ArrowTable, ObjectRowTable, Tables} from '@loaders.gl/core/schema';
6
+
5
7
  /** A graph node with its original identifier and application attributes. */
6
8
  export type GraphNode = {
7
9
  /** Original node identifier. */
@@ -31,3 +33,36 @@ export type GraphData = {
31
33
  /** Edges in document order. */
32
34
  edges: GraphEdge[];
33
35
  };
36
+
37
+ /** Standard table representations supported by graph loaders. */
38
+ export type GraphShape = 'arrow-table' | 'object-row-table' | 'plain-graph-data';
39
+
40
+ /** Named node and edge tables in document order. */
41
+ export type GraphTables<TableType = ArrowTable> = Tables<TableType> & {
42
+ /** Node table followed by edge table. */
43
+ tables: [{name: 'nodes'; table: TableType}, {name: 'edges'; table: TableType}];
44
+ };
45
+
46
+ /** Graph loader output, with Arrow tables as the default representation. */
47
+ export type GraphOutput = GraphTables | GraphTables<ObjectRowTable> | GraphData;
48
+
49
+ /** Scalar and list types declared by graph format attributes. */
50
+ export type GraphAttributeType =
51
+ | 'boolean'
52
+ | 'int'
53
+ | 'long'
54
+ | 'float'
55
+ | 'double'
56
+ | 'string'
57
+ | {
58
+ /** Declared element type for a GEXF list attribute. */
59
+ list: GraphAttributeType;
60
+ };
61
+
62
+ /** Declared application attribute schemas for each graph table. */
63
+ export type GraphAttributeSchemas = {
64
+ /** Declared node attributes, including keys with no values. */
65
+ nodes: Map<string, GraphAttributeType>;
66
+ /** Declared edge attributes, including keys with no values. */
67
+ edges: Map<string, GraphAttributeType>;
68
+ };
@@ -2,8 +2,17 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {Loader} from '@loaders.gl/loader-utils';
6
- import type {GraphData} from './graph-types';
5
+ import type {Loader} from '@loaders.gl/core/loader-utils';
6
+ import type {GraphOutput, GraphShape} from './graph-types';
7
+
8
+ /** Options for selecting the GraphML graph output representation. */
9
+ export type GraphMLLoaderOptions = {
10
+ /** Format-specific graph options. */
11
+ graphml?: {
12
+ /** Representation of each named table; defaults to Arrow. */
13
+ shape?: GraphShape;
14
+ };
15
+ };
7
16
 
8
17
  // @ts-ignore __VERSION__ is injected by the build.
9
18
  const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
@@ -24,8 +33,8 @@ export const GraphMLLoader = {
24
33
  mimeTypes: ['application/graphml+xml'],
25
34
  text: true,
26
35
  worker: false,
27
- dataType: null as unknown as GraphData,
36
+ dataType: null as unknown as GraphOutput,
28
37
  batchType: null as never,
29
- options: {},
38
+ options: {graphml: {shape: 'arrow-table'}},
30
39
  preload
31
- } as const satisfies Loader<GraphData, never>;
40
+ } as const satisfies Loader<GraphOutput, never, GraphMLLoaderOptions>;
@@ -2,13 +2,16 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {GraphMLLoader} from './graphml-loader-types';
5
+ import {GraphMLLoader, type GraphMLLoaderOptions} from './graphml-loader-types';
6
6
  import {parseGraphML} from './lib/parse-graphml';
7
7
 
8
8
  /** Parser-bearing loader for synchronous and asynchronous GraphML parsing. */
9
9
  export const GraphMLLoaderWithParser = {
10
10
  ...GraphMLLoader,
11
- parse: async (data: ArrayBuffer) => parseGraphML(data),
12
- parseSync: parseGraphML,
13
- parseTextSync: parseGraphML
11
+ parse: async (data: ArrayBuffer, options?: GraphMLLoaderOptions) =>
12
+ parseGraphML(data, options?.graphml?.shape ?? 'arrow-table'),
13
+ parseSync: (data: ArrayBuffer, options?: GraphMLLoaderOptions) =>
14
+ parseGraphML(data, options?.graphml?.shape ?? 'arrow-table'),
15
+ parseTextSync: (data: string, options?: GraphMLLoaderOptions) =>
16
+ parseGraphML(data, options?.graphml?.shape ?? 'arrow-table')
14
17
  };
package/src/index.ts CHANGED
@@ -6,3 +6,12 @@ export {GraphMLLoader} from './graphml-loader-types';
6
6
  export type {GraphData, GraphNode, GraphEdge} from './graph-types';
7
7
  export {DOTLoader} from './dot-loader-types';
8
8
  export type {DOTGraphData, DOTMetadata, DOTSubgraph} from './dot-types';
9
+
10
+ export type {GraphShape, GraphTables, GraphOutput} from './graph-types';
11
+ export type {DOTOutput} from './dot-types';
12
+ export type {DOTLoaderOptions} from './dot-loader-types';
13
+ export type {GraphMLLoaderOptions} from './graphml-loader-types';
14
+
15
+ export {GEXFLoader} from './gexf-loader-types';
16
+ export type {GEXFLoaderOptions} from './gexf-loader-types';
17
+ export type {GEXFMetadata, GEXFOutput} from './gexf-types';
@@ -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/core/schema-utils';
20
+ import type {ArrowTable, ObjectRowTable} from '@loaders.gl/core/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,15 +64,29 @@ 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: {
@@ -79,8 +95,14 @@ export function parseDOT(input: string): DOTGraphData {
79
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,
@@ -92,15 +114,8 @@ export function parseDOT(input: string): DOTGraphData {
92
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;
@@ -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
  }