Skip to content

Commit 43a3e30

Browse files
updating web gui to work with temoa v4
1 parent 3f54ea7 commit 43a3e30

16 files changed

Lines changed: 4162 additions & 548 deletions

assets/output_files/2026-04-17_125920/Network_Graph_utopia_1990.html

Lines changed: 940 additions & 0 deletions
Large diffs are not rendered by default.

assets/output_files/2026-04-17_125920/Network_Graph_utopia_2000.html

Lines changed: 940 additions & 0 deletions
Large diffs are not rendered by default.

assets/output_files/2026-04-17_125920/Network_Graph_utopia_2010.html

Lines changed: 940 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
document.addEventListener("DOMContentLoaded", function () {
2+
// --- Master Datasets (Read embedded JSON safely) ---
3+
let data = null;
4+
const dataEl = document.getElementById("graph-data");
5+
if (dataEl && dataEl.textContent) {
6+
try {
7+
data = JSON.parse(dataEl.textContent);
8+
} catch (e) {
9+
console.error("Failed to parse graph data JSON:", e);
10+
}
11+
}
12+
13+
// If data failed to load, stop immediately to prevent further errors.
14+
if (!data) {
15+
console.error("Could not find or parse GRAPH_DATA. Halting script.");
16+
return;
17+
}
18+
19+
const {
20+
nodes_json_primary: allNodesPrimary,
21+
edges_json_primary: allEdgesPrimary,
22+
nodes_json_secondary: allNodesSecondary,
23+
edges_json_secondary: allEdgesSecondary,
24+
options_json_str: optionsRaw,
25+
sectors_json_str: allSectors,
26+
color_legend_json_str: colorLegendData,
27+
style_legend_json_str: styleLegendData,
28+
primary_view_name: primaryViewName,
29+
secondary_view_name: secondaryViewName,
30+
} = data;
31+
32+
let optionsObject = {};
33+
if (typeof optionsRaw === "string") {
34+
try {
35+
optionsObject = JSON.parse(optionsRaw);
36+
} catch (e) {
37+
console.error("Failed to parse graph options JSON:", e);
38+
optionsObject = {};
39+
}
40+
} else {
41+
optionsObject = optionsRaw || {};
42+
}
43+
44+
// Expose for debugging only — enable in production.
45+
const isDebug =
46+
(typeof window !== "undefined" && window.DEBUG_GRAPH) ||
47+
(typeof URLSearchParams !== "undefined" &&
48+
new URLSearchParams(window.location.search).has("debugGraph"));
49+
if (isDebug) {
50+
window.__graph = {
51+
data,
52+
allNodesPrimary,
53+
allEdgesPrimary,
54+
allNodesSecondary,
55+
allEdgesSecondary,
56+
optionsObject,
57+
};
58+
}
59+
// --- DOM Elements ---
60+
const fontSizeSlider = document.getElementById("font-size-slider");
61+
const configWrapper = document.getElementById("config-panel-wrapper");
62+
const configHeader = document.querySelector(".config-panel-header");
63+
const configToggleButton = document.querySelector(".config-toggle-btn");
64+
const advancedControlsToggle = document.getElementById("advanced-controls-toggle");
65+
const visConfigContainer = document.getElementById("vis-config-container");
66+
const searchInput = document.getElementById("search-input");
67+
68+
// --- Visual State ---
69+
let currentView = "primary";
70+
let primaryViewPositions = null;
71+
let secondaryViewPositions = null;
72+
let visualState = {
73+
fontSize: optionsObject?.nodes?.font?.size || 14,
74+
};
75+
76+
if (fontSizeSlider) {
77+
fontSizeSlider.value = String(visualState.fontSize);
78+
}
79+
const resetButton = document.getElementById("reset-view-btn");
80+
const sectorTogglesContainer = document.getElementById("sector-toggles");
81+
const viewToggleButton = document.getElementById("view-toggle-btn");
82+
const graphContainer = document.getElementById("mynetwork");
83+
84+
// --- Config Panel Toggle ---
85+
if (optionsObject?.configure?.enabled) {
86+
optionsObject.configure.container = visConfigContainer;
87+
configHeader.addEventListener("click", () => {
88+
const isCollapsed = configWrapper.classList.toggle("collapsed");
89+
configToggleButton.setAttribute("aria-expanded", !isCollapsed);
90+
});
91+
advancedControlsToggle.addEventListener("click", function (e) {
92+
e.preventDefault();
93+
const isHidden = visConfigContainer.style.display === "none";
94+
visConfigContainer.style.display = isHidden ? "block" : "none";
95+
this.textContent = isHidden
96+
? "Hide Advanced Physics Controls"
97+
: "Show Advanced Physics Controls";
98+
});
99+
}
100+
101+
// --- Visual Settings Sliders ---
102+
let pendingRaf = null;
103+
function updateVisualSettings() {
104+
if (fontSizeSlider) visualState.fontSize = parseInt(fontSizeSlider.value, 10);
105+
106+
if (pendingRaf) return;
107+
108+
pendingRaf = requestAnimationFrame(() => {
109+
pendingRaf = null;
110+
111+
// Use setOptions for global font size - works for edges with smooth enabled
112+
// Note: Don't set per-edge font as it breaks rendering with smooth edges
113+
network.setOptions({
114+
nodes: { font: { size: visualState.fontSize } },
115+
edges: { font: { size: visualState.fontSize, align: "top" } },
116+
});
117+
118+
// Also update nodes individually since they have per-node font from addWithCurrentFontSize
119+
// Note: Per-node font properties must be overwritten because they would otherwise take precedence over the global setting
120+
const nodeUpdates = nodes.get().map((n) => ({
121+
id: n.id,
122+
font: { ...(n.font ?? {}), size: visualState.fontSize },
123+
}));
124+
nodes.update(nodeUpdates);
125+
126+
network.redraw();
127+
});
128+
}
129+
130+
if (fontSizeSlider) fontSizeSlider.addEventListener("input", updateVisualSettings);
131+
132+
// --- Vis.js Network Initialization ---
133+
const nodes = new vis.DataSet();
134+
const edges = new vis.DataSet();
135+
const network = new vis.Network(graphContainer, { nodes, edges }, optionsObject);
136+
137+
// --- Core Functions ---
138+
function applyPositions(positions) {
139+
if (!positions) return;
140+
const updates = Object.keys(positions).map((nodeId) => ({
141+
id: nodeId,
142+
x: positions[nodeId].x,
143+
y: positions[nodeId].y,
144+
}));
145+
if (updates.length > 0) nodes.update(updates);
146+
}
147+
148+
function switchView() {
149+
if (currentView === "primary") {
150+
primaryViewPositions = network.getPositions();
151+
} else {
152+
secondaryViewPositions = network.getPositions();
153+
}
154+
nodes.clear();
155+
edges.clear();
156+
157+
if (currentView === "primary") {
158+
addWithCurrentFontSize(allNodesSecondary, allEdgesSecondary);
159+
currentView = "secondary";
160+
viewToggleButton.textContent = `Switch to ${primaryViewName}`;
161+
viewToggleButton.setAttribute("aria-pressed", "true");
162+
applyPositions(secondaryViewPositions);
163+
} else {
164+
addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary);
165+
currentView = "primary";
166+
viewToggleButton.textContent = `Switch to ${secondaryViewName}`;
167+
viewToggleButton.setAttribute("aria-pressed", "false");
168+
applyPositions(primaryViewPositions);
169+
}
170+
applyAllFilters();
171+
network.fit();
172+
}
173+
174+
function applyAllFilters() {
175+
// Preserve current positions so filtering doesn't reset layout
176+
const currentPositions = network.getPositions();
177+
const checkedSectors = new Set(
178+
Array.from(sectorTogglesContainer.querySelectorAll("input:checked")).map((c) => c.value),
179+
);
180+
const searchQuery = searchInput.value;
181+
let regex = null;
182+
if (searchQuery) {
183+
try {
184+
regex = new RegExp(searchQuery, "i");
185+
} catch (e) {
186+
console.error("Invalid Regex:", e);
187+
return;
188+
}
189+
}
190+
const activeNodesData = currentView === "primary" ? allNodesPrimary : allNodesSecondary;
191+
const activeEdgesData = currentView === "primary" ? allEdgesPrimary : allEdgesSecondary;
192+
const sectorFilteredNodes = activeNodesData.filter((node) => {
193+
let match = node.group === null || node.group === undefined || checkedSectors.has(node.group);
194+
if (currentView === "primary" && node.alwaysVisible === true) {
195+
match = true;
196+
}
197+
return match;
198+
});
199+
let visibleNodes, visibleEdges;
200+
if (regex) {
201+
const seedNodes = sectorFilteredNodes.filter((node) => regex.test(node.label || node.id));
202+
const seedNodeIds = new Set(seedNodes.map((n) => n.id));
203+
const nodesToShowIds = new Set(seedNodeIds);
204+
activeEdgesData.forEach((edge) => {
205+
if (seedNodeIds.has(edge.from)) nodesToShowIds.add(edge.to);
206+
if (seedNodeIds.has(edge.to)) nodesToShowIds.add(edge.from);
207+
});
208+
visibleNodes = activeNodesData.filter((node) => nodesToShowIds.has(node.id));
209+
visibleEdges = activeEdgesData.filter(
210+
(edge) => nodesToShowIds.has(edge.from) && nodesToShowIds.has(edge.to),
211+
);
212+
} else {
213+
visibleNodes = sectorFilteredNodes;
214+
const visibleNodeIds = new Set(visibleNodes.map((n) => n.id));
215+
visibleEdges = activeEdgesData.filter(
216+
(edge) => visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to),
217+
);
218+
}
219+
220+
addWithCurrentFontSize(visibleNodes, visibleEdges);
221+
applyPositions(currentPositions);
222+
}
223+
224+
function createStyleLegend() {
225+
const container = document.getElementById("style-legend-container");
226+
if (!container || !styleLegendData || styleLegendData.length === 0) return;
227+
styleLegendData.forEach((itemData) => {
228+
const item = document.createElement("div");
229+
item.className = "legend-item";
230+
const swatch = document.createElement("div");
231+
swatch.className = "legend-color-swatch";
232+
if (itemData.borderColor) swatch.style.borderColor = itemData.borderColor;
233+
if (itemData.borderWidth) swatch.style.borderWidth = itemData.borderWidth + "px";
234+
const label = document.createElement("span");
235+
label.className = "legend-label";
236+
label.textContent = itemData.label;
237+
item.append(swatch, label);
238+
container.appendChild(item);
239+
});
240+
}
241+
242+
function createSectorLegend() {
243+
const container = document.getElementById("legend-container");
244+
if (!container || !colorLegendData || Object.keys(colorLegendData).length === 0) return;
245+
Object.keys(colorLegendData)
246+
.sort()
247+
.forEach((key) => {
248+
const item = document.createElement("div");
249+
item.className = "legend-item";
250+
const swatch = document.createElement("div");
251+
swatch.className = "legend-color-swatch";
252+
swatch.style.backgroundColor = colorLegendData[key];
253+
const label = document.createElement("span");
254+
label.className = "legend-label";
255+
label.textContent = key;
256+
item.append(swatch, label);
257+
container.appendChild(item);
258+
});
259+
}
260+
261+
function createSectorToggles() {
262+
if (!allSectors || allSectors.length === 0) {
263+
sectorTogglesContainer.style.display = "none";
264+
return;
265+
}
266+
allSectors.forEach((sector) => {
267+
const item = document.createElement("div");
268+
item.className = "sector-toggle-item";
269+
const checkbox = document.createElement("input");
270+
checkbox.type = "checkbox";
271+
checkbox.id = `toggle-${sector}`;
272+
checkbox.value = sector;
273+
checkbox.checked = true;
274+
checkbox.addEventListener("change", applyAllFilters);
275+
const swatch = document.createElement("div");
276+
swatch.className = "toggle-color-swatch";
277+
swatch.style.backgroundColor = colorLegendData[sector] || "#ccc";
278+
const label = document.createElement("label");
279+
label.htmlFor = checkbox.id;
280+
label.textContent = sector;
281+
item.append(swatch, checkbox, label);
282+
item.addEventListener("click", (e) => {
283+
if (e.target.tagName === "INPUT") return;
284+
e.preventDefault();
285+
checkbox.checked = !checkbox.checked;
286+
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
287+
});
288+
sectorTogglesContainer.appendChild(item);
289+
});
290+
}
291+
292+
function addWithCurrentFontSize(newNodes, newEdges) {
293+
nodes.clear();
294+
edges.clear();
295+
nodes.add(
296+
newNodes.map((n) => ({
297+
...n,
298+
font: { ...(n.font ?? {}), size: visualState.fontSize },
299+
})),
300+
);
301+
// Don't set per-edge font - let network.setOptions() handle it
302+
// vis.js ignores global font options when edges have per-item font set
303+
edges.add(newEdges);
304+
}
305+
306+
function resetView() {
307+
searchInput.value = "";
308+
primaryViewPositions = null;
309+
secondaryViewPositions = null;
310+
if (currentView !== "primary") {
311+
switchView(); // This will switch back to primary and apply null positions
312+
} else {
313+
// If already on primary, just reload the original data
314+
addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary);
315+
applyPositions(primaryViewPositions); // Apply null to reset
316+
network.fit();
317+
}
318+
sectorTogglesContainer
319+
.querySelectorAll("input[type=checkbox]")
320+
.forEach((c) => (c.checked = true));
321+
applyAllFilters();
322+
}
323+
324+
function showNeighborhood(nodeId) {
325+
const activeNodes = currentView === "primary" ? allNodesPrimary : allNodesSecondary;
326+
const activeEdges = currentView === "primary" ? allEdgesPrimary : allEdgesSecondary;
327+
searchInput.value = "";
328+
const nodesToShow = new Set([nodeId]);
329+
activeEdges.forEach((edge) => {
330+
if (edge.from === nodeId) nodesToShow.add(edge.to);
331+
else if (edge.to === nodeId) nodesToShow.add(edge.from);
332+
});
333+
const filteredNodes = activeNodes.filter((node) => nodesToShow.has(node.id));
334+
const filteredEdges = activeEdges.filter(
335+
(edge) => nodesToShow.has(edge.from) && nodesToShow.has(edge.to),
336+
);
337+
addWithCurrentFontSize(filteredNodes, filteredEdges);
338+
network.fit();
339+
}
340+
341+
// --- Event Listeners & Initial Setup ---
342+
if (allNodesSecondary && allNodesSecondary.length > 0) {
343+
viewToggleButton.textContent = `Switch to ${secondaryViewName}`;
344+
viewToggleButton.addEventListener("click", switchView);
345+
} else {
346+
document.getElementById("view-toggle-panel").style.display = "none";
347+
}
348+
resetButton.addEventListener("click", resetView);
349+
searchInput.addEventListener("input", applyAllFilters);
350+
network.on("doubleClick", (params) => {
351+
if (params.nodes.length > 0) {
352+
showNeighborhood(params.nodes[0]);
353+
}
354+
});
355+
356+
createStyleLegend();
357+
createSectorLegend();
358+
createSectorToggles();
359+
// Initial data load with consistent font handling
360+
addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary);
361+
});

0 commit comments

Comments
 (0)