d3graph 3.1.2__tar.gz → 3.2.0__tar.gz

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 (24) hide show
  1. {d3graph-3.1.2 → d3graph-3.2.0}/MANIFEST.in +1 -0
  2. {d3graph-3.1.2/d3graph.egg-info → d3graph-3.2.0}/PKG-INFO +1 -1
  3. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/__init__.py +1 -1
  4. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3graph.py +74 -37
  5. d3graph-3.2.0/d3graph/d3js/__init__.py +0 -0
  6. d3graph-3.1.2/d3graph/d3js/style.css → d3graph-3.2.0/d3graph/d3js/d3graph.css +3 -2
  7. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3graph.html.j2 +70 -31
  8. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3graph.js +4 -4
  9. d3graph-3.2.0/d3graph/d3js/logo.txt +1 -0
  10. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples.py +8 -6
  11. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_rankings.py +2 -2
  12. {d3graph-3.1.2 → d3graph-3.2.0/d3graph.egg-info}/PKG-INFO +1 -1
  13. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/SOURCES.txt +3 -2
  14. d3graph-3.1.2/d3graph/d3js/logo.txt +0 -1
  15. {d3graph-3.1.2 → d3graph-3.2.0}/LICENSE +0 -0
  16. {d3graph-3.1.2 → d3graph-3.2.0}/README.md +0 -0
  17. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3.v3.js +0 -0
  18. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_docs.py +0 -0
  19. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_rankings_Pvalue.py +0 -0
  20. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/dependency_links.txt +0 -0
  21. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/requires.txt +0 -0
  22. {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/top_level.txt +0 -0
  23. {d3graph-3.1.2 → d3graph-3.2.0}/pyproject.toml +0 -0
  24. {d3graph-3.1.2 → d3graph-3.2.0}/setup.cfg +0 -0
@@ -2,3 +2,4 @@ include d3graph/d3js/*.js
2
2
  include d3graph/d3js/*.css
3
3
  include d3graph/d3js/*.j2
4
4
  include d3graph/d3js/logo.txt
5
+ include d3graph/d3js/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 3.1.2
3
+ Version: 3.2.0
4
4
  Summary: Python package to create interactive network based on d3js.
5
5
  Author-email: Erdogan Taskesen <erdogant@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -17,7 +17,7 @@ from d3graph.d3graph import (
17
17
 
18
18
  __author__ = 'Erdogan Tasksen'
19
19
  __email__ = 'erdogant@gmail.com'
20
- __version__ = '3.1.2'
20
+ __version__ = '3.2.0'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -99,7 +99,7 @@ class d3graph:
99
99
  self.config['curpath'] = os.path.dirname(os.path.abspath(__file__))
100
100
  self.config['d3_library'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/d3.v3.js'))
101
101
  self.config['d3_script'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/d3graph.js'))
102
- self.config['css'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/style.css'))
102
+ self.config['css'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/d3graph.css'))
103
103
 
104
104
  def _clean(self, clean_config: bool = True) -> None:
105
105
  """Clean previous results to ensure correct working."""
@@ -116,7 +116,7 @@ class d3graph:
116
116
  show_slider: bool = True,
117
117
  set_slider: bool = 0,
118
118
  click={'fill': None, 'stroke': 'black', 'size': 1.3, 'stroke-width': 3},
119
- background_color = '#FFFFFF',
119
+ color_background = None,
120
120
  dark_mode = True,
121
121
  notebook: bool = False,
122
122
  save_button: bool = True,
@@ -130,8 +130,10 @@ class d3graph:
130
130
  density_grid_size: int = 60,
131
131
  density_blur: int = 15,
132
132
  density_opacity: float = 0.8,
133
- show_controls: bool = True,
133
+ show_side_panel: bool = True,
134
+ show_top_panel: bool = True,
134
135
  highlight_full_network: bool = True,
136
+ return_html: bool = False,
135
137
  ) -> None:
136
138
  """Build and show the graph.
137
139
 
@@ -162,8 +164,10 @@ class d3graph:
162
164
  * {'fill': 'red', 'stroke': 'black', 'size': 1.3, 'stroke-width': 3}
163
165
  * {'fill': None, 'stroke': '#FFF000', 'size': 2, 'stroke-width': 1}
164
166
  * None : No action on click.
165
- background_color : str, optional
166
- The background color of the HTML page and SVG. Default is '#FFFFFF'.
167
+ color_background : list or str, optional
168
+ Background for page, top panel, and side panels.
169
+ ``[dark_hex, light_hex]``, or a named preset (e.g. ``'streamlit'``).
170
+ None uses theme defaults.
167
171
  dark_mode : bool, optional
168
172
  If True, enables dark mode for the visualization. Default is False.
169
173
  notebook : bool
@@ -190,19 +194,9 @@ class d3graph:
190
194
  Above this many visible edges, edges are drawn on a <canvas> layer instead of
191
195
  as individual SVG <line> elements. SVG's per-element DOM overhead is what makes
192
196
  tens of thousands of edges freeze the page; canvas draw calls stay cheap
193
- regardless of edge count. Nodes always stay SVG (drag/click/tooltips). Only
194
- applies once edges exceed this count, so small/medium graphs are unaffected -
195
- note that in canvas mode, the "Save as SVG" export won't include edges, since
196
- they no longer live in the SVG DOM.
197
+ regardless of edge count.
197
198
  show_density : bool, (default: False)
198
- Adds a node-clustering heatmap layer (grid-binned density of node positions),
199
- drawn on its own canvas beneath the edges and nodes, with a toggle button in
200
- the UI ("Show/Hide Density") to turn it on/off regardless of this default.
201
- Recomputed from live node positions each frame it's visible, so it tracks the
202
- force layout as nodes settle, and it responds to the weight/component sliders
203
- since it's based on whichever nodes are currently on screen. Color scheme is
204
- a yellow-to-red heat gradient in light mode, single-hue blue in dark mode
205
- (updates live when dark mode is toggled).
199
+ Adds a node-clustering heatmap layer (grid-binned density of node positions).
206
200
  density_grid_size : int, (default: 40)
207
201
  Grid resolution for the density heatmap (cells along the longer axis of the
208
202
  node bounding box). Higher = finer detail on tight clusters, more cells to draw.
@@ -211,21 +205,18 @@ class d3graph:
211
205
  blocky grid.
212
206
  density_opacity : float, (default: 0.6)
213
207
  Maximum heatmap opacity, reached at the highest-density grid cell.
214
- show_controls : bool, (default: True)
215
- Whether to render the top-panel buttons (Dark Mode, Hide Edges, Show Density,
216
- Save) at all. Set to False when embedding the generated HTML into an existing
217
- page/app that provides its own UI chrome and doesn't need d3graph's built-in
218
- controls - the buttons (and their JS wiring) are omitted entirely rather than
219
- just hidden, so there's no extra DOM/clutter in the embed. The weight/component
220
- sliders are controlled separately via show_slider; the Save button specifically
221
- is also controlled via save_button, independent of this.
208
+ show_side_panel : bool, (default: True)
209
+ Render the side panels (Export, Network Statistic, Physics, etc.).
210
+ Set to False when embedding the generated HTML into an existing page/app that provides its own UI chrome (streamlit).
211
+ show_top_panel : bool, (default: True)
212
+ Render the top chrome (logo, search field, and the Dark Mode / Hide Edges / Show Density buttons).
222
213
  highlight_full_network : bool, (default: True)
223
214
  Controls how much of the graph lights up when a node is clicked.
224
- True: The entire connected network reachable from the clicked node (every node
225
- and edge in its connected component, not just its direct neighbors) is
226
- highlighted; everything outside that component is dimmed.
227
- False: Only the clicked node's directly-connected neighbors/edges (one hop)
228
- are highlighted, matching the old default behavior.
215
+ True: The entire connected network reachable from the clicked node is highlighted; everything outside that component is dimmed.
216
+ False: Only the clicked node's directly-connected neighbors/edges (one hop) are highlighted, matching the old default behavior.
217
+ return_html : bool, (default: False)
218
+ * True: Return the generated HTML string (useful for Streamlit, notebooks, embedding).
219
+ * False: Do not return HTML.
229
220
 
230
221
  Returns
231
222
  -------
@@ -237,6 +228,10 @@ class d3graph:
237
228
  logger.warning('No graph detected. <return> Hint: "d3.graph(df)"')
238
229
  return None
239
230
 
231
+ # Resolve color
232
+ bg_dark, bg_light = resolve_color_background(color_background)
233
+
234
+ # Save to config
240
235
  self.config['figsize'] = figsize
241
236
  self.config['network_title'] = title
242
237
  self.config['show_slider'] = show_slider
@@ -245,7 +240,7 @@ class d3graph:
245
240
  self.config['notebook'] = notebook
246
241
  self.config['click'] = click
247
242
  self.config['save_button'] = save_button
248
- self.config['background_color'] = background_color
243
+ self.config['color_background'] = [bg_dark, bg_light]
249
244
  self.config['dark_mode'] = dark_mode
250
245
  self.config['node_text_inside'] = node_text_inside
251
246
  self.config['max_ticks'] = max_ticks
@@ -255,7 +250,8 @@ class d3graph:
255
250
  self.config['density_grid_size'] = density_grid_size
256
251
  self.config['density_blur'] = density_blur
257
252
  self.config['density_opacity'] = density_opacity
258
- self.config['show_controls'] = show_controls
253
+ self.config['show_top_panel'] = show_top_panel
254
+ self.config['show_side_panel'] = show_side_panel
259
255
  self.config['highlight_full_network'] = highlight_full_network
260
256
 
261
257
  # Allow show() to override the link_tension set at __init__ time
@@ -273,11 +269,14 @@ class d3graph:
273
269
  # Make slider
274
270
  self.setup_slider()
275
271
  # Create json
276
- json_data = json_create(self.G, compute_stats=self.config.get('show_controls', True))
272
+ json_data = json_create(self.G, compute_stats=self.config.get('show_side_panel', True))
277
273
  # Create html with json file embedded
278
274
  html = self.write_html(json_data, overwrite=overwrite)
279
275
  # Display the chart
280
- return self.display(html)
276
+ self.display(html)
277
+ # return html
278
+ if return_html:
279
+ return html
281
280
 
282
281
  def display(self, html):
283
282
  """Display."""
@@ -886,7 +885,11 @@ class d3graph:
886
885
  'density_grid_size': self.config.get('density_grid_size', 40),
887
886
  'density_blur': self.config.get('density_blur', 8),
888
887
  'density_opacity': self.config.get('density_opacity', 0.6),
889
- 'show_controls': self.config.get('show_controls', True),
888
+ 'show_side_panel': self.config.get('show_side_panel', True),
889
+ 'show_top_panel': self.config.get('show_top_panel', True),
890
+ # Boolean flag required by the template (HTML comment wrappers
891
+ # alone are not enough — Jinja needs a real truthy value).
892
+ 'show_slider': bool(self.config.get('show_slider', True)),
890
893
  'highlight_full_network': self.config.get('highlight_full_network', True),
891
894
  'save_button': self.config['save_button'],
892
895
  'node_text_inside': self.config.get('node_text_inside', False),
@@ -900,7 +903,8 @@ class d3graph:
900
903
  'slider_comment_stop': show_slider[1],
901
904
  'SET_SLIDER': self.config['set_slider'],
902
905
  'SUPPORT': support,
903
- 'background_color': self.config['background_color'],
906
+ 'COLOR_BACKGROUND_DARK': self.config['color_background'][0],
907
+ 'COLOR_BACKGROUND_LIGHT': self.config['color_background'][1],
904
908
  'dark_mode': self.config['dark_mode'],
905
909
  }
906
910
 
@@ -1358,7 +1362,7 @@ def json_create(G: nx.Graph, compute_stats: bool = True) -> str:
1358
1362
  per node and embed them as node_pagerank / node_hits_hub / node_hits_authority /
1359
1363
  node_degree_centrality / node_closeness_centrality / node_betweenness_centrality,
1360
1364
  normalized to [0, 1], for the interactive stats side panel. Skipped when the
1361
- panel won't be shown (show_controls=False) to avoid the wasted computation.
1365
+ panel won't be shown (show_side_panel=False) to avoid the wasted computation.
1362
1366
  Betweenness is approximated (sampled) above 500 nodes to bound generation time.
1363
1367
 
1364
1368
  Returns
@@ -2267,6 +2271,39 @@ def _set_node_fontcolor(self, fontcolor, color, node_names, nodecount):
2267
2271
  # return
2268
2272
  return fontcolor
2269
2273
 
2274
+
2275
+ def resolve_color_background(color_background):
2276
+ """Normalize color_background to (dark_hex, light_hex).
2277
+
2278
+ Accepts:
2279
+ * None → theme defaults
2280
+ * 'streamlit' → Streamlit app theme
2281
+ * 'darkblue' → dark navy / soft light blue-gray
2282
+ * [dark, light] list/tuple of two hex strings
2283
+ * single hex str → same color for both modes (backwards compatible)
2284
+ """
2285
+ # Theme defaults for center / top / side panel backgrounds (match scatter.css --bg).
2286
+ _DEFAULT_BG_DARK = '#222222'
2287
+ _DEFAULT_BG_LIGHT = '#ffffff'
2288
+
2289
+ if color_background is None:
2290
+ return _DEFAULT_BG_DARK, _DEFAULT_BG_LIGHT
2291
+ if isinstance(color_background, str) and color_background == 'streamlit':
2292
+ return "#0E1117", "#FFFFFF"
2293
+ if isinstance(color_background, str) and color_background == 'darkblue':
2294
+ return "#12141c", "#e8eef5"
2295
+ if isinstance(color_background, str):
2296
+ c = color_background.strip()
2297
+ if not c:
2298
+ return _DEFAULT_BG_DARK, _DEFAULT_BG_LIGHT
2299
+ return c, c
2300
+ if isinstance(color_background, (list, tuple)) and len(color_background) >= 2:
2301
+ dark = color_background[0] if color_background[0] else _DEFAULT_BG_DARK
2302
+ light = color_background[1] if color_background[1] else _DEFAULT_BG_LIGHT
2303
+ return dark, light
2304
+ return _DEFAULT_BG_DARK, _DEFAULT_BG_LIGHT
2305
+
2306
+
2270
2307
  def import_example(data='energy', url=None, sep=','):
2271
2308
  """Import example dataset from github source.
2272
2309
 
File without changes
@@ -197,7 +197,8 @@ h3 {
197
197
 
198
198
 
199
199
  .dark-mode {
200
- background-color: #222 !important;
200
+ /* Background is set via COLOR_BACKGROUND_DARK in d3graph.html.j2
201
+ (from color_background / resolve_color_background). */
201
202
  color: #eee !important;
202
203
  }
