add graph

This commit is contained in:
Moses Rolston 2025-03-12 20:47:11 -07:00
parent 89dd7c1f28
commit 7ae10745ef
6 changed files with 339 additions and 140 deletions

View File

@ -9,7 +9,7 @@ import os
import importlib.util import importlib.util
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__, static_folder='static', template_folder='templates')
app.wsgi_app = ProxyFix(app.wsgi_app) app.wsgi_app = ProxyFix(app.wsgi_app)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@ -58,5 +58,9 @@ def sitemap():
def render_member(): def render_member():
return render_template('render_member.html') return render_template('render_member.html')
@app.route('/render_network')
def render_network():
return render_template('render_network.html')
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@ -1,4 +1,3 @@
# endpoints/nodes.py
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from app import get_driver, neo4j_logger from app import get_driver, neo4j_logger
@ -13,35 +12,38 @@ def nodes():
def get_nodes(): def get_nodes():
node_type = request.args.get('type') node_type = request.args.get('type')
if not node_type: if not node_type:
return jsonify({"error": "Node type is required"}), 400 return jsonify({"error": "Node type is required"}), 400
# Get the filter parameter
filter_property = request.args.get('filter') filter_property = request.args.get('filter')
driver = get_driver() driver = get_driver()
with driver.session() as session:
query = f"MATCH (n:{node_type}) RETURN n"
neo4j_logger.info(f"Executing query: {query}")
nodes = session.run(query)
# Convert the nodes to a list of dictionaries try:
nodes_list = [ with driver.session() as session:
{ query = f"MATCH (n:{node_type}) RETURN n"
'id': record['n'].id, neo4j_logger.info(f"Executing query: {query}")
'labels': list(record['n'].labels), nodes = session.run(query)
**{key: value for key, value in record['n'].items()} nodes_list = [
} {
for record in nodes 'id': record['n'].id,
] 'labels': list(record['n'].labels),
**{key: value for key, value in record['n'].items()}
}
for record in nodes
]
if filter_property: if filter_property:
# Filter the results to only include the specified property filtered_nodes_list = []
filtered_nodes_list = [{filter_property: node.get(filter_property)} for node in nodes_list] for node in nodes_list:
return jsonify({"nodes": filtered_nodes_list}) if filter_property in node:
filtered_nodes_list.append({filter_property: node[filter_property]})
return jsonify({"nodes": filtered_nodes_list})
return jsonify({"nodes": nodes_list}) return jsonify({"nodes": nodes_list})
except Exception as e:
neo4j_logger.error(f"Error interacting with Neo4j: {e}")
return jsonify({"error": "An error occurred while interacting with the database"}), 500
def create_node(): def create_node():
data = request.get_json() data = request.get_json()
@ -56,15 +58,19 @@ def create_node():
query = f"CREATE (n:{node_type} {{{properties}}}) RETURN n" query = f"CREATE (n:{node_type} {{{properties}}}) RETURN n"
driver = get_driver() driver = get_driver()
with driver.session() as session: try:
neo4j_logger.info(f"Executing query: {query} with data: {data}") with driver.session() as session:
result = session.run(query, **data) neo4j_logger.info(f"Executing query: {query} with data: {data}")
result = session.run(query, **data)
# Convert the created node to a dictionary new_node = {
new_node = { 'id': result.single()['n'].id,
'id': result.single()['n'].id, 'labels': list(result.single()['n'].labels),
'labels': list(result.single()['n'].labels), **{key: value for key, value in result.single()['n'].items()}
**{key: value for key, value in result.single()['n'].items()} }
}
return jsonify(new_node), 201 return jsonify(new_node), 201
except Exception as e:
neo4j_logger.error(f"Error interacting with Neo4j: {e}")
return jsonify({"error": "An error occurred while interacting with the database"}), 500

View File

