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

openwisp / netjsongraph.js / 16582217787

28 Jul 2025 10:59PM UTC coverage: 80.832% (+2.0%) from 78.786%
16582217787

push

github

web-flow
[change] Uniform geojson map rendering #395

Closes #395

449 of 599 branches covered (74.96%)

Branch coverage included in aggregate %.

140 of 187 new or added lines in 7 files covered. (74.87%)

4 existing lines in 3 files now uncovered.

795 of 940 relevant lines covered (84.57%)

7.27 hits per line

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

93.16
/src/js/netjsongraph.util.js
1
import KDBush from "kdbush";
2
import {geojsonToNetjson as convertGeojson} from "./netjsongraph.geojson";
3

4
class NetJSONGraphUtil {
5
  /**
6
   * @function
7
   * @name JSONParamParse
8
   *
9
   * Perform different operations to call NetJSONDataParse function according to different Param types.
10
   * @param  {object|string}  JSONParam   Url or JSONData
11
   *
12
   * @return {object}    A promise object of JSONData
13
   */
14

15
  JSONParamParse(JSONParam) {
16
    if (typeof JSONParam === "string") {
30✔
17
      return fetch(JSONParam, {
11✔
18
        method: "GET",
19
        headers: {
20
          "Content-Type": "application/json",
21
          Accept: "application/json",
22
        },
23
        credentials: "include",
24
      })
25
        .then((response) => response)
9✔
26
        .catch((msg) => {
27
          console.error(msg);
2✔
28
        });
29
    }
30
    return Promise.resolve(JSONParam);
19✔
31
  }
32

33
  async paginatedDataParse(JSONParam) {
34
    let res;
35
    let data;
36
    try {
24✔
37
      let paginatedResponse = await this.utils.JSONParamParse(JSONParam);
24✔
38
      if (paginatedResponse.json) {
24✔
39
        res = await paginatedResponse.json();
1✔
40
        data = res.results ? res.results : res;
1!
41
        while (res.next && data.nodes.length <= this.config.maxPointsFetched) {
1✔
42
          // eslint-disable-next-line no-await-in-loop
43
          paginatedResponse = await this.utils.JSONParamParse(res.next);
2✔
44
          // eslint-disable-next-line no-await-in-loop
45
          res = await paginatedResponse.json();
2✔
46
          data.nodes = data.nodes.concat(res.results.nodes);
2✔
47
          data.links = data.links.concat(res.results.links);
2✔
48

49
          if (res.next) {
2✔
50
            this.hasMoreData = true;
1✔
51
          } else {
52
            this.hasMoreData = false;
1✔
53
          }
54
        }
55
      } else {
56
        data = paginatedResponse;
22✔
57
      }
58
    } catch (e) {
59
      console.error(e);
1✔
60
    }
61

62
    return data;
24✔
63
  }
64

65
  async getBBoxData(JSONParam, bounds) {
66
    let data;
67
    try {
1✔
68
      // eslint-disable-next-line prefer-destructuring
69
      JSONParam = JSONParam[0].split("?")[0];
1✔
70
      // eslint-disable-next-line no-underscore-dangle
71
      const url = `${JSONParam}bbox?swLat=${bounds._southWest.lat}&swLng=${bounds._southWest.lng}&neLat=${bounds._northEast.lat}&neLng=${bounds._northEast.lng}`;
1✔
72
      const res = await this.utils.JSONParamParse(url);
1✔
73
      data = await res.json();
1✔
74
    } catch (e) {
75
      console.error(e);
×
76
    }
77
    return data;
1✔
78
  }
79

80
  /**
81
   * @function
82
   * @name dateParse
83
   *
84
   * Parse the time in the browser's current time zone based on the incoming matching rules.
85
   * The exec result must be [date, year, month, day, hour, minute, second, millisecond?]
86
   *
87
   * @param  {string}          dateString
88
   * @param  {object(RegExp)}  parseRegular
89
   * @param  {number}          hourDiffer    you can custom time difference, default is the standard time difference
90

91
   *
92
   * @return {string}    Date string
93
   */
94

95
  dateParse({
96
    dateString,
97
    parseRegular = /^([1-9]\d{3})-(\d{1,2})-(\d{1,2})T(\d{1,2}):(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?Z$/,
14✔
98
    hourDiffer = new Date().getTimezoneOffset() / 60,
9✔
99
  }) {
100
    const dateParseArr = parseRegular.exec(dateString);
14✔
101
    if (!dateParseArr || dateParseArr.length < 7) {
14✔
102
      console.error("Date doesn't meet the specifications.");
1✔
103
      return "";
1✔
104
    }
105
    const dateNumberFields = ["dateYear", "dateMonth", "dateDay", "dateHour"];
13✔
106
    const dateNumberObject = {};
13✔
107
    const leapYear =
108
      (dateParseArr[1] % 4 === 0 && dateParseArr[1] % 100 !== 0) ||
13✔
109
      dateParseArr[1] % 400 === 0;
110
    const limitBoundaries = new Map([
13✔
111
      ["dateMonth", 12],
112
      [
113
        "dateDay",
114
        [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
13✔
115
      ],
116
      ["dateHour", 24],
117
    ]);
118

119
    for (let i = dateNumberFields.length; i > 0; i -= 1) {
13✔
120
      dateNumberObject[dateNumberFields[i - 1]] = parseInt(dateParseArr[i], 10);
52✔
121
    }
122

123
    let carry = -hourDiffer;
13✔
124
    let limitBoundary;
125
    for (let i = dateNumberFields.length; i > 0; i -= 1) {
13✔
126
      if (dateNumberFields[i - 1] === "dateYear") {
52✔
127
        dateNumberObject[dateNumberFields[i - 1]] += carry;
13✔
128
        break;
13✔
129
      } else if (dateNumberFields[i - 1] === "dateDay") {
39✔
130
        limitBoundary =
13✔
131
          limitBoundaries.get("dateDay")[dateNumberObject.dateMonth - 1];
132
      } else {
133
        limitBoundary = limitBoundaries.get(dateNumberFields[i - 1]);
26✔
134
      }
135

136
      let calculateResult = dateNumberObject[dateNumberFields[i - 1]] + carry;
39✔
137

138
      if (dateNumberFields[i - 1] === "dateHour") {
39✔
139
        if (calculateResult < 0) {
13✔
140
          carry = -1;
1✔
141
        } else if (calculateResult >= limitBoundary) {
12✔
142
          carry = 1;
3✔
143
        } else {
144
          carry = 0;
9✔
145
        }
146
      } else if (calculateResult <= 0) {
26✔
147
        carry = -1;
2✔
148
      } else if (calculateResult > limitBoundary) {
24✔
149
        carry = 1;
4✔
150
      } else {
151
        carry = 0;
20✔
152
      }
153

154
      if (carry === 1) {
39✔
155
        calculateResult -= limitBoundary;
7✔
156
      } else if (carry < 0) {
32✔
157
        if (dateNumberFields[i - 1] === "dateDay") {
3✔
158
          limitBoundary =
1✔
159
            limitBoundaries.get("dateDay")[
160
              (dateNumberObject[dateNumberFields[i - 1]] + 10) % 11
161
            ];
162
        }
163
        calculateResult += limitBoundary;
3✔
164
      }
165

166
      dateNumberObject[dateNumberFields[i - 1]] = calculateResult;
39✔
167
    }
168

169
    return `${dateNumberObject.dateYear}.${this.numberMinDigit(
13✔
170
      dateNumberObject.dateMonth,
171
    )}.${this.numberMinDigit(dateNumberObject.dateDay)} ${this.numberMinDigit(
172
      dateNumberObject.dateHour,
173
    )}:${this.numberMinDigit(dateParseArr[5])}:${this.numberMinDigit(
174
      dateParseArr[6],
175
    )}${dateParseArr[7] ? `.${this.numberMinDigit(dateParseArr[7], 3)}` : ""}`;
13✔
176
  }
177

178
  /**
179
   * Guaranteed minimum number of digits
180
   *
181
   * @param  {number}      number
182
   * @param  {number}      digit      min digit
183
   * @param  {string}      filler
184
   *
185
   * @return {string}
186
   */
187
  numberMinDigit(number, digit = 2, filler = "0") {
139✔
188
    return (Array(digit).join(filler) + number).slice(-digit);
75✔
189
  }
190

191
  /**
192
   * Judge parameter type
193
   *
194
   * @return {bool}
195
   */
196
  isObject(x) {
197
    return Object.prototype.toString.call(x).slice(8, 14) === "Object";
176✔
198
  }
199

200
  /**
201
   * Judge parameter type
202
   *
203
   * @return {bool}
204
   */
205
  isArray(x) {
206
    return Object.prototype.toString.call(x).slice(8, 13) === "Array";
106✔
207
  }
208

209
  /**
210
   * Judge parameter is a dom element.
211
   *
212
   * @return {bool}
213
   */
214
  isElement(o) {
215
    return typeof HTMLElement === "object"
8!
216
      ? o instanceof HTMLElement // DOM2
217
      : o &&
24✔
218
          typeof o === "object" &&
219
          o !== null &&
220
          o.nodeType === 1 &&
221
          typeof o.nodeName === "string";
222
  }
223

224
  /**
225
   * Judge parameter is a NetJSON network graph object.
226
   *
227
   * @return {bool}
228
   */
229
  isNetJSON(param) {
230
    if (param.nodes && param.links) {
50✔
231
      return (
44✔
232
        this.isObject(param) &&
131✔
233
        this.isArray(param.nodes) &&
234
        this.isArray(param.links)
235
      );
236
    }
237

238
    return false;
5✔
239
  }
240

241
  /**
242
   * Judge parameter is GeoJSON object.
243
   *
244
   * @return {bool}
245
   */
246
  isGeoJSON(param) {
247
    if (param.type && param.type === "FeatureCollection") {
11✔
248
      return this.isObject(param) && this.isArray(param.features);
2✔
249
    }
250

251
    if (param.type && param.type === "Feature") {
9✔
252
      return this.isObject(param) && this.isArray(param.geometry);
1✔
253
    }
254

255
    return false;
8✔
256
  }
257

258
  /**
259
   * Thin wrapper calling the dedicated converter in netjsongraph.geojson.js.
260
   * Keeping it here preserves public API while moving heavy logic out.
261
   */
262
  geojsonToNetjson(geojson) {
263
    return convertGeojson(geojson);
3✔
264
  }
265

266
  /**
267
   * merge two object deeply
268
   *
269
   * @param  {object}
270
   *
271
   * @return {object}      targetObj
272
   */
273
  deepMergeObj(...args) {
274
    const objs = [...args].reverse();
53✔
275
    const len = objs.length;
53✔
276

277
    for (let i = 0; i < len - 1; i += 1) {
53✔
278
      const originObj = objs[i];
55✔
279
      const targetObj = objs[i + 1];
55✔
280
      if (
55✔
281
        originObj &&
209✔
282
        targetObj &&
283
        this.isObject(targetObj) &&
284
        this.isObject(originObj)
285
      ) {
286
        Object.keys(originObj).forEach((attr) => {
51✔
287
          if (
125✔
288
            !targetObj[attr] ||
145✔
289
            !(this.isObject(targetObj[attr]) && this.isObject(originObj[attr]))
25✔
290
          ) {
291
            targetObj[attr] = originObj[attr];
120✔
292
          } else {
293
            this.deepMergeObj(targetObj[attr], originObj[attr]);
5✔
294
          }
295
        });
296
      } else if (!targetObj) {
4✔
297
        objs[i + 1] = originObj;
2✔
298
      }
299
    }
300

301
    return objs[len - 1];
53✔
302
  }
303

304
  makeCluster(self) {
305
    const {nodes, links} = self.data;
2✔
306
    const nonClusterNodes = [];
2✔
307
    const nonClusterLinks = [];
2✔
308
    const clusters = [];
2✔
309
    const nodeMap = new Map();
2✔
310
    let clusterId = 0;
2✔
311

312
    nodes.forEach((node) => {
2✔
313
      node.y = self.leaflet.latLngToContainerPoint([
9✔
314
        node.location.lat,
315
        node.location.lng,
316
      ]).y;
317
      node.x = self.leaflet.latLngToContainerPoint([
9✔
318
        node.location.lat,
319
        node.location.lng,
320
      ]).x;
321
      node.visited = false;
9✔
322
      node.cluster = null;
9✔
323
    });
324

325
    const index = new KDBush(nodes.length);
2✔
326
    /* eslint-disable no-restricted-syntax */
327
    for (const {x, y} of nodes) index.add(x, y);
9✔
328
    /* eslint-enable no-restricted-syntax */
329
    index.finish();
2✔
330

331
    nodes.forEach((node) => {
2✔
332
      let cluster;
333
      let centroid = [0, 0];
9✔
334
      const addNode = (n) => {
9✔
335
        n.visited = true;
9✔
336
        n.cluster = clusterId;
9✔
337
        nodeMap.set(n.id, n.cluster);
9✔
338
        centroid[0] += n.location.lng;
9✔
339
        centroid[1] += n.location.lat;
9✔
340
      };
341
      if (!node.visited) {
9✔
342
        const neighbors = index
7✔
343
          .within(node.x, node.y, self.config.clusterRadius)
344
          .map((id) => nodes[id]);
12✔
345
        const results = neighbors.filter((n) => {
7✔
346
          if (self.config.clusteringAttribute) {
12✔
347
            if (
8✔
348
              n.properties[self.config.clusteringAttribute] ===
13✔
349
                node.properties[self.config.clusteringAttribute] &&
350
              n.cluster === null
351
            ) {
352
              addNode(n);
5✔
353
              return true;
5✔
354
            }
355
            return false;
3✔
356
          }
357

358
          if (n.cluster === null) {
4!
359
            addNode(n);
4✔
360
            return true;
4✔
361
          }
362
          return false;
×
363
        });
364

365
        if (results.length > 1) {
7✔
366
          centroid = [
2✔
367
            centroid[0] / results.length,
368
            centroid[1] / results.length,
369
          ];
370
          cluster = {
2✔
371
            id: clusterId,
372
            cluster: true,
373
            name: results.length,
374
            value: centroid,
375
            childNodes: results,
376
            ...self.config.mapOptions.clusterConfig,
377
          };
378

379
          if (self.config.clusteringAttribute) {
2✔
380
            const {color} = self.config.nodeCategories.find(
1✔
381
              (cat) =>
382
                cat.name === node.properties[self.config.clusteringAttribute],
1✔
383
            ).nodeStyle;
384

385
            cluster.itemStyle = {
1✔
386
              ...cluster.itemStyle,
387
              color,
388
            };
389
          }
390

391
          clusters.push(cluster);
2✔
392
        } else if (results.length === 1) {
5!
393
          nodeMap.set(results[0].id, null);
5✔
394
          nonClusterNodes.push(results[0]);
5✔
395
        }
396
        clusterId += 1;
7✔
397
      }
398
    });
399

400
    links.forEach((link) => {
2✔
401
      if (
2✔
402
        nodeMap.get(link.source) === null &&
3✔
403
        nodeMap.get(link.target) === null
404
      ) {
405
        nonClusterLinks.push(link);
1✔
406
      }
407
    });
408

409
    return {clusters, nonClusterNodes, nonClusterLinks};
2✔
410
  }
411

412
  /**
413
   * @function
414
   * @name updateMetadata
415
   *
416
   * @this  {object}   NetJSONGraph object
417
   *
418
   */
419
  updateMetadata() {
420
    if (this.config.metadata) {
3✔
421
      const metaData = this.utils.getMetadata(this.data);
2✔
422
      const metadataContainer = document.querySelector(".njg-metaData");
2✔
423
      const metadataChildren = document.querySelectorAll(".njg-metaDataItems");
2✔
424

425
      for (let i = 0; i < metadataChildren.length; i += 1) {
2✔
426
        metadataChildren[i].remove();
×
427
      }
428

429
      Object.keys(metaData).forEach((key) => {
2✔
430
        const metaDataItems = document.createElement("div");
2✔
431
        metaDataItems.classList.add("njg-metaDataItems");
2✔
432
        const keyLabel = document.createElement("span");
2✔
433
        keyLabel.setAttribute("class", "njg-keyLabel");
2✔
434
        const valueLabel = document.createElement("span");
2✔
435
        valueLabel.setAttribute("class", "njg-valueLabel");
2✔
436
        keyLabel.innerHTML = key;
2✔
437
        valueLabel.innerHTML = metaData[key];
2✔
438
        metaDataItems.appendChild(keyLabel);
2✔
439
        metaDataItems.appendChild(valueLabel);
2✔
440
        metadataContainer.appendChild(metaDataItems);
2✔
441
      });
442
    }
443
  }
444

445
  /**
446
   * @function
447
   * @name getMetadata
448
   *
449
   * Get metadata dom string.
450
   *
451
   * @this   {object}   NetJSONGraph object
452
   * @return {string}   Dom string
453
   */
454
  getMetadata(data) {
455
    const attrs = [
3✔
456
      "protocol",
457
      "version",
458
      "revision",
459
      "metric",
460
      "router_id",
461
      "topology_id",
462
    ];
463
    const metadata = data;
3✔
464
    const metaDataObj = {};
3✔
465

466
    if (metadata.label) {
3✔
467
      metaDataObj.label = metadata.label;
1✔
468
    }
469
    attrs.forEach((attr) => {
3✔
470
      if (metadata[attr]) {
18✔
471
        metaDataObj[attr] = metadata[attr];
3✔
472
      }
473
    });
474

475
    metaDataObj.nodes = metadata.nodes.length;
3✔
476
    metaDataObj.links = metadata.links.length;
3✔
477
    return metaDataObj;
3✔
478
  }
479

480
  /**
481
   * @function
482
   * @name nodeInfo
483
   *
484
   * Parse the information of incoming node data.
485
   * @param  {object}    node
486
   *
487
   * @return {string}    html dom string
488
   */
489

490
  nodeInfo(node) {
491
    const nodeInfo = {};
5✔
492

493
    // Show public id/label only when they were provided by the data source.
494
    // eslint-disable-next-line no-underscore-dangle
495
    const identityIsPublic = !node._generatedIdentity;
5✔
496

497
    if (identityIsPublic) {
5!
498
      nodeInfo.id = node.id;
5✔
499
      if (node.label && typeof node.label === "string") {
5✔
500
        nodeInfo.label = node.label;
2✔
501
      }
502
    }
503

504
    if (node.name) {
5✔
505
      nodeInfo.name = node.name;
1✔
506
    }
507
    if (node.location) {
5✔
508
      nodeInfo.location = node.location;
1✔
509
    }
510

511
    if (node.properties) {
5✔
512
      Object.keys(node.properties).forEach((key) => {
2✔
513
        if (key === "location") {
5✔
514
          nodeInfo[key] = {
1✔
515
            lat: node.properties.location.lat,
516
            lng: node.properties.location.lng,
517
          };
518
        } else if (key === "time") {
4✔
519
          const time = this.dateParse({
1✔
520
            dateString: node.properties[key],
521
          });
522
          nodeInfo[key] = time;
1✔
523
        } else if (
3!
524
          typeof node.properties[key] === "object" ||
6✔
525
          key.startsWith("_")
526
        ) {
527
          // Skip nested objects and internal metadata
528
          // eslint-disable-next-line no-useless-return
NEW
529
          return;
×
530
        } else {
531
          nodeInfo[key.replace(/_/g, " ")] = node.properties[key];
3✔
532
        }
533
      });
534
    }
535
    if (node.linkCount) {
5✔
536
      nodeInfo.links = node.linkCount;
2✔
537
    }
538
    if (node.local_addresses) {
5✔
539
      nodeInfo.localAddresses = node.local_addresses;
2✔
540
    }
541

542
    return nodeInfo;
5✔
543
  }
544

545
  createTooltipItem(key, value) {
546
    const item = document.createElement("div");
41✔
547
    item.classList.add("njg-tooltip-item");
41✔
548
    const keyLabel = document.createElement("span");
41✔
549
    keyLabel.setAttribute("class", "njg-tooltip-key");
41✔
550
    const valueLabel = document.createElement("span");
41✔
551
    valueLabel.setAttribute("class", "njg-tooltip-value");
41✔
552
    keyLabel.innerHTML = key;
41✔
553
    valueLabel.innerHTML = value;
41✔
554
    item.appendChild(keyLabel);
41✔
555
    item.appendChild(valueLabel);
41✔
556
    return item;
41✔
557
  }
558

559
  getNodeTooltipInfo(node) {
560
    const container = document.createElement("div");
3✔
561
    container.classList.add("njg-tooltip-inner");
3✔
562

563
    // Show public id/label only when they were provided by the data source.
564
    // eslint-disable-next-line no-underscore-dangle
565
    const identityIsPublic = !node._generatedIdentity;
3✔
566

567
    if (identityIsPublic && node.id) {
3!
568
      container.appendChild(this.createTooltipItem("id", node.id));
3✔
569
    }
570
    if (identityIsPublic && node.label && typeof node.label === "string") {
3!
571
      container.appendChild(this.createTooltipItem("label", node.label));
3✔
572
    }
573

574
    if (node.properties) {
3!
575
      Object.keys(node.properties).forEach((key) => {
3✔
576
        if (typeof node.properties[key] === "object" || key.startsWith("_")) {
12✔
577
          return;
3✔
578
        }
579
        if ((key === "id" || key === "label") && identityIsPublic) {
9!
NEW
580
          return;
×
581
        }
582
        if (key === "location") {
9!
UNCOV
583
          container.appendChild(
×
584
            this.createTooltipItem(
585
              "location",
586
              `${Math.round(node.properties.location.lat * 1000) / 1000}, ${
587
                Math.round(node.properties.location.lng * 1000) / 1000
588
              }`,
589
            ),
590
          );
591
        } else if (key === "time") {
9✔
592
          const time = this.dateParse({
3✔
593
            dateString: node.properties[key],
594
          });
595
          container.appendChild(this.createTooltipItem("time", time));
3✔
596
        } else {
597
          container.appendChild(
6✔
598
            this.createTooltipItem(
599
              `${key.replace(/_/g, " ")}`,
600
              node.properties[key],
601
            ),
602
          );
603
        }
604
      });
605
    }
606
    if (node.linkCount) {
3!
607
      container.appendChild(this.createTooltipItem("Links", node.linkCount));
3✔
608
    }
609
    if (node.local_addresses) {
3!
610
      container.appendChild(
3✔
611
        this.createTooltipItem(
612
          "Local Addresses",
613
          node.local_addresses.join("<br/>"),
614
        ),
615
      );
616
    }
617
    return container;
3✔
618
  }
619

620
  getLinkTooltipInfo(link) {
621
    const container = document.createElement("div");
3✔
622
    container.classList.add("njg-tooltip-inner");
3✔
623

624
    const isGeneratedId = (val) =>
3✔
625
      typeof val === "string" && val.startsWith("gjn_");
6✔
626

627
    if (!isGeneratedId(link.source)) {
3!
628
      container.appendChild(this.createTooltipItem("source", link.source));
3✔
629
    }
630
    if (!isGeneratedId(link.target)) {
3!
631
      container.appendChild(this.createTooltipItem("target", link.target));
3✔
632
    }
633

634
    if (link.cost !== undefined && link.cost !== null) {
3!
635
      container.appendChild(this.createTooltipItem("cost", link.cost));
3✔
636
    }
637

638
    if (link.properties) {
3!
639
      Object.keys(link.properties).forEach((key) => {
3✔
640
        const val = link.properties[key];
9✔
641
        if (val === undefined || val === null) return;
9!
642

643
        if (key === "time") {
9✔
644
          const time = this.dateParse({dateString: val});
3✔
645
          container.appendChild(this.createTooltipItem("time", time));
3✔
646
        } else {
647
          const displayVal =
648
            typeof val === "string" ? val.replace(/\n/g, "<br/>") : val;
6!
649
          container.appendChild(
6✔
650
            this.createTooltipItem(`${key.replace(/_/g, " ")}`, displayVal),
651
          );
652
        }
653
      });
654
    }
655
    return container;
3✔
656
  }
657

658
  /**
659
   * @function
660
   * @name linkInfo
661
   *
662
   * Parse the infomation of incoming link data.
663
   * @param  {object}    link
664
   *
665
   * @return {string}    html dom string
666
   */
667

668
  linkInfo(link) {
669
    const linkInfo = {};
4✔
670

671
    const isGeneratedId = (val) =>
4✔
672
      typeof val === "string" && val.startsWith("gjn_");
8✔
673

674
    // Only include source/target if they are not autogenerated ids
675
    if (!isGeneratedId(link.source)) {
4!
676
      linkInfo.source = link.source;
4✔
677
    }
678
    if (!isGeneratedId(link.target)) {
4!
679
      linkInfo.target = link.target;
4✔
680
    }
681

682
    if (link.cost !== undefined && link.cost !== null) {
4✔
683
      linkInfo.cost = link.cost;
1✔
684
    }
685

686
    if (link.properties) {
4✔
687
      Object.keys(link.properties).forEach((key) => {
1✔
688
        const val = link.properties[key];
3✔
689
        if (val === undefined || val === null) return;
3!
690

691
        if (key === "time") {
3✔
692
          const time = this.dateParse({dateString: val});
1✔
693
          linkInfo[key] = time;
1✔
694
        } else {
695
          const displayVal =
696
            typeof val === "string" ? val.replace(/\n/g, "<br/>") : val;
2!
697
          linkInfo[key.replace(/_/g, " ")] = displayVal;
2✔
698
        }
699
      });
700
    }
701

702
    return linkInfo;
4✔
703
  }
704

705
  generateStyle(styleConfig, item) {
706
    const styles =
707
      typeof styleConfig === "function" ? styleConfig(item) : styleConfig;
13✔
708
    return styles;
13✔
709
  }
710

711
  getNodeStyle(node, config, type) {
712
    let nodeStyleConfig;
713
    let nodeSizeConfig = {};
3✔
714
    let nodeEmphasisConfig = {};
3✔
715
    if (node.category && config.nodeCategories.length) {
3✔
716
      const category = config.nodeCategories.find(
1✔
717
        (cat) => cat.name === node.category,
1✔
718
      );
719

720
      nodeStyleConfig = this.generateStyle(category.nodeStyle || {}, node);
1!
721

722
      nodeSizeConfig = this.generateStyle(category.nodeSize || {}, node);
1✔
723

724
      nodeEmphasisConfig = {
1✔
725
        ...nodeEmphasisConfig,
726
        nodeStyle: category.emphasis
1!
727
          ? this.generateStyle(category.emphasis.nodeStyle || {}, node)
1!
728
          : {},
729
      };
730

731
      nodeEmphasisConfig = {
1✔
732
        ...nodeEmphasisConfig,
733
        nodeSize: category.empahsis
1!
734
          ? this.generateStyle(category.emphasis.nodeSize || {}, node)
×
735
          : {},
736
      };
737
    } else if (type === "map") {
2✔
738
      nodeStyleConfig = this.generateStyle(
1✔
739
        config.mapOptions.nodeConfig.nodeStyle,
740
        node,
741
      );
742
      nodeSizeConfig = this.generateStyle(
1✔
743
        config.mapOptions.nodeConfig.nodeSize,
744
        node,
745
      );
746
    } else {
747
      nodeStyleConfig = this.generateStyle(
1✔
748
        config.graphConfig.series.nodeStyle,
749
        node,
750
      );
751
      nodeSizeConfig = this.generateStyle(
1✔
752
        config.graphConfig.series.nodeSize,
753
        node,
754
      );
755
    }
756
    return {nodeStyleConfig, nodeSizeConfig, nodeEmphasisConfig};
3✔
757
  }
758

759
  getLinkStyle(link, config, type) {
760
    let linkStyleConfig;
761
    let linkEmphasisConfig = {};
3✔
762
    if (link.category && config.linkCategories.length) {
3✔
763
      const category = config.linkCategories.find(
1✔
764
        (cat) => cat.name === link.category,
1✔
765
      );
766

767
      linkStyleConfig = this.generateStyle(category.linkStyle || {}, link);
1!
768

769
      linkEmphasisConfig = {
1✔
770
        ...linkEmphasisConfig,
771
        linkStyle: category.emphasis
1!
772
          ? this.generateStyle(category.emphasis.linkStyle || {}, link)
1!
773
          : {},
774
      };
775
    } else if (type === "map") {
2✔
776
      linkStyleConfig = this.generateStyle(
1✔
777
        config.mapOptions.linkConfig.linkStyle,
778
        link,
779
      );
780
    } else {
781
      linkStyleConfig = this.generateStyle(
1✔
782
        config.graphConfig.series.linkStyle,
783
        link,
784
      );
785
    }
786

787
    return {linkStyleConfig, linkEmphasisConfig};
3✔
788
  }
789

790
  /**
791
   * @function
792
   * @name showLoading
793
   * display loading animation
794
   *
795
   * @this {object}      netjsongraph
796
   *
797
   * @return {object}    html dom
798
   */
799

800
  showLoading() {
801
    let loadingContainer = this.el.querySelector(".njg-loadingContainer");
3✔
802

803
    if (!loadingContainer) {
3✔
804
      loadingContainer = document.createElement("div");
1✔
805
      loadingContainer.classList.add("njg-loadingContainer");
1✔
806
      loadingContainer.innerHTML = `
1✔
807
        <div class="loadingElement">
808
          <div class="loadingSprite"></div>
809
          <p class="loadingTip">Loading...</p>
810
        </div>
811
      `;
812

813
      this.el.appendChild(loadingContainer);
1✔
814
    } else {
815
      loadingContainer.style.visibility = "visible";
2✔
816
    }
817

818
    return loadingContainer;
3✔
819
  }
820

821
  /**
822
   * @function
823
   * @name hideLoading
824
   * cancel loading animation
825
   *
826
   * @this {object}      netjsongraph
827
   *
828
   * @return {object}    html dom
829
   */
830

831
  hideLoading() {
832
    const loadingContainer = this.el.querySelector(".njg-loadingContainer");
3✔
833

834
    if (loadingContainer) {
3✔
835
      loadingContainer.style.visibility = "hidden";
2✔
836
    }
837

838
    return loadingContainer;
3✔
839
  }
840

841
  createEvent() {
842
    const events = new Map();
11✔
843
    const eventsOnce = new Map();
11✔
844
    return {
11✔
845
      on(key, ...res) {
846
        events.set(key, [...(events.get(key) || []), ...res]);
1✔
847
      },
848
      once(key, ...res) {
849
        eventsOnce.set(key, [...(eventsOnce.get(key) || []), ...res]);
27✔
850
      },
851
      emit(key) {
852
        const funcs = events.get(key) || [];
3✔
853
        const funcsOnce = eventsOnce.get(key) || [];
3✔
854
        const res = funcs.map((func) => func());
3✔
855
        const resOnce = funcsOnce.map((func) => func());
3✔
856
        eventsOnce.delete(key);
3✔
857
        return [...res, ...resOnce];
3✔
858
      },
859
      delete(key) {
860
        events.delete(key);
1✔
861
        eventsOnce.delete(key);
1✔
862
      },
863
    };
864
  }
865
}
866

867
export default NetJSONGraphUtil;
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