203
204
 
@@ -333,7 +334,7 @@ h3 {
333
334
  }
334
335
 
335
336
  .dark-mode #graphContainer {
336
- background-color: #222 !important;
337
+ /* Background is set via COLOR_BACKGROUND_DARK in d3graph.html.j2. */
337
338
  }
338
339
  .dark-mode .button-switch {
339
340
  background: #333;
@@ -8,22 +8,56 @@
8
8
  {{ SUPPORT }}
9
9
 
10
10
  <style type="text/css">
11
- {% include "style.css" %}
11
+ {% include "d3graph.css" %}
12
+ </style>
13
+ <!-- Page + header row + side panels: per-theme backgrounds from
14
+ color_background (resolved to dark/light hex in Python).
15
+ Theme toggle (body.dark-mode) switches which color is used. -->
16
+ <style>
17
+ body {
18
+ background: {{ COLOR_BACKGROUND_LIGHT }};
19
+ }
20
+ body .header-row {
21
+ background: {{ COLOR_BACKGROUND_LIGHT }};
22
+ }
23
+ body .stats-panel {
24
+ background: {{ COLOR_BACKGROUND_LIGHT }};
25
+ }
26
+ body.dark-mode {
27
+ background: {{ COLOR_BACKGROUND_DARK }};
28
+ color: #eee;
29
+ }
30
+ body.dark-mode .header-row {
31
+ background: {{ COLOR_BACKGROUND_DARK }};
32
+ }
33
+ body.dark-mode .stats-panel {
34
+ background: {{ COLOR_BACKGROUND_DARK }};
35
+ }
36
+ body.dark-mode #graphContainer {
37
+ background-color: {{ COLOR_BACKGROUND_DARK }};
38
+ }
12
39
  </style>
