Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/css/netjsongraph-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@
color: #000;
}

/* Dark mode (applied by JS via .njg-dark-mode) */
.njg-container.njg-dark-mode {
color: #e0e0e0;
}

.njg-container.njg-dark-mode .njg-sideBar {
background-color: #1e1e1e;
color: #e0e0e0;
}

.njg-container.njg-dark-mode .njg-metaData,
.njg-container.njg-dark-mode .njg-infoContainer {
color: #e0e0e0;
}

.njg-container.njg-dark-mode .njg-tooltip {
background: #1e1e1e !important;
}

.njg-container.njg-dark-mode .njg-tooltip-key,
.njg-container.njg-dark-mode .njg-tooltip-value {
color: #e0e0e0;
}

.njg-container.njg-dark-mode .leaflet-popup-content-wrapper,
.njg-container.njg-dark-mode .leaflet-popup-tip {
background: #1e1e1e;
color: #e0e0e0;
}

.njg-container.njg-dark-mode .leaflet-control-attribution {
background: rgba(30, 30, 30, 0.8);
color: #e0e0e0;
}

.njg-container.njg-dark-mode .leaflet-control-attribution a {
color: #e0e0e0;
}

/* Node click popup */
.njg-container .leaflet-popup.njg-node-popup .leaflet-popup-content-wrapper,
.njg-container .leaflet-popup.njg-node-popup .leaflet-popup-tip {
background: #fff;
color: #000;
}

.njg-container.njg-dark-mode .leaflet-popup.njg-node-popup .leaflet-popup-content-wrapper,
.njg-container.njg-dark-mode .leaflet-popup.njg-node-popup .leaflet-popup-tip {
background: #1e1e1e;
color: #e0e0e0;
}

Comment on lines +79 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix code formatting to pass Prettier checks.

The pipeline is reporting code style issues. Please run Prettier to format this file:

npx prettier --write src/css/netjsongraph-theme.css
🤖 Prompt for AI Agents
In @src/css/netjsongraph-theme.css around lines 79 - 130, Prettier formatting
errors are present in the CSS block for dark mode selectors (e.g.,
.njg-container.njg-dark-mode, .njg-container.njg-dark-mode .njg-sideBar,
.njg-container.njg-dark-mode .leaflet-popup.njg-node-popup); fix by running
Prettier on the file (npx prettier --write src/css/netjsongraph-theme.css) or
reformat the file to match Prettier rules (consistent spacing, semicolons,
braces, and final newline) so the pipeline passes.

