add graph
This commit is contained in:
parent
89dd7c1f28
commit
7ae10745ef
@ -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)
|
||||||
|
@ -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,20 +12,17 @@ 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()
|
||||||
|
|
||||||
|
try:
|
||||||
with driver.session() as session:
|
with driver.session() as session:
|
||||||
query = f"MATCH (n:{node_type}) RETURN n"
|
query = f"MATCH (n:{node_type}) RETURN n"
|
||||||
neo4j_logger.info(f"Executing query: {query}")
|
neo4j_logger.info(f"Executing query: {query}")
|
||||||
nodes = session.run(query)
|
nodes = session.run(query)
|
||||||
|
|
||||||
# Convert the nodes to a list of dictionaries
|
|
||||||
nodes_list = [
|
nodes_list = [
|
||||||
{
|
{
|
||||||
'id': record['n'].id,
|
'id': record['n'].id,
|
||||||
@ -37,12 +33,18 @@ def get_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:
|
||||||
|
if filter_property in node:
|
||||||
|
filtered_nodes_list.append({filter_property: node[filter_property]})
|
||||||
return jsonify({"nodes": filtered_nodes_list})
|
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()
|
||||||
if not data:
|
if not data:
|
||||||
@ -56,11 +58,11 @@ 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()
|
||||||
|
try:
|
||||||
with driver.session() as session:
|
with driver.session() as session:
|
||||||
neo4j_logger.info(f"Executing query: {query} with data: {data}")
|
neo4j_logger.info(f"Executing query: {query} with data: {data}")
|
||||||
result = session.run(query, **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),
|
||||||
@ -68,3 +70,7 @@ def create_node():
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
60
api/endpoints/person_network.py
Normal file
60
api/endpoints/person_network.py
Normal 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
|
@ -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>
|
||||||
|
189
api/templates/render_network.html
Normal file
189
api/templates/render_network.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user