13
40
  </head>
14
- <body style="background-color: {{ background_color }};{% if dark_mode %} color: #eee;{% endif %}"{% if dark_mode %} class="dark-mode"{% endif %}>
15
-
16
- <!-- Top row: edge-threshold slider (centered) + top-panel buttons (right).
17
- The slider's visibility is governed by show_slider, not show_controls,
18
- so it still renders even when show_controls=False (e.g. embedding this
19
- HTML into a page that supplies its own button chrome but still wants
20
- the slider). -->
21
- {% if show_controls or max_slider > min_slider %}
41
+ <body{% if dark_mode %} class="dark-mode"{% endif %}>
42
+
43
+ <!-- Header row visibility:
44
+ - show_top_panel=True → logo, search, buttons (and slider if show_slider)
45
+ - show_top_panel=False no logo / search / buttons
46
+ - show_slider=True and a usable weight range slider only (even when
47
+ show_top_panel is False), so embeds can keep the threshold control
48
+ without the rest of the top chrome.
49
+ Side panels are independent (show_side_panel) and do not force this row.
50
+ NOTE: show_slider must be passed as a real bool from Python (not only via
51
+ HTML comment wrappers), otherwise Jinja treats it as undefined/falsy and
52
+ the slider never renders. -->
53
+ {% set _show_slider = show_slider if show_slider is defined else true %}
54
+ {% if show_top_panel or (_show_slider and max_slider > min_slider) %}
22
55
  <div class="header-row">
