add a bunch of stuff
rendering the graph via exported json works now
This commit is contained in:
parent
b012392fc9
commit
276b57fcdf
@ -18,7 +18,7 @@ CONGRESS_API_KEY = os.getenv('CONGRESS_API_KEY')
|
||||
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
|
||||
|
||||
def fetch_bills(offset):
|
||||
url = f"https://api.congress.gov/v3/bill/117/sres?offset={offset}&api_key={CONGRESS_API_KEY}"
|
||||
url = f"https://api.congress.gov/v3/bill/119/sres?offset={offset}&api_key={CONGRESS_API_KEY}"
|
||||
print(f"Fetching data from {url}")
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
|
@ -30,7 +30,6 @@ def main(csv_file_path):
|
||||
|
||||
# Connect to Neo4j
|
||||
driver = create_neo4j_session(NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD)
|
||||
|
||||
with driver.session() as session:
|
||||
for _, row in df.iterrows():
|
||||
node_type = row['type']
|
||||
@ -43,7 +42,7 @@ def main(csv_file_path):
|
||||
'date_repealed': pd.to_datetime(row['date_repealed']).strftime('%Y-%m-%d') if pd.notnull(row['date_repealed']) else None
|
||||
}
|
||||
# Create or merge the node in Neo4j
|
||||
session.write_transaction(create_or_merge_node, node_type, properties)
|
||||
session.execute_write(create_or_merge_node, node_type, properties)
|
||||
|
||||
driver.close()
|
||||
|
||||
|
55
add_law_sponsors.py
Normal file
55
add_law_sponsors.py
Normal file
@ -0,0 +1,55 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from neo4j import GraphDatabase
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Get Neo4j connection info from environment variables
|
||||
NEO4J_URI = os.getenv('NEO4J_URI')
|
||||
NEO4J_USER = os.getenv('NEO4J_USER')
|
||||
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
|
||||
|
||||
# Function to connect to the Neo4j database and fetch the "sponsors.0.bioguideId" property of all Bill nodes
|
||||
def get_sponsors_bioguide_id(uri, user, password):
|
||||
driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
|
||||
with driver.session() as session:
|
||||
# Cypher query to get all Bill nodes and match Person node with the same bioguideId
|
||||
query = """
|
||||
MATCH (b:Bill)
|
||||
WITH b, b.`sponsors.0.bioguideId` AS bioguideId
|
||||
OPTIONAL MATCH (p:Person {bioguideId: bioguideId})
|
||||
WHERE NOT EXISTS((b)-[:SPONSORED]->(p))
|
||||
RETURN b.`sponsors.0.bioguideId` AS sponsorBioguideId, p
|
||||
"""
|
||||
|
||||
result = session.run(query)
|
||||
|
||||
for record in result:
|
||||
sponsor_bioguide_id = record['sponsorBioguideId']
|
||||
matched_person = record['p']
|
||||
|
||||
# Print the value of sponsors.0.bioguideId and the matched Person node
|
||||
# print(f"Value of sponsors.0.bioguideId: {sponsor_bioguide_id}")
|
||||
if matched_person:
|
||||
person_properties = matched_person.items()
|
||||
print("Matched Person Node:")
|
||||
for key, value in person_properties:
|
||||
print(f"{key}: {value}")
|
||||
|
||||
# Create the SPONSORED relationship
|
||||
create_relationship_query = """
|
||||
MATCH (b:Bill), (p:Person {bioguideId: $bioguideId})
|
||||
WHERE b.`sponsors.0.bioguideId` = $sponsorBioguideId
|
||||
CREATE (p)-[:SPONSORED]->(b)
|
||||
"""
|
||||
session.run(create_relationship_query, bioguideId=sponsor_bioguide_id, sponsorBioguideId=sponsor_bioguide_id)
|
||||
print("Created SPONSORED relationship.")
|
||||
else:
|
||||
continue # print("No matching Person node found.")
|
||||
|
||||
driver.close()
|
||||
|
||||
# Call the function with your connection info
|
||||
get_sponsors_bioguide_id(NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD)
|
68
add_law_type.py
Normal file
68
add_law_type.py
Normal file
@ -0,0 +1,68 @@
|
||||
import os
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from neo4j import GraphDatabase
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Neo4j connection details
|
||||
NEO4J_URI = os.getenv('NEO4J_URI')
|
||||
NEO4J_USER = os.getenv('NEO4J_USER')
|
||||
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
|
||||
|
||||
# Congress API key
|
||||
CONGRESS_API_KEY = os.getenv('CONGRESS_API_KEY')
|
||||
|
||||
# Initialize Neo4j driver
|
||||
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
|
||||
|
||||
def fetch_bills(offset):
|
||||
url = f"https://api.congress.gov/v3/law/119/priv?offset={offset}&api_key={CONGRESS_API_KEY}"
|
||||
print(f"Fetching data from {url}")
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"Failed to fetch data: {response.status_code}")
|
||||
|
||||
def flatten_json(y):
|
||||
out = {}
|
||||
def flatten(x, name=''):
|
||||
if type(x) is dict:
|
||||
for a in x:
|
||||
flatten(x[a], name + a + '_')
|
||||
elif type(x) is list:
|
||||
i = 0
|
||||
for a in x:
|
||||
flatten(a, name + str(i) + '_')
|
||||
i += 1
|
||||
else:
|
||||
out[name[:-1]] = x
|
||||
flatten(y)
|
||||
return out
|
||||
|
||||
def create_bill_node(tx, bill_data):
|
||||
flat_bill_data = flatten_json(bill_data)
|
||||
print(f"Creating Bill node with properties: {flat_bill_data}")
|
||||
query = "CREATE (b:Law $properties)"
|
||||
tx.run(query, properties=flat_bill_data)
|
||||
|
||||
def main():
|
||||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
bills_data = fetch_bills(offset)
|
||||
if not bills_data or len(bills_data['bills']) == 0:
|
||||
break
|
||||
with driver.session() as session:
|
||||
for bill in bills_data['bills']:
|
||||
session.write_transaction(create_bill_node, bill)
|
||||
offset += 250
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
break
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
driver.close()
|
77
api/endpoints/ingest_bills.py
Normal file
77
api/endpoints/ingest_bills.py
Normal file
@ -0,0 +1,77 @@
|
||||
from flask import Blueprint, jsonify
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from app import get_driver, neo4j_logger
|
||||
|
||||
load_dotenv()
|
||||
bp = Blueprint('ingest_bills', __name__)
|
||||
|
||||
@bp.route('/ingest_bills', methods=['POST'])
|
||||
def ingest_bills():
|
||||
api_key = os.getenv('CONGRESS_API_KEY')
|
||||
if not api_key:
|
||||
neo4j_logger.error("Congress API key is missing in environment variables")
|
||||
return jsonify({"error": "Congress API key is missing"}), 500
|
||||
|
||||
congress_number = 117
|
||||
start_offset = 0
|
||||
page_size = 250
|
||||
bills_url_template = f"https://api.congress.gov/v3/bill/{congress_number}?offset={{start_offset}}&limit={page_size}&api_key={api_key}"
|
||||
|
||||
while True:
|
||||
url = bills_url_template.format(start_offset=start_offset)
|
||||
response = requests.get(url, timeout=10) # Add a timeout to prevent hanging requests
|
||||
if response.status_code != 200:
|
||||
neo4j_logger.error(f"Failed to fetch bills data: {response.status_code}, {response.text}")
|
||||
return jsonify({"error": "Failed to fetch bills data"}), 500
|
||||
|
||||
bills_data = response.json()
|
||||
|
||||
# Check for pagination details
|
||||
total_results = int(bills_data.get('totalResults', 0))
|
||||
if not bills_data.get('bills'):
|
||||
break
|
||||
|
||||
# Process and create Bill nodes in Neo4j
|
||||
driver = get_driver()
|
||||
with driver.session() as session:
|
||||
for bill in bills_data['bills']:
|
||||
try:
|
||||
bill_properties = {}
|
||||
for key, value in bill.items():
|
||||
if isinstance(value, dict):
|
||||
# Flatten nested dictionaries
|
||||
for sub_key, sub_value in value.items():
|
||||
bill_properties[f"{key}_{sub_key}"] = sub_value or None
|
||||
else:
|
||||
bill_properties[key] = value or None
|
||||
|
||||
# Ensure there is a unique identifier (bill_id)
|
||||
if not bill_properties.get('bill_id'):
|
||||
# Construct a bill_id based on available fields, e.g., bill_type and number
|
||||
bill_id = f"{bill_properties['billType']}_{bill_properties['number']}"
|
||||
bill_properties['bill_id'] = bill_id
|
||||
|
||||
query_to_execute = session.write_transaction(create_bill_node, bill_properties)
|
||||
print(f"Executing query: {query_to_execute}")
|
||||
except Exception as e:
|
||||
neo4j_logger.error(f"Error creating Bill node: {e}")
|
||||
|
||||
# Update offset for pagination
|
||||
start_offset += page_size
|
||||
if start_offset >= total_results:
|
||||
break
|
||||
|
||||
return jsonify({"message": "Bill ingestion completed successfully"}), 200
|
||||
|
||||
|
||||
def create_bill_node(tx, properties):
|
||||
query = """
|
||||
MERGE (b:Bill {bill_id: $bill_id})
|
||||
SET b += $properties
|
||||
RETURN b
|
||||
"""
|
||||
result = tx.run(query, bill_id=properties.get('bill_id'), properties=properties)
|
||||
return result.summary().query
|
10
api/static/css/styles.css
Normal file
10
api/static/css/styles.css
Normal file
@ -0,0 +1,10 @@
|
||||
#chart {
|
||||
border: 1px solid black;
|
||||
margin: 20px auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
text {
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
}
|
20
render/app.py
Normal file
20
render/app.py
Normal file
@ -0,0 +1,20 @@
|
||||
from flask import Flask, render_template
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from neo4j import GraphDatabase
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__,
|
||||
static_url_path='',
|
||||
static_folder='static',
|
||||
template_folder='templates')
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
27
render/static/css/3d-force-graph.css
Normal file
27
render/static/css/3d-force-graph.css
Normal file
@ -0,0 +1,27 @@
|
||||
.graph-info-msg {
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: lavender;
|
||||
opacity: 0.7;
|
||||
font-size: 22px;
|
||||
position: absolute;
|
||||
font-family: Sans-serif;
|
||||
}
|
||||
|
||||
.scene-container .clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scene-container .grabbable {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.scene-container .grabbable:active {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
17
render/static/css/styles.css
Normal file
17
render/static/css/styles.css
Normal file
@ -0,0 +1,17 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#chart {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.node {
|
||||
fill: steelblue;
|
||||
stroke: white;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
pointer-events: none;
|
||||
}
|
17183
render/static/data/policymap.json
Normal file
17183
render/static/data/policymap.json
Normal file
File diff suppressed because one or more lines are too long
444
render/static/js/3d-force-graph.js
Normal file
444
render/static/js/3d-force-graph.js
Normal file
@ -0,0 +1,444 @@
|
||||
import { AmbientLight, DirectionalLight, Vector3, REVISION } from 'three';
|
||||
|
||||
const three = window.THREE
|
||||
? window.THREE // Prefer consumption from global THREE, if exists
|
||||
: { AmbientLight, DirectionalLight, Vector3, REVISION };
|
||||
|
||||
import { DragControls as ThreeDragControls } from 'three/examples/jsm/controls/DragControls.js';
|
||||
|
||||
import ThreeForceGraph from 'three-forcegraph';
|
||||
import ThreeRenderObjects from 'three-render-objects';
|
||||
|
||||
import accessorFn from 'accessor-fn';
|
||||
import Kapsule from 'kapsule';
|
||||
|
||||
import linkKapsule from './kapsule-link.js';
|
||||
|
||||
//
|
||||
|
||||
const CAMERA_DISTANCE2NODES_FACTOR = 170;
|
||||
|
||||
//
|
||||
|
||||
// Expose config from forceGraph
|
||||
const bindFG = linkKapsule('forceGraph', ThreeForceGraph);
|
||||
const linkedFGProps = Object.assign(...[
|
||||
'jsonUrl',
|
||||
'graphData',
|
||||
'numDimensions',
|
||||
'dagMode',
|
||||
'dagLevelDistance',
|
||||
'dagNodeFilter',
|
||||
'onDagError',
|
||||
'nodeRelSize',
|
||||
'nodeId',
|
||||
'nodeVal',
|
||||
'nodeResolution',
|
||||
'nodeColor',
|
||||
'nodeAutoColorBy',
|
||||
'nodeOpacity',
|
||||
'nodeVisibility',
|
||||
'nodeThreeObject',
|
||||
'nodeThreeObjectExtend',
|
||||
'linkSource',
|
||||
'linkTarget',
|
||||
'linkVisibility',
|
||||
'linkColor',
|
||||
'linkAutoColorBy',
|
||||
'linkOpacity',
|
||||
'linkWidth',
|
||||
'linkResolution',
|
||||
'linkCurvature',
|
||||
'linkCurveRotation',
|
||||
'linkMaterial',
|
||||
'linkThreeObject',
|
||||
'linkThreeObjectExtend',
|
||||
'linkPositionUpdate',
|
||||
'linkDirectionalArrowLength',
|
||||
'linkDirectionalArrowColor',
|
||||
'linkDirectionalArrowRelPos',
|
||||
'linkDirectionalArrowResolution',
|
||||
'linkDirectionalParticles',
|
||||
'linkDirectionalParticleSpeed',
|
||||
'linkDirectionalParticleWidth',
|
||||
'linkDirectionalParticleColor',
|
||||
'linkDirectionalParticleResolution',
|
||||
'forceEngine',
|
||||
'd3AlphaDecay',
|
||||
'd3VelocityDecay',
|
||||
'd3AlphaMin',
|
||||
'ngraphPhysics',
|
||||
'warmupTicks',
|
||||
'cooldownTicks',
|
||||
'cooldownTime',
|
||||
'onEngineTick',
|
||||
'onEngineStop'
|
||||
].map(p => ({ [p]: bindFG.linkProp(p)})));
|
||||
const linkedFGMethods = Object.assign(...[
|
||||
'refresh',
|
||||
'getGraphBbox',
|
||||
'd3Force',
|
||||
'd3ReheatSimulation',
|
||||
'emitParticle'
|
||||
].map(p => ({ [p]: bindFG.linkMethod(p)})));
|
||||
|
||||
// Expose config from renderObjs
|
||||
const bindRenderObjs = linkKapsule('renderObjs', ThreeRenderObjects);
|
||||
const linkedRenderObjsProps = Object.assign(...[
|
||||
'width',
|
||||
'height',
|
||||
'backgroundColor',
|
||||
'showNavInfo',
|
||||
'enablePointerInteraction'
|
||||
].map(p => ({ [p]: bindRenderObjs.linkProp(p)})));
|
||||
const linkedRenderObjsMethods = Object.assign(
|
||||
...[
|
||||
'lights',
|
||||
'cameraPosition',
|
||||
'postProcessingComposer'
|
||||
].map(p => ({ [p]: bindRenderObjs.linkMethod(p)})),
|
||||
{
|
||||
graph2ScreenCoords: bindRenderObjs.linkMethod('getScreenCoords'),
|
||||
screen2GraphCoords: bindRenderObjs.linkMethod('getSceneCoords')
|
||||
}
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
export default Kapsule({
|
||||
|
||||
props: {
|
||||
nodeLabel: { default: 'name', triggerUpdate: false },
|
||||
linkLabel: { default: 'name', triggerUpdate: false },
|
||||
linkHoverPrecision: { default: 1, onChange: (p, state) => state.renderObjs.lineHoverPrecision(p), triggerUpdate: false },
|
||||
enableNavigationControls: {
|
||||
default: true,
|
||||
onChange(enable, state) {
|
||||
const controls = state.renderObjs.controls();
|
||||
if (controls) {
|
||||
controls.enabled = enable;
|
||||
// trigger mouseup on re-enable to prevent sticky controls
|
||||
enable && controls.domElement && controls.domElement.dispatchEvent(new PointerEvent('pointerup'));
|
||||
}
|
||||
},
|
||||
triggerUpdate: false
|
||||
},
|
||||
enableNodeDrag: { default: true, triggerUpdate: false },
|
||||
onNodeDrag: { default: () => {}, triggerUpdate: false },
|
||||
onNodeDragEnd: { default: () => {}, triggerUpdate: false },
|
||||
onNodeClick: { triggerUpdate: false },
|
||||
onNodeRightClick: { triggerUpdate: false },
|
||||
onNodeHover: { triggerUpdate: false },
|
||||
onLinkClick: { triggerUpdate: false },
|
||||
onLinkRightClick: { triggerUpdate: false },
|
||||
onLinkHover: { triggerUpdate: false },
|
||||
onBackgroundClick: { triggerUpdate: false },
|
||||
onBackgroundRightClick: { triggerUpdate: false },
|
||||
...linkedFGProps,
|
||||
...linkedRenderObjsProps
|
||||
},
|
||||
|
||||
methods: {
|
||||
zoomToFit: function(state, transitionDuration, padding, ...bboxArgs) {
|
||||
state.renderObjs.fitToBbox(
|
||||
state.forceGraph.getGraphBbox(...bboxArgs),
|
||||
transitionDuration,
|
||||
padding
|
||||
);
|
||||
return this;
|
||||
},
|
||||
pauseAnimation: function(state) {
|
||||
if (state.animationFrameRequestId !== null) {
|
||||
cancelAnimationFrame(state.animationFrameRequestId);
|
||||
state.animationFrameRequestId = null;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
resumeAnimation: function(state) {
|
||||
if (state.animationFrameRequestId === null) {
|
||||
this._animationCycle();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
_animationCycle(state) {
|
||||
if (state.enablePointerInteraction) {
|
||||
// reset canvas cursor (override dragControls cursor)
|
||||
this.renderer().domElement.style.cursor = null;
|
||||
}
|
||||
|
||||
// Frame cycle
|
||||
state.forceGraph.tickFrame();
|
||||
state.renderObjs.tick();
|
||||
state.animationFrameRequestId = requestAnimationFrame(this._animationCycle);
|
||||
},
|
||||
scene: state => state.renderObjs.scene(), // Expose scene
|
||||
camera: state => state.renderObjs.camera(), // Expose camera
|
||||
renderer: state => state.renderObjs.renderer(), // Expose renderer
|
||||
controls: state => state.renderObjs.controls(), // Expose controls
|
||||
tbControls: state => state.renderObjs.tbControls(), // To be deprecated
|
||||
_destructor: function() {
|
||||
this.pauseAnimation();
|
||||
this.graphData({ nodes: [], links: []});
|
||||
},
|
||||
...linkedFGMethods,
|
||||
...linkedRenderObjsMethods
|
||||
},
|
||||
|
||||
stateInit: ({ controlType, rendererConfig, extraRenderers }) => {
|
||||
const forceGraph = new ThreeForceGraph();
|
||||
return {
|
||||
forceGraph,
|
||||
renderObjs: ThreeRenderObjects({ controlType, rendererConfig, extraRenderers })
|
||||
.objects([forceGraph]) // Populate scene
|
||||
.lights([
|
||||
new three.AmbientLight(0xcccccc, Math.PI),
|
||||
new three.DirectionalLight(0xffffff, 0.6 * Math.PI)
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
init: function(domNode, state) {
|
||||
// Wipe DOM
|
||||
domNode.innerHTML = '';
|
||||
|
||||
// Add relative container
|
||||
domNode.appendChild(state.container = document.createElement('div'));
|
||||
state.container.style.position = 'relative';
|
||||
|
||||
// Add renderObjs
|
||||
const roDomNode = document.createElement('div');
|
||||
state.container.appendChild(roDomNode);
|
||||
state.renderObjs(roDomNode);
|
||||
const camera = state.renderObjs.camera();
|
||||
const renderer = state.renderObjs.renderer();
|
||||
const controls = state.renderObjs.controls();
|
||||
controls.enabled = !!state.enableNavigationControls;
|
||||
state.lastSetCameraZ = camera.position.z;
|
||||
|
||||
// Add info space
|
||||
let infoElem;
|
||||
state.container.appendChild(infoElem = document.createElement('div'));
|
||||
infoElem.className = 'graph-info-msg';
|
||||
infoElem.textContent = '';
|
||||
|
||||
// config forcegraph
|
||||
state.forceGraph
|
||||
.onLoading(() => { infoElem.textContent = 'Loading...' })
|
||||
.onFinishLoading(() => { infoElem.textContent = '' })
|
||||
.onUpdate(() => {
|
||||
// sync graph data structures
|
||||
state.graphData = state.forceGraph.graphData();
|
||||
|
||||
// re-aim camera, if still in default position (not user modified)
|
||||
if (camera.position.x === 0 && camera.position.y === 0 && camera.position.z === state.lastSetCameraZ && state.graphData.nodes.length) {
|
||||
camera.lookAt(state.forceGraph.position);
|
||||
state.lastSetCameraZ = camera.position.z = Math.cbrt(state.graphData.nodes.length) * CAMERA_DISTANCE2NODES_FACTOR;
|
||||
}
|
||||
})
|
||||
.onFinishUpdate(() => {
|
||||
// Setup node drag interaction
|
||||
if (state._dragControls) {
|
||||
const curNodeDrag = state.graphData.nodes.find(node => node.__initialFixedPos && !node.__disposeControlsAfterDrag); // detect if there's a node being dragged using the existing drag controls
|
||||
if (curNodeDrag) {
|
||||
curNodeDrag.__disposeControlsAfterDrag = true; // postpone previous controls disposal until drag ends
|
||||
} else {
|
||||
state._dragControls.dispose(); // cancel previous drag controls
|
||||
}
|
||||
|
||||
state._dragControls = undefined;
|
||||
}
|
||||
|
||||
if (state.enableNodeDrag && state.enablePointerInteraction && state.forceEngine === 'd3') { // Can't access node positions programmatically in ngraph
|
||||
const dragControls = state._dragControls = new ThreeDragControls(
|
||||
state.graphData.nodes.map(node => node.__threeObj).filter(obj => obj),
|
||||
camera,
|
||||
renderer.domElement
|
||||
);
|
||||
|
||||
dragControls.addEventListener('dragstart', function (event) {
|
||||
const nodeObj = getGraphObj(event.object);
|
||||
if (!nodeObj) return;
|
||||
|
||||
controls.enabled = false; // Disable controls while dragging
|
||||
|
||||
// track drag object movement
|
||||
event.object.__initialPos = event.object.position.clone();
|
||||
event.object.__prevPos = event.object.position.clone();
|
||||
|
||||
const node = nodeObj.__data;
|
||||
!node.__initialFixedPos && (node.__initialFixedPos = {fx: node.fx, fy: node.fy, fz: node.fz});
|
||||
!node.__initialPos && (node.__initialPos = {x: node.x, y: node.y, z: node.z});
|
||||
|
||||
// lock node
|
||||
['x', 'y', 'z'].forEach(c => node[`f${c}`] = node[c]);
|
||||
|
||||
// drag cursor
|
||||
renderer.domElement.classList.add('grabbable');
|
||||
});
|
||||
|
||||
dragControls.addEventListener('drag', function (event) {
|
||||
const nodeObj = getGraphObj(event.object);
|
||||
if (!nodeObj) return;
|
||||
|
||||
if (!event.object.hasOwnProperty('__graphObjType')) {
|
||||
// If dragging a child of the node, update the node object instead
|
||||
const initPos = event.object.__initialPos;
|
||||
const prevPos = event.object.__prevPos;
|
||||
const newPos = event.object.position;
|
||||
|
||||
nodeObj.position.add(newPos.clone().sub(prevPos)); // translate node object by the motion delta
|
||||
prevPos.copy(newPos);
|
||||
newPos.copy(initPos); // reset child back to its initial position
|
||||
}
|
||||
|
||||
const node = nodeObj.__data;
|
||||
const newPos = nodeObj.position;
|
||||
const translate = {x: newPos.x - node.x, y: newPos.y - node.y, z: newPos.z - node.z};
|
||||
// Move fx/fy/fz (and x/y/z) of nodes based on object new position
|
||||
['x', 'y', 'z'].forEach(c => node[`f${c}`] = node[c] = newPos[c]);
|
||||
|
||||
state.forceGraph
|
||||
.d3AlphaTarget(0.3) // keep engine running at low intensity throughout drag
|
||||
.resetCountdown(); // prevent freeze while dragging
|
||||
|
||||
node.__dragged = true;
|
||||
state.onNodeDrag(node, translate);
|
||||
});
|
||||
|
||||
dragControls.addEventListener('dragend', function (event) {
|
||||
const nodeObj = getGraphObj(event.object);
|
||||
if (!nodeObj) return;
|
||||
|
||||
delete(event.object.__initialPos); // remove tracking attributes
|
||||
delete(event.object.__prevPos);
|
||||
|
||||
const node = nodeObj.__data;
|
||||
|
||||
// dispose previous controls if needed
|
||||
if (node.__disposeControlsAfterDrag) {
|
||||
dragControls.dispose();
|
||||
delete(node.__disposeControlsAfterDrag);
|
||||
}
|
||||
|
||||
const initFixedPos = node.__initialFixedPos;
|
||||
const initPos = node.__initialPos;
|
||||
const translate = {x: initPos.x - node.x, y: initPos.y - node.y, z: initPos.z - node.z};
|
||||
if (initFixedPos) {
|
||||
['x', 'y', 'z'].forEach(c => {
|
||||
const fc = `f${c}`;
|
||||
if (initFixedPos[fc] === undefined) {
|
||||
delete(node[fc])
|
||||
}
|
||||
});
|
||||
delete(node.__initialFixedPos);
|
||||
delete(node.__initialPos);
|
||||
if (node.__dragged) {
|
||||
delete(node.__dragged);
|
||||
state.onNodeDragEnd(node, translate);
|
||||
}
|
||||
}
|
||||
|
||||
state.forceGraph
|
||||
.d3AlphaTarget(0) // release engine low intensity
|
||||
.resetCountdown(); // let the engine readjust after releasing fixed nodes
|
||||
|
||||
if (state.enableNavigationControls) {
|
||||
controls.enabled = true; // Re-enable controls
|
||||
controls.domElement && controls.domElement.ownerDocument && controls.domElement.ownerDocument.dispatchEvent(
|
||||
// simulate mouseup to ensure the controls don't take over after dragend
|
||||
new PointerEvent('pointerup', { pointerType: 'touch' })
|
||||
);
|
||||
}
|
||||
|
||||
// clear cursor
|
||||
renderer.domElement.classList.remove('grabbable');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// config renderObjs
|
||||
three.REVISION < 155 && (state.renderObjs.renderer().useLegacyLights = false); // force behavior for three < 155
|
||||
state.renderObjs
|
||||
.hoverOrderComparator((a, b) => {
|
||||
// Prioritize graph objects
|
||||
const aObj = getGraphObj(a);
|
||||
if (!aObj) return 1;
|
||||
const bObj = getGraphObj(b);
|
||||
if (!bObj) return -1;
|
||||
|
||||
// Prioritize nodes over links
|
||||
const isNode = o => o.__graphObjType === 'node';
|
||||
return isNode(bObj) - isNode(aObj);
|
||||
})
|
||||
.tooltipContent(obj => {
|
||||
const graphObj = getGraphObj(obj);
|
||||
return graphObj ? accessorFn(state[`${graphObj.__graphObjType}Label`])(graphObj.__data) || '' : '';
|
||||
})
|
||||
.hoverDuringDrag(false)
|
||||
.onHover(obj => {
|
||||
// Update tooltip and trigger onHover events
|
||||
const hoverObj = getGraphObj(obj);
|
||||
|
||||
if (hoverObj !== state.hoverObj) {
|
||||
const prevObjType = state.hoverObj ? state.hoverObj.__graphObjType : null;
|
||||
const prevObjData = state.hoverObj ? state.hoverObj.__data : null;
|
||||
const objType = hoverObj ? hoverObj.__graphObjType : null;
|
||||
const objData = hoverObj ? hoverObj.__data : null;
|
||||
if (prevObjType && prevObjType !== objType) {
|
||||
// Hover out
|
||||
const fn = state[`on${prevObjType === 'node' ? 'Node' : 'Link'}Hover`];
|
||||
fn && fn(null, prevObjData);
|
||||
}
|
||||
if (objType) {
|
||||
// Hover in
|
||||
const fn = state[`on${objType === 'node' ? 'Node' : 'Link'}Hover`];
|
||||
fn && fn(objData, prevObjType === objType ? prevObjData : null);
|
||||
}
|
||||
|
||||
// set pointer if hovered object is clickable
|
||||
renderer.domElement.classList[
|
||||
((hoverObj && state[`on${objType === 'node' ? 'Node' : 'Link'}Click`]) || (!hoverObj && state.onBackgroundClick)) ? 'add' : 'remove'
|
||||
]('clickable');
|
||||
|
||||
state.hoverObj = hoverObj;
|
||||
}
|
||||
})
|
||||
.clickAfterDrag(false)
|
||||
.onClick((obj, ev) => {
|
||||
const graphObj = getGraphObj(obj);
|
||||
if (graphObj) {
|
||||
const fn = state[`on${graphObj.__graphObjType === 'node' ? 'Node' : 'Link'}Click`];
|
||||
fn && fn(graphObj.__data, ev);
|
||||
} else {
|
||||
state.onBackgroundClick && state.onBackgroundClick(ev);
|
||||
}
|
||||
})
|
||||
.onRightClick((obj, ev) => {
|
||||
// Handle right-click events
|
||||
const graphObj = getGraphObj(obj);
|
||||
if (graphObj) {
|
||||
const fn = state[`on${graphObj.__graphObjType === 'node' ? 'Node' : 'Link'}RightClick`];
|
||||
fn && fn(graphObj.__data, ev);
|
||||
} else {
|
||||
state.onBackgroundRightClick && state.onBackgroundRightClick(ev);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
// Kick-off renderer
|
||||
this._animationCycle();
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
function getGraphObj(object) {
|
||||
let obj = object;
|
||||
// recurse up object chain until finding the graph object
|
||||
while (obj && !obj.hasOwnProperty('__graphObjType')) {
|
||||
obj = obj.parent;
|
||||
}
|
||||
return obj;
|
||||
}
|
90
render/static/js/index.d.ts
vendored
Normal file
90
render/static/js/index.d.ts
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
import { Scene, Light, Camera, WebGLRenderer, WebGLRendererParameters, Renderer } from 'three';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
||||
import { ThreeForceGraphGeneric, NodeObject, LinkObject } from 'three-forcegraph';
|
||||
|
||||
export interface ConfigOptions {
|
||||
controlType?: 'trackball' | 'orbit' | 'fly'
|
||||
rendererConfig?: WebGLRendererParameters,
|
||||
extraRenderers?: Renderer[]
|
||||
}
|
||||
|
||||
type Accessor<In, Out> = Out | string | ((obj: In) => Out);
|
||||
type ObjAccessor<T, InT = object> = Accessor<InT, T>;
|
||||
|
||||
type Label = string | HTMLElement;
|
||||
|
||||
type Coords = { x: number; y: number; z: number; };
|
||||
|
||||
// don't surface these internal props from inner ThreeForceGraph
|
||||
type ExcludedInnerProps = 'onLoading' | 'onFinishLoading' | 'onUpdate' | 'onFinishUpdate' | 'tickFrame' | 'd3AlphaTarget' | 'resetCountdown';
|
||||
|
||||
interface ForceGraph3DGenericInstance<ChainableInstance, N extends NodeObject = NodeObject, L extends LinkObject<N> = LinkObject<N>>
|
||||
extends Omit<ThreeForceGraphGeneric<ChainableInstance, N, L>, ExcludedInnerProps> {
|
||||
|
||||
_destructor(): void;
|
||||
|
||||
// Container layout
|
||||
width(): number;
|
||||
width(width: number): ChainableInstance;
|
||||
height(): number;
|
||||
height(height: number): ChainableInstance;
|
||||
backgroundColor(): string;
|
||||
backgroundColor(color: string): ChainableInstance;
|
||||
showNavInfo(): boolean;
|
||||
showNavInfo(enabled: boolean): ChainableInstance;
|
||||
|
||||
// Labels
|
||||
nodeLabel(): ObjAccessor<Label, N>;
|
||||
nodeLabel(textAccessor: ObjAccessor<Label, N>): ChainableInstance;
|
||||
linkLabel(): ObjAccessor<Label, L>;
|
||||
linkLabel(textAccessor: ObjAccessor<Label, L>): ChainableInstance;
|
||||
|
||||
// Interaction
|
||||
onNodeClick(callback: (node: N, event: MouseEvent) => void): ChainableInstance;
|
||||
onNodeRightClick(callback: (node: N, event: MouseEvent) => void): ChainableInstance;
|
||||
onNodeHover(callback: (node: N | null, previousNode: N | null) => void): ChainableInstance;
|
||||
onNodeDrag(callback: (node: N, translate: Coords) => void): ChainableInstance;
|
||||
onNodeDragEnd(callback: (node: N, translate: Coords) => void): ChainableInstance;
|
||||
onLinkClick(callback: (link: L, event: MouseEvent) => void): ChainableInstance;
|
||||
onLinkRightClick(callback: (link: L, event: MouseEvent) => void): ChainableInstance;
|
||||
onLinkHover(callback: (link: L | null, previousLink: L | null) => void): ChainableInstance;
|
||||
onBackgroundClick(callback: (event: MouseEvent) => void): ChainableInstance;
|
||||
onBackgroundRightClick(callback: (event: MouseEvent) => void): ChainableInstance;
|
||||
linkHoverPrecision(): number;
|
||||
linkHoverPrecision(precision: number): ChainableInstance;
|
||||
enablePointerInteraction(): boolean;
|
||||
enablePointerInteraction(enable: boolean): ChainableInstance;
|
||||
enableNodeDrag(): boolean;
|
||||
enableNodeDrag(enable: boolean): ChainableInstance;
|
||||
enableNavigationControls(): boolean;
|
||||
enableNavigationControls(enable: boolean): ChainableInstance;
|
||||
|
||||
// Render control
|
||||
pauseAnimation(): ChainableInstance;
|
||||
resumeAnimation(): ChainableInstance;
|
||||
cameraPosition(): Coords;
|
||||
cameraPosition(position: Partial<Coords>, lookAt?: Coords, transitionMs?: number): ChainableInstance;
|
||||
zoomToFit(durationMs?: number, padding?: number, nodeFilter?: (node: N) => boolean): ChainableInstance;
|
||||
postProcessingComposer(): EffectComposer;
|
||||
lights(): Light[];
|
||||
lights(lights: Light[]): ChainableInstance;
|
||||
scene(): Scene;
|
||||
camera(): Camera;
|
||||
renderer(): WebGLRenderer;
|
||||
controls(): object;
|
||||
|
||||
// Utility
|
||||
graph2ScreenCoords(x: number, y: number, z: number): Coords;
|
||||
screen2GraphCoords(screenX: number, screenY: number, distance: number): Coords;
|
||||
}
|
||||
|
||||
export type ForceGraph3DInstance<NodeType extends NodeObject = NodeObject, LinkType extends LinkObject<NodeType> = LinkObject<NodeType>>
|
||||
= ForceGraph3DGenericInstance<ForceGraph3DInstance<NodeType, LinkType>, NodeType, LinkType>;
|
||||
|
||||
interface IForceGraph3D<NodeType extends NodeObject = NodeObject, LinkType extends LinkObject<NodeType> = LinkObject<NodeType>> {
|
||||
new(element: HTMLElement, configOptions?: ConfigOptions): ForceGraph3DInstance<NodeType, LinkType>;
|
||||
}
|
||||
|
||||
declare const ForceGraph3D: IForceGraph3D;
|
||||
|
||||
export default ForceGraph3D;
|
3
render/static/js/index.js
Normal file
3
render/static/js/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import './3d-force-graph.css';
|
||||
|
||||
export { default } from "./3d-force-graph.js";
|
26
render/static/js/kapsule-link.js
Normal file
26
render/static/js/kapsule-link.js
Normal file
@ -0,0 +1,26 @@
|
||||
export default function(kapsulePropName, kapsuleType) {
|
||||
|
||||
const dummyK = new kapsuleType(); // To extract defaults
|
||||
dummyK._destructor && dummyK._destructor();
|
||||
|
||||
return {
|
||||
linkProp: function(prop) { // link property config
|
||||
return {
|
||||
default: dummyK[prop](),
|
||||
onChange(v, state) { state[kapsulePropName][prop](v) },
|
||||
triggerUpdate: false
|
||||
}
|
||||
},
|
||||
linkMethod: function(method) { // link method pass-through
|
||||
return function(state, ...args) {
|
||||
const kapsuleInstance = state[kapsulePropName];
|
||||
const returnVal = kapsuleInstance[method](...args);
|
||||
|
||||
return returnVal === kapsuleInstance
|
||||
? this // chain based on the parent object, not the inner kapsule
|
||||
: returnVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
153
render/templates/index.html
Normal file
153
render/templates/index.html
Normal file
@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
#3d-graph { width: 100vw; height: 100vh; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
}
|
||||
#node-details {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
}
|
||||
#close-tooltip {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<!-- Load the ForceGraph library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/3d-force-graph@latest/dist/3d-force-graph.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="3d-graph"></div>
|
||||
<div class="tooltip" id="node-tooltip">
|
||||
<span id="close-tooltip">×</span>
|
||||
</div>
|
||||
<div id="node-details">
|
||||
<h3>Node Details</h3>
|
||||
<pre id="node-details-content"></pre>
|
||||
</div>
|
||||
<script>
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch('/data/policymap.json');
|
||||
const textData = await response.text();
|
||||
const jsonData = textData.trim().split('\n').map(line => JSON.parse(line));
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
jsonData.forEach(item => {
|
||||
if (item.labels) {
|
||||
const id = item.id || item.properties.id;
|
||||
let name = '';
|
||||
let title = '';
|
||||
if (item.properties.name) {
|
||||
name = item.properties.name;
|
||||
} else if (item.properties.title) {
|
||||
title = item.properties.title;
|
||||
}
|
||||
nodes.push({
|
||||
id: id,
|
||||
name: name,
|
||||
title: title,
|
||||
type: item.labels[0],
|
||||
properties: item.properties
|
||||
});
|
||||
} else if (item.start.id && item.end.id) {
|
||||
links.push({
|
||||
source: item.start.id,
|
||||
target: item.end.id,
|
||||
value: item?.weight || 1,
|
||||
label: item.label
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
nodes: nodes,
|
||||
links: links
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching or transforming JSON:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function initGraph() {
|
||||
const data = await fetchData();
|
||||
if (!data || !data.nodes || !data.links) {
|
||||
console.error('Data is incomplete:', data);
|
||||
return;
|
||||
}
|
||||
const elem = document.getElementById('3d-graph');
|
||||
const tooltip = document.getElementById('node-tooltip');
|
||||
const nodeDetailsContent = document.getElementById('node-details-content');
|
||||
const closeTooltipButton = document.getElementById('close-tooltip');
|
||||
|
||||
closeTooltipButton.addEventListener('click', () => {
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
|
||||
const Graph = ForceGraph3D()(elem)
|
||||
.graphData(data)
|
||||
.nodeColor(node => {
|
||||
if (node.labels === 'Bill' || node.type === 'Legislation') {
|
||||
return 'pink';
|
||||
} else if (node.labels === 'Order') {
|
||||
return 'orange';
|
||||
} else if (node.type === 'Law') {
|
||||
return 'blue';
|
||||
} else if (node.type === 'Person') {
|
||||
return 'red';
|
||||
}
|
||||
})
|
||||
.nodeLabel(node => node.labels === 'Person' ? node.properties.name : node.properties.title || 'Node')
|
||||
.linkWidth(link => link.weight || 1)
|
||||
.onNodeHover(node => {
|
||||
if (node) {
|
||||
const properties = Object.entries(node.properties).map(([key, value]) => `${key}: ${value}`).join('<br>');
|
||||
tooltip.innerHTML = properties;
|
||||
tooltip.style.display = 'block';
|
||||
} else {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.onNodeDragEnd(() => tooltip.style.display = 'none')
|
||||
.onNodeClick(node => {
|
||||
const nodeDetailsDiv = document.getElementById('node-details');
|
||||
if (node) {
|
||||
nodeDetailsContent.innerHTML = JSON.stringify(node.properties, null, 2);
|
||||
nodeDetailsDiv.style.display = 'block';
|
||||
} else {
|
||||
nodeDetailsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
elem.addEventListener('mousemove', event => {
|
||||
const rect = elem.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
tooltip.style.transform = `translate(${x}px, ${y}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
initGraph();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
99
update_laws.py
Normal file
99
update_laws.py
Normal file
@ -0,0 +1,99 @@
|
||||
import sys
|
||||
from neo4j import GraphDatabase
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from flatten_json import flatten
|
||||
|
||||
# Global variable to store the list of bill numbers
|
||||
bill_numbers = []
|
||||
|
||||
def search_bills(bill_type):
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Get connection information from environment variables
|
||||
uri = os.getenv('NEO4J_URI')
|
||||
user = os.getenv('NEO4J_USER')
|
||||
password = os.getenv('NEO4J_PASSWORD')
|
||||
|
||||
# Connect to Neo4j database
|
||||
driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
|
||||
try:
|
||||
with driver.session() as session:
|
||||
# Query to find nodes with label 'Bill' and property 'type' matching the provided value
|
||||
query = "MATCH (b:Law) WHERE b.laws_0_type = 'Public Law' RETURN b.number"
|
||||
|
||||
# Execute the query
|
||||
result = session.run(query, bill_type=bill_type)
|
||||
|
||||
# Collect the list of bill numbers
|
||||
global bill_numbers
|
||||
bill_numbers = [record["b.number"] for record in result]
|
||||
finally:
|
||||
# Close the driver connection
|
||||
driver.close()
|
||||
|
||||
def get_bill_details(congress, bill_type, bill_number):
|
||||
url = f"https://api.congress.gov/v3/law/{congress}/{bill_type.lower()}/{bill_number}?format=json&api_key={os.getenv('CONGRESS_API_KEY')}"
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to fetch bill details for {bill_number}: {response.status_code}")
|
||||
print(f"Response Text: {response.text}")
|
||||
return None
|
||||
|
||||
def flatten_json(y):
|
||||
out = {}
|
||||
|
||||
def flatten(x, name=''):
|
||||
if type(x) is dict:
|
||||
for a in x:
|
||||
flatten(x[a], name + a + '.')
|
||||
elif type(x) is list:
|
||||
i = 0
|
||||
for a in x:
|
||||
flatten(a, name + str(i) + '.')
|
||||
i += 1
|
||||
else:
|
||||
out[name[:-1]] = x
|
||||
|
||||
flatten(y)
|
||||
return {k.replace('bill.', ''): v for k, v in out.items()}
|
||||
|
||||
def update_bill_node(driver, bill_number, properties):
|
||||
with driver.session() as session:
|
||||
# Remove existing properties
|
||||
query_remove_properties = f"MATCH (b:Law {{number: $bill_number}}) SET b += {{}}"
|
||||
session.run(query_remove_properties, bill_number=bill_number)
|
||||
|
||||
# Add new properties
|
||||
query_add_properties = f"MATCH (b:Law {{number: $bill_number}}) SET b += $properties RETURN b"
|
||||
session.run(query_add_properties, bill_number=bill_number, properties=properties)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python search_bills.py <congress> <bill_type>")
|
||||
sys.exit(1)
|
||||
|
||||
congress = sys.argv[1]
|
||||
bill_type = sys.argv[2]
|
||||
|
||||
search_bills(bill_type)
|
||||
|
||||
# Connect to Neo4j database
|
||||
driver = GraphDatabase.driver(os.getenv('NEO4J_URI'), auth=(os.getenv('NEO4J_USER'), os.getenv('NEO4J_PASSWORD')))
|
||||
|
||||
for bill_number in bill_numbers:
|
||||
print(f"Fetching details for bill number {bill_number}...")
|
||||
bill_details = get_bill_details(congress, bill_type, bill_number)
|
||||
if bill_details:
|
||||
flattened_properties = flatten_json(bill_details)
|
||||
update_bill_node(driver, bill_number, flattened_properties)
|
||||
print(f"Updated bill node with properties from JSON response for bill number {bill_number}")
|
||||
|
||||
# Close the driver connection
|
||||
driver.close()
|
Loading…
Reference in New Issue
Block a user