@ -0,0 +1,60 @@
# endpoints/person_network.py
from flask import Blueprint, jsonify
from app import get_driver, neo4j_logger
bp = Blueprint('network', __name__)
@bp.route('/person_network', methods=['GET'])
def person_network():
driver = get_driver()
try:
with driver.session() as session:
query = """
MATCH (p:Person)-[r]->(n)
RETURN p, r, n
"""
result = session.run(query)
nodes = {}
links = []
for record in result:
person_node = record['p']
relationship = record['r']
connected_node = record['n']
# Add Person node if not already added
person_id = person_node.id
if person_id not in nodes:
properties = {key: value for key, value in person_node.items()}
properties['id'] = person_id
nodes[person_id] = {
**properties,
"group": 1 # Group for Person
}
# Add connected node if not already added
connected_id = connected_node.id
if connected_id not in nodes:
properties = {key: value for key, value in connected_node.items()}
properties['id'] = connected_id
nodes[connected_id] = {
**properties,
"group": 2 # Group for other nodes (e.g., Organization, Position)
}
# Add link
links.append({
"source": person_id,
"target": connected_id,
"type": relationship.type,
"rel_properties": {key: value for key, value in relationship.items()}
})
return jsonify({"nodes": list(nodes.values()), "links": links})
except Exception as e:
neo4j_logger.error(f"Error interacting with Neo4j: {e}")
return jsonify({"error": "An error occurred while interacting with the database"}), 500

View File