23
-
56
+
57
+ {% if show_top_panel %}
24
58
  <div class="header-row-left">
25
59
  <div class="top-panel-left">
26
- <a href="https://d3blocks.github.io/d3blocks/" title="D3Blocks">
60
+ <a href="https://d3blocks.github.io/d3blocks/" title="D3Blocks" target="_blank" rel="noopener noreferrer">
27
61
  <img src="{% include 'logo.txt' %}" alt="D3Blocks" style="width:150px;height:auto;display:block;pointer-events:auto" />
28
62
  </a>
29
63
  </div>
@@ -35,9 +69,9 @@
35
69
  <div class="search-results" id="searchResults"></div>
36
70
  </div>
37
71
  </div>
72
+ {% endif %}
38
73
 
39
- {% if max_slider > min_slider %}
40
- {{ slider_comment_start }}
74
+ {% if _show_slider and max_slider > min_slider %}
41
75
  <div class="slider-container">
42
76
  <form style="margin:0;">
43
77
  <span class="slider-label">Edge Threshold: <span id="thresholdValue">{{ SET_SLIDER }}</span></span>
@@ -46,21 +80,20 @@
46
80
  <span class="slider-label">{{ max_slider }}</span>
47
81
  </form>
48
82
  </div>
49
- {{ slider_comment_stop }}
50
83
  {% endif %}