.njg-container .marker-cluster div {
color: #fff;
background-color: rgba(21, 102, 169, 0.8) !important;
Expand Down
39 changes: 39 additions & 0 deletions src/js/netjsongraph.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const NetJSONGraphDefaultConfig = {
showLabelsAtZoomLevel: 13,
showGraphLabelsAtZoom: null,
crs: L.CRS.EPSG3857,
// Dark mode behavior:
// - true: always dark
// - false: always light
// - "auto": follow document theme (html[data-theme="dark"] or html.dark-mode) or OS preference
darkMode: "auto",
echartsOption: {
aria: {
show: true,
Expand Down Expand Up @@ -255,6 +260,16 @@ const NetJSONGraphDefaultConfig = {
attribution: `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors,
tiles offered by <a href="https://www.mapbox.com">Mapbox</a>`,
},
// Optional dark tile layer (used when dark mode is active).
// Can be overridden by providing darkUrlTemplate/darkOptions in user config.
darkUrlTemplate:
process.env.MAPBOX_DARK_URL_TEMPLATE ||
"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
darkOptions: {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, '
+ 'tiles by <a href="https://carto.com/">CARTO</a>',
Comment on lines +270 to +271
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concatenation operator on line 271 uses a plus sign at the start of the next line, which is inconsistent with the concatenation on line 270 where the plus is at the end of the line. For consistency and better readability, move the plus operator to the end of line 270 rather than the beginning of line 271.

Suggested change
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, '
+ 'tiles by <a href="https://carto.com/">CARTO</a>',
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, ' +
'tiles by <a href="https://carto.com/">CARTO</a>',

Copilot uses AI. Check for mistakes.
},
},
],
geoOptions: {
Expand Down Expand Up @@ -287,6 +302,19 @@ const NetJSONGraphDefaultConfig = {
],
linkCategories: [],

// Optional popup on node click (mainly for map / indoor map).
// Disabled by default to preserve existing behavior.
nodePopupOnClick: false,
// Function (node, nodeInfo, instance) => string|HTMLElement
nodePopupContent: null,
// Leaflet popup options passed to L.popup(options)
nodePopupOptions: {
closeButton: true,
autoClose: true,
closeOnClick: true,
className: "njg-node-popup",
},

/**
* @function
* @name prepareData
Expand Down Expand Up @@ -344,6 +372,17 @@ const NetJSONGraphDefaultConfig = {

this.gui.getNodeLinkInfo(type, nodeLinkData);
this.gui.sideBar.classList.remove("hidden");

// Optional Leaflet popup on node click (works for geo map and CRS.Simple indoor map).
if (
type === "node" &&
this.config.nodePopupOnClick &&
this.leaflet &&
this.utils &&
typeof this.utils.showNodePopup === "function"
) {
this.utils.showNodePopup.call(this, data, nodeLinkData);
}
},

/**
Expand Down
35 changes: 35 additions & 0 deletions src/js/netjsongraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,41 @@ class NetJSONGraph {
}
this.utils.hideLoading.call(this);

// Theme support (dark mode): keep map tiles + info UI consistent.
// Applies once on load, and (when darkMode is "auto") tracks document theme changes.
if (this.utils && typeof this.utils.applyTheme === "function") {
this.utils.applyTheme.call(this);

if (this.config && this.config.darkMode === "auto" && !this._njgThemeObserver) {
const apply = () => this.utils.applyTheme.call(this);

// Track html class / data-theme changes (common theme toggles)
try {
const docEl = document.documentElement;
const observer = new MutationObserver(() => apply());
observer.observe(docEl, {attributes: true, attributeFilter: ["class", "data-theme"]});
this._njgThemeObserver = observer;
} catch (e) {
// ignore
}

// Track OS theme changes
try {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => apply();
if (mql && typeof mql.addEventListener === "function") {
mql.addEventListener("change", handler);
this._njgThemeMql = {mql, handler};
} else if (mql && typeof mql.addListener === "function") {
mql.addListener(handler);
this._njgThemeMql = {mql, handler};
}
} catch (e) {
// ignore
}
}
}
Comment on lines +156 to +189
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The theme initialization code lacks test coverage. Since the repository has comprehensive automated testing (test/netjsongraph.dom.test.js, test/netjsongraph.spec.js), the new theme observer setup, MutationObserver configuration, and media query listener registration should have test cases to verify they work correctly and handle edge cases (e.g., when darkMode is "auto" vs boolean values).

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +189
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MutationObserver and media query listeners created for theme tracking are never cleaned up. This can lead to memory leaks if the NetJSONGraph instance is destroyed and recreated multiple times. Consider adding a cleanup method (e.g., destroy() or dispose()) that disconnects the MutationObserver and removes the media query event listeners.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +189
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix ESLint violations and improve observer guard logic.

The pipeline is failing due to no-underscore-dangle violations on the property names _njgThemeObserver and _njgThemeMql.

Additionally, Line 161 guards observer installation by checking only !this._njgThemeObserver, but both observers could already exist independently. If _njgThemeMql exists but _njgThemeObserver doesn't, the media query listener would be added twice.

🔎 Proposed fix

Option 1: Remove underscores (recommended)

-      if (this.config && this.config.darkMode === "auto" && !this._njgThemeObserver) {
+      if (this.config && this.config.darkMode === "auto" && !this.njgThemeObserver) {
         const apply = () => this.utils.applyTheme.call(this);
 
         // Track html class / data-theme changes (common theme toggles)
         try {
           const docEl = document.documentElement;
           const observer = new MutationObserver(() => apply());
           observer.observe(docEl, {attributes: true, attributeFilter: ["class", "data-theme"]});
-          this._njgThemeObserver = observer;
+          this.njgThemeObserver = observer;
         } catch (e) {
           // ignore
         }
 
         // Track OS theme changes
         try {
           const mql = window.matchMedia("(prefers-color-scheme: dark)");
           const handler = () => apply();
           if (mql && typeof mql.addEventListener === "function") {
             mql.addEventListener("change", handler);
-            this._njgThemeMql = {mql, handler};
+            this.njgThemeMql = {mql, handler};
           } else if (mql && typeof mql.addListener === "function") {
             mql.addListener(handler);
-            this._njgThemeMql = {mql, handler};
+            this.njgThemeMql = {mql, handler};
           }
         } catch (e) {
           // ignore
         }
       }

Option 2: If underscores are necessary, disable ESLint for these lines and improve guard

-      if (this.config && this.config.darkMode === "auto" && !this._njgThemeObserver) {
+      // eslint-disable-next-line no-underscore-dangle
+      if (this.config && this.config.darkMode === "auto" && !this._njgThemeObserver && !this._njgThemeMql) {
         const apply = () => this.utils.applyTheme.call(this);
 
         // Track html class / data-theme changes (common theme toggles)
         try {
           const docEl = document.documentElement;
           const observer = new MutationObserver(() => apply());
           observer.observe(docEl, {attributes: true, attributeFilter: ["class", "data-theme"]});
+          // eslint-disable-next-line no-underscore-dangle
           this._njgThemeObserver = observer;
         } catch (e) {
           // ignore
         }
 
         // Track OS theme changes
         try {
           const mql = window.matchMedia("(prefers-color-scheme: dark)");
           const handler = () => apply();
           if (mql && typeof mql.addEventListener === "function") {
             mql.addEventListener("change", handler);
+            // eslint-disable-next-line no-underscore-dangle
             this._njgThemeMql = {mql, handler};
           } else if (mql && typeof mql.addListener === "function") {
             mql.addListener(handler);
+            // eslint-disable-next-line no-underscore-dangle
             this._njgThemeMql = {mql, handler};
           }
         } catch (e) {
           // ignore
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Theme support (dark mode): keep map tiles + info UI consistent.
// Applies once on load, and (when darkMode is "auto") tracks document theme changes.
if (this.utils && typeof this.utils.applyTheme === "function") {
this.utils.applyTheme.call(this);
if (this.config && this.config.darkMode === "auto" && !this._njgThemeObserver) {
const apply = () => this.utils.applyTheme.call(this);
// Track html class / data-theme changes (common theme toggles)
try {
const docEl = document.documentElement;
const observer = new MutationObserver(() => apply());
observer.observe(docEl, {attributes: true, attributeFilter: ["class", "data-theme"]});
this._njgThemeObserver = observer;
} catch (e) {
// ignore
}
// Track OS theme changes
try {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => apply();
if (mql && typeof mql.addEventListener === "function") {
mql.addEventListener("change", handler);
this._njgThemeMql = {mql, handler};
} else if (mql && typeof mql.addListener === "function") {
mql.addListener(handler);
this._njgThemeMql = {mql, handler};
}
} catch (e) {
// ignore
}
}
}
// Theme support (dark mode): keep map tiles + info UI consistent.
// Applies once on load, and (when darkMode is "auto") tracks document theme changes.
if (this.utils && typeof this.utils.applyTheme === "function") {
this.utils.applyTheme.call(this);
if (this.config && this.config.darkMode === "auto" && !this.njgThemeObserver) {
const apply = () => this.utils.applyTheme.call(this);
// Track html class / data-theme changes (common theme toggles)
try {
const docEl = document.documentElement;
const observer = new MutationObserver(() => apply());
observer.observe(docEl, {attributes: true, attributeFilter: ["class", "data-theme"]});
this.njgThemeObserver = observer;
} catch (e) {
// ignore
}
// Track OS theme changes
try {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => apply();
if (mql && typeof mql.addEventListener === "function") {
mql.addEventListener("change", handler);
this.njgThemeMql = {mql, handler};
} else if (mql && typeof mql.addListener === "function") {
mql.addListener(handler);
this.njgThemeMql = {mql, handler};
}
} catch (e) {
// ignore
}
}
}
🧰 Tools
🪛 GitHub Actions: netjsongraph.js CI BUILD

[error] 161-161: eslint: Unexpected dangling '_' in '_njgThemeObserver' (no-underscore-dangle)


[error] 169-169: eslint: Unexpected dangling '_' in '_njgThemeObserver' (no-underscore-dangle)


[error] 180-180: eslint: Unexpected dangling '_' in '_njgThemeMql' (no-underscore-dangle)


[error] 183-183: eslint: Unexpected dangling '_' in '_njgThemeMql' (no-underscore-dangle)

🤖 Prompt for AI Agents
In @src/js/netjsongraph.js around lines 156 - 189, Rename the internal observer
properties to remove leading underscores (e.g., use njgThemeObserver and
njgThemeMql instead of _njgThemeObserver/_njgThemeMql) to satisfy ESLint
no-underscore-dangle, and update the guard/installation logic in the theme setup
so each observer is checked and created independently (check njgThemeObserver
before creating the MutationObserver and check njgThemeMql before adding the
media query listener) to avoid double-registration; ensure you also set the new
property names when assigning the observer and cleanup code uses the new names.


// Expose helper to attach clients overlay for examples or apps
// Not enabled by default to avoid side effects.
this.attachClientsOverlay = (opts) => attachClientsOverlay(this, opts);
Expand Down
175 changes: 175 additions & 0 deletions src/js/netjsongraph.render.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,181 @@ echarts.use([
]);

class NetJSONGraphRender {
/**
* Determine whether dark mode is currently active.
*
* Priority:
* 1) config.darkMode boolean (force)
* 2) document theme markers (html.dark-mode or html[data-theme="dark"])
* 3) OS preference (prefers-color-scheme)
*/
isDarkModeActive() {
if (typeof this.config.darkMode === "boolean") {
return this.config.darkMode;
}

const docEl = document && document.documentElement;
if (docEl) {
if (docEl.classList.contains("dark-mode")) {
return true;
}
const dataTheme = docEl.getAttribute("data-theme");
if (typeof dataTheme === "string" && dataTheme.toLowerCase() === "dark") {
return true;
}
}

try {
return !!(
window &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
} catch (e) {
return false;
}
}

/**
* Swap Leaflet tile layers based on the active theme.
* This only touches base TileLayer instances; overlays (geojson, markers, etc) remain.
*/
updateLeafletTilesForTheme(isDark) {
if (!this.leaflet) {
return;
}

const tiles = Array.isArray(this.config.mapTileConfig)
? this.config.mapTileConfig
: [];
if (!tiles.length) {
return;
}

// Remove existing tile layers
Object.keys(this.leaflet._layers || {}).forEach((k) => {
const layer = this.leaflet._layers[k];
if (layer && layer instanceof L.TileLayer) {
this.leaflet.removeLayer(layer);
}
});

// Remove existing layer control created by us (if any)
if (this.leaflet._njgBaseLayerControl) {
try {
this.leaflet._njgBaseLayerControl.remove();
} catch (e) {
// ignore
}
this.leaflet._njgBaseLayerControl = null;
}

const baseLayers = {};
let baseLayerAdded = false;

tiles.forEach((tile) => {
const urlTemplate =
isDark && tile.darkUrlTemplate ? tile.darkUrlTemplate : tile.urlTemplate;
const options =
isDark && tile.darkOptions ? tile.darkOptions : tile.options;

const tileLayer = L.tileLayer(urlTemplate, options);
if (tile.label) {
if (!baseLayerAdded) {
tileLayer.addTo(this.leaflet);
baseLayerAdded = true;
}
baseLayers[tile.label] = tileLayer;
} else {
tileLayer.addTo(this.leaflet);
}
});

if (tiles.length > 1) {
const layerControlOpts = this.config.layerControl || {};
this.leaflet._njgBaseLayerControl = L.control
.layers(baseLayers, {}, layerControlOpts)
.addTo(this.leaflet);
}
Comment on lines +108 to +124
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layer control is only created when there are multiple tiles (tiles.length > 1), but the baseLayers object is always populated for tiles with labels. If only one tile has a label and others don't, the layer control won't be created but layers will still be added to baseLayers. This logic may not handle all tile configurations correctly. Consider whether a layer control should be created when there's at least one labeled tile, or clarify the intended behavior in a comment.

Copilot uses AI. Check for mistakes.
}

/**
* Apply theme to DOM and map tiles.
*/
applyTheme() {
const isDark = this.utils.isDarkModeActive.call(this);
if (this.el) {
this.el.classList.toggle("njg-dark-mode", !!isDark);
}
// Only maps need tile swapping
if (this.config && this.config.render === this.utils.mapRender) {
this.utils.updateLeafletTilesForTheme.call(this, !!isDark);
}
}

/**
* Resolve popup coordinates from a node-like object.
* Supports:
* - NetJSONGraph nodes: node.properties.location or node.location
* - GeoJSON point array: node.coordinates [lng, lat]
* - GeoJSON feature geometry: node.geometry.coordinates [lng, lat]
*/
getPopupLatLng(node) {
const loc = node?.properties?.location || node?.location;
if (loc && typeof loc.lat === "number" && typeof loc.lng === "number") {
return [loc.lat, loc.lng];
}

if (Array.isArray(node?.coordinates) && node.coordinates.length >= 2) {
return [node.coordinates[1], node.coordinates[0]];
}

if (Array.isArray(node?.geometry?.coordinates) && node.geometry.coordinates.length >= 2) {
return [node.geometry.coordinates[1], node.geometry.coordinates[0]];
}

return null;
}
Comment on lines +141 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix parsing error: Optional chaining may not be supported.

The pipeline reports a parsing error at Line 149: "Unexpected token ." This indicates the optional chaining operator (?.) may not be supported in your current JavaScript environment or build configuration.

🔎 Refactor to avoid optional chaining
  getPopupLatLng(node) {
-    const loc = node?.properties?.location || node?.location;
+    const loc = (node && node.properties && node.properties.location) || (node && node.location);
    if (loc && typeof loc.lat === "number" && typeof loc.lng === "number") {
      return [loc.lat, loc.lng];
    }

-    if (Array.isArray(node?.coordinates) && node.coordinates.length >= 2) {
+    if (node && Array.isArray(node.coordinates) && node.coordinates.length >= 2) {
      return [node.coordinates[1], node.coordinates[0]];
    }

-    if (Array.isArray(node?.geometry?.coordinates) && node.geometry.coordinates.length >= 2) {
+    if (
+      node &&
+      node.geometry &&
+      Array.isArray(node.geometry.coordinates) &&
+      node.geometry.coordinates.length >= 2
+    ) {
      return [node.geometry.coordinates[1], node.geometry.coordinates[0]];
    }

    return null;
  }

Alternatively, ensure your build pipeline includes Babel with the @babel/plugin-proposal-optional-chaining plugin.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Resolve popup coordinates from a node-like object.
* Supports:
* - NetJSONGraph nodes: node.properties.location or node.location
* - GeoJSON point array: node.coordinates [lng, lat]
* - GeoJSON feature geometry: node.geometry.coordinates [lng, lat]
*/
getPopupLatLng(node) {
const loc = node?.properties?.location || node?.location;
if (loc && typeof loc.lat === "number" && typeof loc.lng === "number") {
return [loc.lat, loc.lng];
}
if (Array.isArray(node?.coordinates) && node.coordinates.length >= 2) {
return [node.coordinates[1], node.coordinates[0]];
}
if (Array.isArray(node?.geometry?.coordinates) && node.geometry.coordinates.length >= 2) {
return [node.geometry.coordinates[1], node.geometry.coordinates[0]];
}
return null;
}
getPopupLatLng(node) {
const loc = (node && node.properties && node.properties.location) || (node && node.location);
if (loc && typeof loc.lat === "number" && typeof loc.lng === "number") {
return [loc.lat, loc.lng];
}
if (node && Array.isArray(node.coordinates) && node.coordinates.length >= 2) {
return [node.coordinates[1], node.coordinates[0]];
}
if (
node &&
node.geometry &&
Array.isArray(node.geometry.coordinates) &&
node.geometry.coordinates.length >= 2
) {
return [node.geometry.coordinates[1], node.geometry.coordinates[0]];
}
return null;
}
🧰 Tools
🪛 GitHub Actions: netjsongraph.js CI BUILD

[error] 149-149: eslint: Parsing error: Unexpected token .

🤖 Prompt for AI Agents
In @src/js/netjsongraph.render.js around lines 141 - 163, The getPopupLatLng
function uses optional chaining (e.g., node?.properties?.location) which causes
parse errors in environments without that syntax; replace all optional chaining
in getPopupLatLng with explicit null/undefined checks (e.g., if (node &&
node.properties && node.properties.location) { const loc =
node.properties.location; } and similar guards for node.location,
node.coordinates and node.geometry.coordinates) so the logic and return values
remain the same but without using the ?. operator.


/**
* Show a Leaflet popup for a node (map & indoor map).
*
* @param {object} node Raw node object passed from click handler
* @param {object} nodeInfo Optional processed info object (from utils.nodeInfo)
*/
showNodePopup(node, nodeInfo = null) {
if (!this.leaflet) {
return;
}

const latLng = this.utils.getPopupLatLng.call(this, node);
if (!latLng || Number.isNaN(latLng[0]) || Number.isNaN(latLng[1])) {
// No coordinates: nothing to anchor the popup to.
return;
}

// Determine popup content
let content = null;
if (typeof this.config.nodePopupContent === "function") {
content = this.config.nodePopupContent.call(this, node, nodeInfo, this);
}

if (content === undefined || content === null || content === "") {
const title =
node?.label ||
node?.name ||
node?.properties?.name ||
(node?.id !== undefined && node?.id !== null ? String(node.id) : "Node");
content = `<div class="njg-node-popup-content"><strong>${title}</strong></div>`;
}

const popupOpts = this.config.nodePopupOptions || {};
const popup = L.popup(popupOpts).setLatLng(latLng).setContent(content);
popup.openOn(this.leaflet);
Comment on lines +182 to +199
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential XSS vulnerability: The popup content from user-provided callback (this.config.nodePopupContent) or from node data (node.label, node.name, etc.) is directly passed to Leaflet's setContent() without sanitization. If node data comes from an untrusted source, this could allow script injection. Consider sanitizing the content or documenting that users must sanitize content in their nodePopupContent callback.

Copilot uses AI. Check for mistakes.

this._njgCurrentNodePopup = popup;
}
Comment on lines +165 to +202
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Pop Up notification" in the PR title but this implementation is a popup display feature, not a notification system. A "notification" typically refers to toast messages or alerts, while this is a map popup anchored to coordinates. Consider updating documentation to clarify this is a node information popup feature rather than a notification system to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +202
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new dark mode and popup features lack test coverage. No tests are added for isDarkModeActive(), updateLeafletTilesForTheme(), applyTheme(), getPopupLatLng(), or showNodePopup() methods. Since the repository has comprehensive test files (e.g., test/netjsongraph.render.test.js), these new features should have corresponding test cases to verify the dark mode detection logic, theme switching, and popup functionality work correctly.

Copilot uses AI. Check for mistakes.

/**
* @function
* @name echartsSetOption
Expand Down
Loading
Loading