Forums

React Flow Interactive Diagram with Flask App

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

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!

It's a good practice to use logs / debugger tools when something is not working as expected. If something is not workig at the frontend side I'd check the Network tab in the developer tools in the browser while hitting the Visualize button and see if you get any failed requests and/or the Console tab for JS errors, for example.

Thanks, I actually found some errors! It looks like there are 2 main problems:

1) I'm getting 404 not found errors for some of the import statements I have in my html:

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

In particular, "https://unpkg.com/react-flow-renderer@11/umd/react-flow-renderer.development.js" and "https://unpkg.com/react-flow@11/umd/react-flow.development.js" are giving the not found errors. Any ideas what I should replace them with?

2) dagre core error

While it looks like the dagre package is being found, in my Firefox network debugger tool I see the following error message a bunch of times from the dagre.core.js package : "Uncaught TypeError: require(...) is undefined"

Do I also need to update this package, or is there something else I need to do?

For more context, I have reactflow v11.8.3, React v18.2.0, npm v9.8.1, nvm v0.37.2, dagre v0.8.5, and Node.js v20.6.1 installed, and I do have Vite but I can't use a devserver since I can't access localhost

Is there a way for me to upload pictures from my computer here? I also have a screenshot of the errors from the network tab if that would be helpful

Tried uploading picture and it didn't work :/

Ok, I got some advice on the import statements and updated them so they work! (I was using legacy modules) But any ideas on the dagre core TypeError? I don't think I can post pictures, but you should be able to see the error in the network tab of debugging tools on ronjones.pythonanywhere.com

Or actually, do I even need a dagre script tag in the html if I'm only using it in the backend to update node coordinates?

The dagre TypeError sounds like an issue with dagre or how you are using it. You'll have better luck looking for help on forums devoted to dagre.

Gotcha, I asked and I think got it resolved in a dagre group! But unfortunately, my react component won't load :/ are there any tutorials on serving react flow components through a Flask app on PythonAnywhere?

I'm afraid we don't have a help page for that.