51
- {% if show_controls %}
52
84
 
53
- <div class="top-panel">
54
- <button id="darkModeSwitch" class="button-switch">{% if dark_mode %}☀ Light Mode{% else %}🌙 Dark Mode{% endif %}</button>
55
- <button id="edgeToggleButton" class="button-switch">Hide Edges</button>
56
- <button id="densityToggleButton" class="button-switch">{% if show_density %}Hide Density{% else %}Show Density{% endif %}</button>
57
- </div>
85
+ {% if show_top_panel %}
86
+ <div class="top-panel">
87
+ <button id="darkModeSwitch" class="button-switch">{% if dark_mode %}☀ Light Mode{% else %}🌙 Dark Mode{% endif %}</button>
88
+ <button id="edgeToggleButton" class="button-switch">Hide Edges</button>
89
+ <button id="densityToggleButton" class="button-switch">{% if show_density %}Hide Density{% else %}Show Density{% endif %}</button>
90
+ </div>
91
+ {% endif %}
58
92
 
59
- {% endif %}
60
93
  </div>
61
94
  {% endif %}
62
95
 
63
- {% if show_controls %}
96
+ {% if show_side_panel %}
64
97
  <!-- Side panel: data/image export actions, previously loose buttons in the
65
98
  top-panel row — grouped here since they're one-off actions rather than
66
99
  persistent view controls, and it keeps the top-panel row shorter. -->
@@ -177,7 +210,9 @@
177
210
  collision: {{ collision }},
178
211
  link_tension: {{ link_tension }},
179
212
  sticky: {{ sticky | lower }},
180
- background_color: '{{ background_color }}',
213
+ color_background: '{{ COLOR_BACKGROUND_DARK if dark_mode else COLOR_BACKGROUND_LIGHT }}',
214
+ COLOR_BACKGROUND_DARK: '{{ COLOR_BACKGROUND_DARK }}',
215
+ COLOR_BACKGROUND_LIGHT: '{{ COLOR_BACKGROUND_LIGHT }}',
181
216
  node_text_inside: {{ node_text_inside | lower }},
182
217
  max_ticks: {{ max_ticks }},
183
218
  canvas_edge_threshold: {{ canvas_edge_threshold }},
