-
Notifications
You must be signed in to change notification settings - Fork 62
font size slider for visualizer #256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9c74074
2e63ad0
2b7148b
64f0278
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,10 +175,15 @@ def calculate_initial_positions( | |
| return positions | ||
|
|
||
| # Arrange sector "anchors" in a large circle | ||
| layout_radius = 2000 # The radius of the main circle for sectors | ||
| jitter_radius = 1000 # How far nodes can be from their sector anchor | ||
| sector_anchors = {} | ||
| # Scale radius based on the number of sectors and nodes to handle small models better | ||
| num_sectors = len(sectors_to_place) | ||
| num_nodes = len(nodes_to_place) | ||
|
|
||
| # Base radius + incremental scaling | ||
| layout_radius = max(800, min(2000, 400 + 200 * num_sectors + 2 * num_nodes)) | ||
| jitter_radius = layout_radius // 2 | ||
|
|
||
| sector_anchors = {} | ||
|
|
||
| for i, sector in enumerate(sectors_to_place): | ||
| angle = (i / num_sectors) * 2 * math.pi | ||
|
|
@@ -218,42 +223,58 @@ def calculate_tech_graph_positions( | |
| """ | ||
| positions = {} | ||
|
|
||
| # Materialize the iterable to avoid consumption issues | ||
| all_edges_list = list(all_edges) | ||
|
|
||
| # 1. Identify all unique sectors present in the technology list | ||
| sectors_to_place = sorted({tech.sector for tech in all_edges if tech.sector}) | ||
| sectors_to_place = sorted({edge.sector for edge in all_edges_list if edge.sector}) | ||
|
|
||
| if not sectors_to_place: | ||
| # If no sectors, just return empty positions and let physics handle it | ||
| return {} | ||
|
|
||
| # 2. Arrange sector "anchors" in a large circle | ||
| layout_radius = 2500 # Use a large radius to ensure initial separation | ||
| jitter_radius = 600 # Controls the size of the initial clusters | ||
| sector_anchors = {} | ||
| # Scale radius based on the number of sectors and unique technologies | ||
| unique_techs_to_place = sorted( | ||
| {edge.tech for edge in all_edges_list if edge.tech}, key=lambda t: str(t) | ||
| ) | ||
| num_sectors = len(sectors_to_place) | ||
| num_nodes = len(unique_techs_to_place) | ||
|
|
||
| if not unique_techs_to_place: | ||
| return {} | ||
|
|
||
| layout_radius = max(1000, min(2500, 500 + 300 * num_sectors + 5 * num_nodes)) | ||
| jitter_radius = layout_radius // 4 | ||
|
|
||
| sector_anchors = {} | ||
|
|
||
| for i, sector in enumerate(sectors_to_place): | ||
| angle = (i / num_sectors) * 2 * math.pi | ||
| cx = layout_radius * math.cos(angle) | ||
| cy = layout_radius * math.sin(angle) | ||
| sector_anchors[sector] = (cx, cy) | ||
|
|
||
| # 3. Place each technology node near its sector's anchor point with jitter | ||
| for edge_tuple in all_edges: | ||
| primary_sector = edge_tuple.sector | ||
| # 3. Place each unique technology node near its sector's anchor point with jitter | ||
| # Create a mapping of tech to its primary sector from the edges | ||
| tech_to_sector = {edge.tech: edge.sector for edge in all_edges_list if edge.tech} | ||
|
|
||
| for tech in unique_techs_to_place: | ||
| primary_sector = tech_to_sector.get(tech) | ||
|
Comment on lines
+258
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve the first non-empty sector for each technology.
🔧 Proposed fix- tech_to_sector = {edge.tech: edge.sector for edge in all_edges_list if edge.tech}
+ tech_to_sector = {}
+ for edge in all_edges_list:
+ if edge.tech and edge.sector and edge.tech not in tech_to_sector:
+ tech_to_sector[edge.tech] = edge.sector🤖 Prompt for AI Agents |
||
| if not primary_sector or primary_sector not in sector_anchors: | ||
| # Place nodes without a defined sector at the center | ||
| cx, cy = 0, 0 | ||
| else: | ||
| cx, cy = sector_anchors[primary_sector] | ||
|
|
||
| # Apply deterministic "jitter" to prevent stacking (stable per-tech) | ||
| seed = uuid.uuid5(uuid.NAMESPACE_DNS, str(edge_tuple.tech)).int | ||
| seed = uuid.uuid5(uuid.NAMESPACE_DNS, str(tech)).int | ||
| rng = random.Random(seed) | ||
| rand_angle = rng.uniform(0, 2 * math.pi) | ||
| rand_radius = rng.uniform(0, jitter_radius) | ||
| x = cx + rand_radius * math.cos(rand_angle) | ||
| y = cy + rand_radius * math.sin(rand_angle) | ||
|
|
||
| positions[edge_tuple.tech] = {'x': x, 'y': y} | ||
| positions[tech] = {'x': x, 'y': y} | ||
|
|
||
| return positions | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,26 +28,59 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| primary_view_name: primaryViewName, | ||||||
| secondary_view_name: secondaryViewName, | ||||||
| } = data; | ||||||
| const optionsObject = (typeof optionsRaw === 'string') ? JSON.parse(optionsRaw) : optionsRaw; | ||||||
| // --- State --- | ||||||
| let currentView = 'primary'; | ||||||
| let primaryViewPositions = null; | ||||||
| let secondaryViewPositions = null; | ||||||
|
|
||||||
| let optionsObject = {}; | ||||||
| if (typeof optionsRaw === "string") { | ||||||
| try { | ||||||
| optionsObject = JSON.parse(optionsRaw); | ||||||
| } catch (e) { | ||||||
| console.error('Failed to parse graph options JSON:', e); | ||||||
| optionsObject = {}; | ||||||
| } | ||||||
| } else { | ||||||
| optionsObject = optionsRaw || {}; | ||||||
| } | ||||||
|
|
||||||
| // Expose for debugging only — enable in production. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo in comment: "enable in production" should be "disable in production". The comment says "enable in production" but the intent is clearly to enable debugging only during development. 🔧 Proposed fix- // Expose for debugging only — enable in production.
+ // Expose for debugging only — disable in production.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| const isDebug = (typeof window !== 'undefined' && window.DEBUG_GRAPH) || | ||||||
| (typeof URLSearchParams !== 'undefined' && new URLSearchParams(window.location.search).has('debugGraph')); | ||||||
| if (isDebug) { | ||||||
| window.__graph = { | ||||||
| data, | ||||||
| allNodesPrimary, | ||||||
| allEdgesPrimary, | ||||||
| allNodesSecondary, | ||||||
| allEdgesSecondary, | ||||||
| optionsObject, | ||||||
| }; | ||||||
| } | ||||||
| // --- DOM Elements --- | ||||||
| const fontSizeSlider = document.getElementById('font-size-slider'); | ||||||
| const configWrapper = document.getElementById('config-panel-wrapper'); | ||||||
| const configHeader = document.querySelector('.config-panel-header'); | ||||||
| const configToggleButton = document.querySelector('.config-toggle-btn'); | ||||||
| const advancedControlsToggle = document.getElementById('advanced-controls-toggle'); | ||||||
| const visConfigContainer = document.getElementById('vis-config-container'); | ||||||
| const searchInput = document.getElementById('search-input'); | ||||||
|
|
||||||
| // --- Visual State --- | ||||||
| let currentView = 'primary'; | ||||||
| let primaryViewPositions = null; | ||||||
| let secondaryViewPositions = null; | ||||||
| let visualState = { | ||||||
| fontSize: (optionsObject?.nodes?.font?.size) || 14 | ||||||
| }; | ||||||
|
|
||||||
| if (fontSizeSlider) { | ||||||
| fontSizeSlider.value = String(visualState.fontSize); | ||||||
| } | ||||||
| const resetButton = document.getElementById('reset-view-btn'); | ||||||
| const sectorTogglesContainer = document.getElementById('sector-toggles'); | ||||||
| const viewToggleButton = document.getElementById('view-toggle-btn'); | ||||||
| const graphContainer = document.getElementById('mynetwork'); | ||||||
|
|
||||||
| // --- Config Panel Toggle --- | ||||||
| if (optionsObject.configure && optionsObject.configure.enabled) { | ||||||
| if (optionsObject?.configure?.enabled) { | ||||||
| optionsObject.configure.container = visConfigContainer; | ||||||
| configHeader.addEventListener('click', () => { | ||||||
| const isCollapsed = configWrapper.classList.toggle('collapsed'); | ||||||
|
|
@@ -61,9 +94,41 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| }); | ||||||
| } | ||||||
|
|
||||||
| // --- Visual Settings Sliders --- | ||||||
| let pendingRaf = null; | ||||||
| function updateVisualSettings() { | ||||||
| if (fontSizeSlider) visualState.fontSize = parseInt(fontSizeSlider.value, 10); | ||||||
|
|
||||||
| if (pendingRaf) return; | ||||||
|
|
||||||
| pendingRaf = requestAnimationFrame(() => { | ||||||
| pendingRaf = null; | ||||||
|
|
||||||
| // Use setOptions for global font size - works for edges with smooth enabled | ||||||
| // Note: Don't set per-edge font as it breaks rendering with smooth edges | ||||||
| network.setOptions({ | ||||||
| nodes: { font: { size: visualState.fontSize } }, | ||||||
| edges: { font: { size: visualState.fontSize, align: 'top' } } | ||||||
| }); | ||||||
|
|
||||||
| // Also update nodes individually since they have per-node font from addWithCurrentFontSize | ||||||
| // Note: Per-node font properties must be overwritten because they would otherwise take precedence over the global setting | ||||||
| const nodeUpdates = nodes.get().map(n => ({ | ||||||
| id: n.id, | ||||||
| font: { ...(n.font ?? {}), size: visualState.fontSize } | ||||||
| })); | ||||||
| nodes.update(nodeUpdates); | ||||||
|
|
||||||
| network.redraw(); | ||||||
| }); | ||||||
| } | ||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| if (fontSizeSlider) fontSizeSlider.addEventListener('input', updateVisualSettings); | ||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
|
|
||||||
| // --- Vis.js Network Initialization --- | ||||||
| const nodes = new vis.DataSet(allNodesPrimary); | ||||||
| const edges = new vis.DataSet(allEdgesPrimary); | ||||||
| const nodes = new vis.DataSet(); | ||||||
| const edges = new vis.DataSet(); | ||||||
| const network = new vis.Network(graphContainer, { nodes, edges }, optionsObject); | ||||||
|
|
||||||
| // --- Core Functions --- | ||||||
|
|
@@ -84,13 +149,13 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| nodes.clear(); edges.clear(); | ||||||
|
|
||||||
| if (currentView === 'primary') { | ||||||
| nodes.add(allNodesSecondary); edges.add(allEdgesSecondary); | ||||||
| addWithCurrentFontSize(allNodesSecondary, allEdgesSecondary); | ||||||
| currentView = 'secondary'; | ||||||
| viewToggleButton.textContent = `Switch to ${primaryViewName}`; | ||||||
| viewToggleButton.setAttribute('aria-pressed', 'true'); | ||||||
| applyPositions(secondaryViewPositions); | ||||||
| } else { | ||||||
| nodes.add(allNodesPrimary); edges.add(allEdgesPrimary); | ||||||
| addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary); | ||||||
| currentView = 'primary'; | ||||||
| viewToggleButton.textContent = `Switch to ${secondaryViewName}`; | ||||||
| viewToggleButton.setAttribute('aria-pressed', 'false'); | ||||||
|
|
@@ -134,8 +199,8 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| const visibleNodeIds = new Set(visibleNodes.map(n => n.id)); | ||||||
| visibleEdges = activeEdgesData.filter(edge => visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to)); | ||||||
| } | ||||||
| nodes.clear(); edges.clear(); | ||||||
| nodes.add(visibleNodes); edges.add(visibleEdges); | ||||||
|
|
||||||
| addWithCurrentFontSize(visibleNodes, visibleEdges); | ||||||
| applyPositions(currentPositions); | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -205,6 +270,20 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| }); | ||||||
| } | ||||||
|
|
||||||
| function addWithCurrentFontSize(newNodes, newEdges) { | ||||||
| nodes.clear(); | ||||||
| edges.clear(); | ||||||
| nodes.add( | ||||||
| newNodes.map(n => ({ | ||||||
| ...n, | ||||||
| font: { ...(n.font ?? {}), size: visualState.fontSize }, | ||||||
| })), | ||||||
| ); | ||||||
| // Don't set per-edge font - let network.setOptions() handle it | ||||||
| // vis.js ignores global font options when edges have per-item font set | ||||||
| edges.add(newEdges); | ||||||
| } | ||||||
|
|
||||||
| function resetView() { | ||||||
| searchInput.value = ""; | ||||||
| primaryViewPositions = null; | ||||||
|
|
@@ -213,8 +292,7 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| switchView(); // This will switch back to primary and apply null positions | ||||||
| } else { | ||||||
| // If already on primary, just reload the original data | ||||||
| nodes.clear(); edges.clear(); | ||||||
| nodes.add(allNodesPrimary); edges.add(allEdgesPrimary); | ||||||
| addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary); | ||||||
| applyPositions(primaryViewPositions); // Apply null to reset | ||||||
| network.fit(); | ||||||
| } | ||||||
|
|
@@ -233,9 +311,7 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| }); | ||||||
| const filteredNodes = activeNodes.filter(node => nodesToShow.has(node.id)); | ||||||
| const filteredEdges = activeEdges.filter(edge => nodesToShow.has(edge.from) && nodesToShow.has(edge.to)); | ||||||
| nodes.clear(); edges.clear(); | ||||||
| nodes.add(filteredNodes); | ||||||
| edges.add(filteredEdges); | ||||||
| addWithCurrentFontSize(filteredNodes, filteredEdges); | ||||||
| network.fit(); | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -257,4 +333,6 @@ document.addEventListener('DOMContentLoaded', function () { | |||||
| createStyleLegend(); | ||||||
| createSectorLegend(); | ||||||
| createSectorToggles(); | ||||||
| // Initial data load with consistent font handling | ||||||
| addWithCurrentFontSize(allNodesPrimary, allEdgesPrimary); | ||||||
| }); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Simplify the lambda to
key=str.The lambda
lambda t: str(t)is equivalent to juststr.♻️ Proposed simplification
🧰 Tools
🪛 Ruff (0.15.7)
[warning] 239-239: Lambda may be unnecessary; consider inlining inner function
Inline function call
(PLW0108)
🤖 Prompt for AI Agents