add a bunch of stuff

rendering the graph via exported json works now
This commit is contained in:
Moses Rolston 2025-03-20 06:45:07 -07:00
parent b012392fc9
commit 276b57fcdf
16 changed files with 18274 additions and 3 deletions

View File

@ -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:

View File

@ -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
View 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
View 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()

View 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
View 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
View 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)

View 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;
}

View 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;
}

File diff suppressed because one or more lines are too long

View 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
View 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;

View File

@ -0,0 +1,3 @@
import './3d-force-graph.css';
export { default } from "./3d-force-graph.js";

View 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
View 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">&times;</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
View 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()