@@ -203,8 +238,10 @@
203
238
  function positionSidePanels() {
204
239
  var headerRow = document.querySelector('.header-row') || document.querySelector('.top-panel');
205
240
  var panels = document.querySelectorAll('.stats-panel');
206
- if (!headerRow || !panels.length) return;
207
- var y = headerRow.getBoundingClientRect().bottom + 8;
241
+ if (!panels.length) return;
242
+ // No top chrome at all (show_top_panel=False and no slider): stack
243
+ // side panels from the top of the viewport instead of bailing out.
244
+ var y = headerRow ? (headerRow.getBoundingClientRect().bottom + 8) : 10;
208
245
  panels.forEach(function (panel) {
209
246
  panel.style.top = y + 'px';
210
247
  panel.style.left = '10px';
@@ -262,24 +299,26 @@
262
299
  });
263
300
  });
264
301
 
265
- // Dark mode toggle logic
302
+ // Dark mode toggle logic — backgrounds come from color_background
303
+ // (resolved in Python to COLOR_BACKGROUND_DARK / COLOR_BACKGROUND_LIGHT).
266
304
  const darkSwitch = document.getElementById('darkModeSwitch');
305
+ const BG_DARK = '{{ COLOR_BACKGROUND_DARK }}';
306
+ const BG_LIGHT = '{{ COLOR_BACKGROUND_LIGHT }}';
267
307
  let darkMode = {{ 'true' if dark_mode else 'false' }};
268
- let originalBg = document.body.style.backgroundColor;
269
308
  function setDarkMode(on) {
270
309
  if (on) {
271
310
  document.body.classList.add('dark-mode');
272
- document.body.style.backgroundColor = '#222';
311
+ document.body.style.backgroundColor = BG_DARK;
273
312
  document.body.style.color = '#eee';
274
313
  let graphBg = document.getElementById('graphContainer');
275
- if (graphBg) graphBg.style.backgroundColor = '#222';
314
+ if (graphBg) graphBg.style.backgroundColor = BG_DARK;
276
315
  if (darkSwitch) darkSwitch.textContent = '☀ Light Mode';
277
316
  } else {
278
317
  document.body.classList.remove('dark-mode');
279
- document.body.style.backgroundColor = originalBg;
318
+ document.body.style.backgroundColor = BG_LIGHT;
280
319
  document.body.style.color = '';
281
320
  let graphBg = document.getElementById('graphContainer');
282
- if (graphBg) graphBg.style.backgroundColor = originalBg;
321
+ if (graphBg) graphBg.style.backgroundColor = BG_LIGHT;
283
322
  if (darkSwitch) darkSwitch.textContent = '🌙 Dark Mode';
284
323
  }
285
324
  if (window.d3graphSetDarkMode) window.d3graphSetDarkMode(on);
@@ -8,7 +8,7 @@ function d3graphscript(config = {
8
8
  collision: 0.5,
9
9
  link_tension: 1,
10
10
  sticky: false,
11
- background_color: '#FFFFFF',
11
+ color_background: '#FFFFFF',
12
12
  node_text_inside: false,
13
13
  max_ticks: 300,
14
14
  label_zoom_threshold: 0.6,
@@ -35,7 +35,7 @@ function d3graphscript(config = {
35
35
  height = window.innerHeight - headerHeight;
36
36
  }
37
37
 
38
- var background_color = config.background_color || '#FFFFFF';
38
+ var color_background = config.color_background || '#FFFFFF';
39
39
  var sticky = config.sticky || false;
40
40
  // Cap how many simulation ticks run before auto-stopping, instead of
41
41
  // letting a large graph's force layout cool down naturally over
@@ -181,7 +181,7 @@ function d3graphscript(config = {
181
181
  var densityCanvasEl, densityCtx, densityOffscreen;
182
182
 
183
183
  // Set the body background color
184
- document.body.style.backgroundColor = background_color;
184
+ document.body.style.backgroundColor = color_background;
185
185
 
186
186
  //Set up the colour scale
187
187
  var color = d3.scale.category20();
@@ -385,7 +385,7 @@ function d3graphscript(config = {
385
385
  .style("position", "relative")
386
386
  .style("width", width + "px")
387
387
  .style("height", height + "px")
388
- .style("background-color", background_color);
388
+ .style("background-color", color_background);
389
389
 
390
390
  // Density (clustering heatmap) layer — created first so it stacks
391
391
  // beneath both the edge canvas and the SVG node layer.