• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.21
/src/data/PajekDataSource.ts
1
import type {AdHocData} from "../config/common.js";
2
import {BaseDataSourceConfig, DataSource, DataSourceChunk} from "./DataSource.js";
15✔
3

4
// Pajek has no additional config currently, so just use the base config
5
export type PajekDataSourceConfig = BaseDataSourceConfig;
6

7
interface ParsedVertex {
8
    id: string;
9
    label?: string;
10
    x?: number;
11
    y?: number;
12
    z?: number;
13
}
14

15
interface ParsedEdge {
16
    src: string;
17
    dst: string;
18
    weight?: number;
19
    directed: boolean;
20
}
21

22
/**
23
 * Data source for loading graph data from Pajek NET format files.
24
 * Supports vertices, edges (arcs), and undirected edges with coordinates and weights.
25
 */
26
export class PajekDataSource extends DataSource {
15✔
27
    static readonly type = "pajek";
19✔
28

29
    private config: PajekDataSourceConfig;
30

31
    /**
32
     * Creates a new PajekDataSource instance.
33
     * @param config - Configuration options for Pajek parsing and data loading
34
     */
35
    constructor(config: PajekDataSourceConfig) {
19✔
36
        super(config.errorLimit ?? 100, config.chunkSize);
39✔
37
        this.config = config;
39✔
38
    }
39✔
39

40
    protected getConfig(): BaseDataSourceConfig {
19✔
41
        return this.config;
37✔
42
    }
37✔
43

44
    /**
45
     * Fetches and parses Pajek NET format data into graph chunks.
46
     * @yields DataSourceChunk objects containing parsed nodes and edges
47
     */
48
    async *sourceFetchData(): AsyncGenerator<DataSourceChunk, void, unknown> {
19✔
49
        // Get Pajek content
50
        const content = await this.getContent();
37✔
51

52
        // Parse Pajek NET format
53
        const {vertices, edges} = this.parsePajek(content);
35✔
54

55
        // Convert to AdHocData
56
        const nodes = vertices.map((v) => this.vertexToNode(v));
35✔
57
        const edgeData = edges.map((e) => this.edgeToEdgeData(e));
35✔
58

59
        // Use shared chunking helper
60
        yield* this.chunkData(nodes, edgeData);
35✔
61
    }
37✔
62

63
    private parsePajek(content: string): {vertices: ParsedVertex[], edges: ParsedEdge[]} {
19✔
64
        const lines = content.split("\n").map((line) => line.trim());
35✔
65
        const vertices: ParsedVertex[] = [];
35✔
66
        const edges: ParsedEdge[] = [];
35✔
67

68
        let section: "none" | "vertices" | "arcs" | "edges" = "none";
35✔
69

70
        for (let lineNum = 0; lineNum < lines.length; lineNum++) {
35✔
71
            const line = lines[lineNum];
1,299✔
72

73
            // Skip blank lines
74
            if (!line) {
1,299!
75
                continue;
18✔
76
            }
18✔
77

78
            // Check for section headers
79
            if (line.toLowerCase().startsWith("*vertices")) {
1,299✔
80
                section = "vertices";
32✔
81
                continue;
32✔
82
            }
32✔
83

84
            if (line.toLowerCase().startsWith("*arcs")) {
1,299✔
85
                section = "arcs";
9✔
86
                continue;
9✔
87
            }
9✔
88

89
            if (line.toLowerCase().startsWith("*edges")) {
1,299✔
90
                section = "edges";
21✔
91
                continue;
21✔
92
            }
21✔
93

94
            // Process line based on current section
95
            if (section === "vertices") {
1,299✔
96
                const vertex = this.parseVertexLine(line, lineNum);
301✔
97
                if (vertex) {
301✔
98
                    vertices.push(vertex);
301✔
99
                }
301✔
100
            } else if (section === "arcs") {
1,299✔
101
                const edge = this.parseEdgeLine(line, lineNum, true);
25✔
102
                if (edge) {
25✔
103
                    edges.push(edge);
25✔
104
                }
25✔
105
            } else if (section === "edges") {
913✔
106
                const edge = this.parseEdgeLine(line, lineNum, false);
887✔
107
                if (edge) {
887✔
108
                    edges.push(edge);
887✔
109
                }
887✔
110
            }
887✔
111
        }
1,299✔
112

113
        return {vertices, edges};
35✔
114
    }
35✔
115

116
    private parseVertexLine(line: string, lineNum: number): ParsedVertex | null {
19✔
117
        try {
301✔
118
            // Vertex format: id "label" x y z
119
            // Or: id x y z
120
            // Or: id "label"
121
            // Or: id
122

123
            // Extract vertex ID (first token)
124
            const {tokens, quotedIndices} = this.tokenizeLine(line);
301✔
125
            if (tokens.length === 0) {
301!
126
                return null;
×
127
            }
×
128

129
            const id = tokens[0];
301✔
130
            let label: string | undefined;
301✔
131
            let x: number | undefined;
301✔
132
            let y: number | undefined;
301✔
133
            let z: number | undefined;
301✔
134

135
            // Check if second token is a quoted string (label)
136
            let coordStartIndex = 1;
301✔
137
            if (tokens.length > 1 && quotedIndices.has(1)) {
301✔
138
                label = tokens[1];
298✔
139
                coordStartIndex = 2;
298✔
140
            }
298✔
141

142
            // Parse coordinates
143
            if (tokens.length > coordStartIndex) {
301✔
144
                const xVal = parseFloat(tokens[coordStartIndex]);
26✔
145
                if (!isNaN(xVal)) {
26✔
146
                    x = xVal;
25✔
147
                }
25✔
148
            }
26✔
149

150
            if (tokens.length > coordStartIndex + 1) {
301✔
151
                const yVal = parseFloat(tokens[coordStartIndex + 1]);
26✔
152
                if (!isNaN(yVal)) {
26✔
153
                    y = yVal;
25✔
154
                }
25✔
155
            }
26✔
156

157
            if (tokens.length > coordStartIndex + 2) {
301✔
158
                const zVal = parseFloat(tokens[coordStartIndex + 2]);
24✔
159
                if (!isNaN(zVal)) {
24✔
160
                    z = zVal;
23✔
161
                }
23✔
162
            }
24✔
163

164
            return {id, label, x, y, z};
301✔
165
        } catch (error) {
301!
166
            const canContinue = this.errorAggregator.addError({
×
167
                message: `Failed to parse vertex line ${lineNum + 1}: ${error instanceof Error ? error.message : String(error)}`,
×
168
                category: "parse-error",
×
169
                line: lineNum + 1,
×
170
            });
×
171

172
            if (!canContinue) {
×
173
                throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
174
            }
×
175

176
            return null;
×
177
        }
×
178
    }
301✔
179

180
    private parseEdgeLine(line: string, lineNum: number, directed: boolean): ParsedEdge | null {
19✔
181
        try {
912✔
182
            // Edge format: src dst weight
183
            // Or: src dst
184
            const {tokens} = this.tokenizeLine(line);
912✔
185

186
            if (tokens.length < 2) {
912!
187
                throw new Error("Edge must have at least source and target");
×
188
            }
×
189

190
            const src = tokens[0];
912✔
191
            const dst = tokens[1];
912✔
192
            let weight: number | undefined;
912✔
193

194
            if (tokens.length > 2) {
912✔
195
                const weightVal = parseFloat(tokens[2]);
122✔
196
                if (!isNaN(weightVal)) {
122✔
197
                    weight = weightVal;
121✔
198
                }
121✔
199
            }
122✔
200

201
            return {src, dst, weight, directed};
912✔
202
        } catch (error) {
912!
203
            const canContinue = this.errorAggregator.addError({
×
204
                message: `Failed to parse edge line ${lineNum + 1}: ${error instanceof Error ? error.message : String(error)}`,
×
205
                category: "parse-error",
×
206
                line: lineNum + 1,
×
207
            });
×
208

209
            if (!canContinue) {
×
210
                throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
211
            }
×
212

213
            return null;
×
214
        }
×
215
    }
912✔
216

217
    private tokenizeLine(line: string): {tokens: string[], quotedIndices: Set<number>} {
19✔
218
        const tokens: string[] = [];
1,213✔
219
        const quotedIndices = new Set<number>();
1,213✔
220
        let current = "";
1,213✔
221
        let inQuotes = false;
1,213✔
222

223
        for (const char of line) {
1,213✔
224
            if (char === "\"") {
8,354✔
225
                if (inQuotes) {
596✔
226
                    // End of quoted string
227
                    quotedIndices.add(tokens.length);
298✔
228
                    tokens.push(current);
298✔
229
                    current = "";
298✔
230
                    inQuotes = false;
298✔
231
                } else {
298✔
232
                    // Start of quoted string
233
                    if (current.trim()) {
298!
234
                        tokens.push(current.trim());
×
235
                        current = "";
×
236
                    }
×
237

238
                    inQuotes = true;
298✔
239
                }
298✔
240

241
                continue;
596✔
242
            }
596✔
243

244
            if (inQuotes) {
8,354✔
245
                current += char;
1,939✔
246
            } else if (/\s/.test(char)) {
8,354✔
247
                if (current.trim()) {
1,415✔
248
                    tokens.push(current.trim());
1,384✔
249
                    current = "";
1,384✔
250
                }
1,384✔
251
            } else {
5,819✔
252
                current += char;
4,404✔
253
            }
4,404✔
254
        }
8,354✔
255

256
        if (current.trim()) {
1,213✔
257
            tokens.push(current.trim());
940✔
258
        }
940✔
259

260
        return {tokens, quotedIndices};
1,213✔
261
    }
1,213✔
262

263
    private vertexToNode(vertex: ParsedVertex): AdHocData {
19✔
264
        const node: Record<string, unknown> = {
301✔
265
            id: vertex.id,
301✔
266
        };
301✔
267

268
        if (vertex.label !== undefined) {
301✔
269
            node.label = vertex.label;
298✔
270
        }
298✔
271

272
        if (vertex.x !== undefined) {
301✔
273
            node.x = vertex.x;
25✔
274
        }
25✔
275

276
        if (vertex.y !== undefined) {
301✔
277
            node.y = vertex.y;
25✔
278
        }
25✔
279

280
        if (vertex.z !== undefined) {
301✔
281
            node.z = vertex.z;
23✔
282
        }
23✔
283

284
        return node as AdHocData;
301✔
285
    }
301✔
286

287
    private edgeToEdgeData(edge: ParsedEdge): AdHocData {
19✔
288
        const edgeData: Record<string, unknown> = {
912✔
289
            src: edge.src,
912✔
290
            dst: edge.dst,
912✔
291
            directed: edge.directed,
912✔
292
        };
912✔
293

294
        if (edge.weight !== undefined) {
912✔
295
            edgeData.weight = edge.weight;
121✔
296
        }
121✔
297

298
        return edgeData as AdHocData;
912✔
299
    }
912✔
300
}
19✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc