Hi everyone!
I'm building a web app to help visualize SQL queries with Directed Acyclic Graphs. The good news is, I've been using Python for data analysis for a while and got the back-end processing to work, so I can display the visualizations! (Check out http://ronjones.pythonanywhere.com/)
Currently, I'm returning static images of the visualized queries on the right pane of my site, but I'd like to show an interactive version of the charts on the right half instead.
I think this would both improve the user experience and help me handle more traffic (currently I'm saving all rendered graph files and deleting them at the end of the day, but my free membership will have a storage problem as more people use it, so I'd like to render the graphs without storing any files).
I did some research and am pretty confident this is possible through React Flow (to build the DAGs) and Dagre (to balance the node positioning). I even made an example DAG in a React Flow sandbox (https://codesandbox.io/s/priceless-nobel-zfzcn5) and am have my Python data processed to deliver to React Flow as a JSON object containing node and edge definitions like the structure in the sandbox.
I thought all I had to do was make some JSON objects in Python -> pass that into Javascript -> spin up a React Flow app with a balanced DAG component -> place that in the right-container
div of my html when I render the page for the user and I'd be done. But, looks like I'm doing something wrong.
I have extremely little Javascript experience and no prior React experience, and I think I must have implemented React Flow wrong somehow. There are no errors in the PythonAnywhere files/logs, and no errors on the browser console. Through some research (https://reactflow.dev/docs/quickstart/) and AI assistance, I installed node.js, npm, nvm, dagre, and react flow on my account, and created the following files (source code included). Here is the structure of my directory:
- home
- ronjones
- .npm/
- .nvm/
- node_modules
- mysite
- flask_app.py
- processing.py
- static
- styling.css
- react-dom.development.js (not mine, downloaded from the internet)
- react.development.js (not mine, downloaded from the internet)
- my-react-flow-app
- index.html
- package-lock.json
- package.json
- vite.config.js
- README.md/.gitignore/.eslintrc.cjs
- node_modules
- lots of individual files
- public
- vite.svg
- src
- App.css
- App.jsx
- dagre_interactive.jsx
- index.css
- main.jsx
- assets
- react.svg
- ronjones
And here are the react files (most were auto-installed after I got React Flow, but I made dagre_interactive.jsx and edited App.jsx):
App.css:
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
App.jsx:
import React from 'react';
import ReactFlow from 'reactflow';
import 'reactflow/dist/style.css';
import DagreInteractive from './dagre_interactive.jsx';
export default function App() {
return (
<div className="App">
<DagreInteractive />
</div>
);
}
Index.css:
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
Main.jsx:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
I wrote this file, dagre_interactive.jsx, to create and balance my DAG given the node and edges object:
// import React, { useEffect } from 'react';
// import dagre from 'dagre';
const dagre = require('dagre');
const React = require('react');
const { useEffect } = require('react');
const DagreInteractive = () => {
useEffect(() => {
const runDagreLayout = (nodes, edges) => {
// Create a new graph
const graph = new dagre.graphlib.Graph();
// Set an attribute 'rankdir' to specify layout direction ('TB' for top to bottom layout)
// Increase the ranksep and nodesep for more separation
graph.setGraph({ rankdir: 'TB', ranksep: 100, nodesep: 100 });
// Set default edge label (if needed)
graph.setDefaultEdgeLabel(() => ({}));
nodes.forEach(node => {
graph.setNode(node.id, { width: 50, height: 50 }); // Set node dimensions as needed
});
edges.forEach(edge => {
graph.setEdge(edge.source, edge.target);
});
// Perform layout
dagre.layout(graph);
// Retrieve node positions and update existing positions
const nodePositions = nodes.map(node => {
const layoutNode = graph.node(node.id);
node.position = { x: layoutNode.x, y: layoutNode.y }; // Update node position
return node;
});
// Output updated nodes
console.log(nodePositions);
// Output updated edges (if needed)
const updatedEdges = edges.map(edge => {
return {
...edge,
sourcePosition: nodePositions.find(node => node.id === edge.source).position,
targetPosition: nodePositions.find(node => node.id === edge.target).position,
};
});
// Return the final nodes and edges
return { nodes: nodePositions, edges: updatedEdges };
};
// Extract nodes and edges from the JSON data (Assuming jsonData is available)
const { nodes, edges } = jsonData; // Make sure jsonData is defined
// Call the function
const updatedData = runDagreLayout(nodes, edges);
// You can use updatedData as needed
}, []);
return <div id="dagreContainer" style={{ height: '100%' }}></div>;
};
// export default DagreInteractive;
module.exports = DagreInteractive;
Here is the (edited for a bit of clarity) flask app code that I'm currently using to render my html (it looks like templates might have been better than a formatted html string, but I'm not super comfortable with web dev so I followed this tutorial - https://blog.pythonanywhere.com/169/):
from flask import Flask, request
import processing
from sql_metadata import Parser
#import sys
import json
import mimetypes
# import os
app = Flask(__name__, static_url_path='/static')
app.config["DEBUG"] = True
mimetypes.add_type("text/javascript", ".js", True)
@app.route('/', methods=["GET", "POST"])
def hello_world():
json_data=""
if request.method == "POST":
if query is not None:
try:
json_data = json.dumps(json_dict)
except:
errors += "<p>There was a problem rendering your DAG :( </p>\n"
return f'''
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/static/styling.css" />
<!-- Importing JS modules using CDN for React Flow and Dagre -->
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-flow-renderer@11/umd/react-flow-renderer.development.js">
</script>
<script src="https://unpkg.com/react-flow@11/umd/react-flow.development.js"></script>
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.core.js"></script>
</head>
<body>
<div class="container">
<div class="left-half">
<div class="resizer"></div>
<h1> Welcome to my SQL Visualizer!</h1>
<form method="post" action=".">
<textarea name="user_input" id="editor" rows="45" cols="90">{query_for_box}</textarea></br>
<input type="submit" value="Visualize Query">
</form>
</div>
<div class="right-half">
<div id="dagreContainer" style="height: 100%;"></div>
<script>
var jsonData = {{json_data}}; // Embed the JSON data directly
</script>
<script type="text/jsx" src="static/my-react-flow-app/src/dagre_interactive.jsx"></script>
</div>
</div>
</body>
</html>
'''.format(errors=errors, query_for_box=query_for_box, json_data=json_data)
And here is styling.css, the stylesheet I wrote for the main page:
body {
font-family: 'Titillium Web', sans-serif;
}
html, body {
margin: 0;
height: 100%;
}
.container {
display: flex;
}
.left-half {
flex: 1;
background: linear-gradient(180deg,#fff,#F1F5F9);
/*background-color: #f5f5f5; /*#f0f0f0;*/
position: relative;
padding-right: 100px;
padding-left: 20px;
padding-bottom: 20px;
max-height: 100%; /*set maximum height to vh, NEEEEEWWW */
overflow-y: auto; /*Enable vertical scrollbar*/
}
.right-half {
flex: 1;
padding: 20px;
background-color: #ffffff; /* White background for contrast */
max-height: 100%; /*set maximum height to vh, NEEEEEWWW */
overflow-y: auto; /*Enable vertical scrollbar */
text-align: center;
}
.resizer {
width: 5px;
height: 100%;
background: #666;
cursor: ew-resize; /*Indicates a horizontal resizing cursor*/
position: absolute;
top: 0;
right: 0px; /* Adjust this value as needed*/
}
body {
margin: 0;
height: 100%;
background-color: #ffffff; /* White background color */
color: #333333; /* Dark text color */
}
/* Text styles */
h1 {
font-size: 2em; /* Larger heading */
color: #333333;
margin-bottom: 10px; /* Added margin for breathing space */
}
p {
font-size: 1.1em; /* Slightly larger font */
color: #555555;
line-height: 1.6; /* Increased line height for readability */
}
/* Button styles */
input[type="submit"] {
background-color: #4caf50;
color: #ffffff;
padding: 10px 20px;
border: none;
font-size: 1em;
cursor: pointer;
transition: background-color 0.3s; /* Smooth button hover effect */
border-radius: 4px; /* Rounded corners */
}
input[type="submit"]:hover {
background-color: #45a049; /* Darker green on hover */
}
My React app is in my /static folder and I have set up static file mapping (URL = '/static/' Directory = '/home/ronjones/mysite/static'). When I run this code my webpage renders normally, but when I click Visualize the interactive DAG doesn't appear (I have the static version running right now for demonstration purposes).
Can anyone help me understand where I'm going wrong and how to fix it? I've been banging my head against the keyboard for so long and would be forever grateful!