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.
- {d3graph-3.1.2 → d3graph-3.2.0}/MANIFEST.in +1 -0
- {d3graph-3.1.2/d3graph.egg-info → d3graph-3.2.0}/PKG-INFO +1 -1
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/__init__.py +1 -1
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3graph.py +74 -37
- d3graph-3.2.0/d3graph/d3js/__init__.py +0 -0
- d3graph-3.1.2/d3graph/d3js/style.css → d3graph-3.2.0/d3graph/d3js/d3graph.css +3 -2
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3graph.html.j2 +70 -31
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3graph.js +4 -4
- d3graph-3.2.0/d3graph/d3js/logo.txt +1 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples.py +8 -6
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_rankings.py +2 -2
- {d3graph-3.1.2 → d3graph-3.2.0/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/SOURCES.txt +3 -2
- d3graph-3.1.2/d3graph/d3js/logo.txt +0 -1
- {d3graph-3.1.2 → d3graph-3.2.0}/LICENSE +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/README.md +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_docs.py +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph/examples_rankings_Pvalue.py +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/pyproject.toml +0 -0
- {d3graph-3.1.2 → d3graph-3.2.0}/setup.cfg +0 -0
|
@@ -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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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.
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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['
|
|
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['
|
|
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('
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
15
|
-
|
|
16
|
-
<!--
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
207
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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",
|
|
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.
|