@ -1,128 +1,68 @@
<!-- templates/render_member.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Member Network</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Render Members</title>
<!-- Include D3.js -->
<script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<style> <style>
.node { /* Optional: Add some basic styling */
stroke: #fff; #chart {
stroke-width: 1.5px; border: 1px solid black;
margin: 20px auto;
display: block;
} }
.link { text {
stroke: #999; dominant-baseline: middle;
stroke-opacity: 0.6; text-anchor: middle;
} }
</style> </style>
</head> </head>
<body> <body>
<h1>Member Network</h1> <div id="chart"></div>
<select id="memberSelect">
<!-- Options will be populated dynamically -->
</select>
<div id="graph"></div>
<script> <script type="text/javascript">
// Fetch member names from Neo4j // Fetch data from the Flask endpoint for Person nodes
fetch('/get_member_names') d3.json('/nodes?type=Person').then(function(data) {
.then(response => response.json()) console.log("Fetched Data:", data);
.then(members => {
const select = document.getElementById('memberSelect');
members.forEach(member => {
const option = document.createElement('option');
option.value = member;
option.text = member;
select.appendChild(option);
});
});
// Handle member selection var svg = d3.select("#chart")
document.getElementById('memberSelect').addEventListener('change', function() {
console.log("Member selected:", this.value); // Log the selected member name
const selectedMember = this.value;
fetch(`/list_members?member_name=${encodeURIComponent(selectedMember)}`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
console.log("Graph data received:", data); // Log the graph data
renderGraph(data);
})
.catch(error => {
console.error("Error fetching graph data:", error); // Log any errors
});
});
// Render D3 network graph
function renderGraph(data) {
const width = 800;
const height = 600;
const svg = d3.select("#graph")
.selectAll("svg") // Remove existing SVG elements first
.remove()
.append("svg") .append("svg")
.attr("width", width) .attr("width", 1000)
.attr("height", height); .attr("height", 1000);
const simulation = d3.forceSimulation(data.nodes) // Function to generate random position within the SVG canvas
.force("link", d3.forceLink(data.links).id(d => d.id)) function getRandomPosition() {
.force("charge", d3.forceManyBody().strength(-500)) return Math.random() * 950 + 25; // Random x or y between 25 and 975 to keep nodes inside the border
.force("center", d3.forceCenter(width / 2, height / 2)); }
const link = svg.append("g") // Render circles for each node with green color and random positions
.attr("class", "links") svg.selectAll("circle")
.selectAll("line")
.data(data.links)
.enter().append("line")
.attr("stroke-width", 2);
const node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data.nodes) .data(data.nodes)
.enter().append("circle") .enter()
.attr("r", 5) .append("circle")
.attr("fill", d => d.label === 'Person' ? 'steelblue' : 'orange') .attr("cx", getRandomPosition) // Random x position
.call(d3.drag() .attr("cy", getRandomPosition) // Random y position
.on("start", dragstarted) .attr("r", 10)
.on("drag", dragged) .style("fill", "green"); // Set node color to green
.on("end", dragended));
node.append("title") // Add labels for each node using the 'name' and 'bioguideId' properties
.text(d => `${d.name} (${d.label})`); svg.selectAll("text.label")
.data(data.nodes)
simulation.on("tick", () => { .enter()
link .append("text")
.attr("x1", d => d.source.x) .attr("class", "label")
.attr("y1", d => d.source.y) .attr("x", function(d) { return d3.select(this.previousSibling).attr("cx"); }) // Position slightly to the right of the circle
.attr("x2", d => d.target.x) .attr("y", function(d) { return d3.select(this.previousSibling).attr("cy") + 15; }) // Position below the circle
.attr("y2", d => d.target.y); .text(function(d) {
return `${d.name || 'Person'}\nID: ${d.bioguideId || 'N/A'}`; // Single line with both name and ID
node })
.attr("cx", d => d.x) .style("fill", "black")
.attr("cy", d => d.y); .style("font-size", "12px");
}); }).catch(function(error) {
console.error("Error fetching data:", error);
function dragstarted(event, d) { });
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Render Network</title>
<!-- Include D3.js -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
#chart {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.node {
stroke: #fff;
stroke-width: 1.5px;
cursor: pointer; /* Change cursor to indicate interactivity */
}
.link {
fill: none;
stroke: #999;
stroke-opacity: 0.6;
}
text {
font-size: 12px;
}
/* Tooltip styles */
.tooltip {
position: absolute;
padding: 10px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #ccc;
pointer-events: none; /* Make sure the tooltip doesn't interfere with mouse events */
}
</style>
</head>
<body>
<div id="chart"></div>
<!-- Tooltip container -->
<div class="tooltip" style="display: none;"></div>
<script type="text/javascript">
// Get container dimensions
const chartContainer = d3.select("#chart");
let width = parseInt(chartContainer.style("width"));
let height = parseInt(chartContainer.style("height"));
// Create SVG container
const svg = chartContainer.append("svg")
.attr("width", width)
.attr("height", height);
// Define simulation
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-500)) // Adjust charge strength for better distribution
.force("center", d3.forceCenter(width / 2, height / 2));
// Fetch network data from Flask endpoint
d3.json('/person_network').then(function(data) {
const nodesData = data.nodes;
const linksData = data.links;
console.log("Nodes:", nodesData);
console.log("Links:", linksData);
// Append links to the SVG container
const link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(linksData)
.enter()
.append("line")
.attr("class", "link");
// Append nodes to the SVG container
const node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("class", "node")
.attr("r", d => d.group === 1 ? 8 : 6) // Larger circles for Person nodes
.style("fill", d => d.group === 1 ? "green" : "lightblue") // Green for Person, light blue for others
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", showTooltip);
// Append labels to the SVG container
const label = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodesData)
.enter()
.append("text")
.text(d => {
if (d.labels.includes('Person')) {
return d.name || 'Person';
} else if (['Legislation', 'Bill', 'Law'].some(label => d.labels.includes(label))) {
return d.title || 'Title';
}
return 'Node'; // Fallback for other types
})
.attr("dx", 10) // Offset from node
.attr("dy", 4); // Offset from node
// Initialize simulation with nodes and links
simulation.nodes(nodesData)
.on("tick", ticked);
simulation.force("link")
.links(linksData);
function ticked() {
link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("cx", d => Math.max(10, Math.min(width - 10, d.x)))
.attr("cy", d => Math.max(10, Math.min(height - 10, d.y)));
label.attr("x", d => d.x)
.attr("y", d => d.y);
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// Show tooltip on node click
function showTooltip(event, d) {
const tooltip = d3.select(".tooltip");
tooltip.style("display", "block")
.html(Object.entries(d).map(([key, value]) => `<strong>${key}:</strong> ${value}`).join("<br />"))
.style("left", `${event.pageX}px`)
.style("top", `${event.pageY}px`);
}
// Hide tooltip on document click
d3.select(document).on("click.tooltip", function() {
const tooltip = d3.select(".tooltip");
if (!d3.event.target.classList.contains("node")) {
tooltip.style("display", "none");
}
});
}).catch(function(error) {
console.error("Error fetching data:", error);
});
// Handle window resize
window.addEventListener('resize', () => {
width = parseInt(chartContainer.style("width"));
height = parseInt(chartContainer.style("height"));
svg.attr("width", width)
.attr("height", height);
simulation.force("center")
.x(width / 2)
.y(height / 2);
});
</script>
</body>
</html>