Skip to content

20 Visualization Frontend

JP Barbosa edited this page Apr 15, 2023 · 2 revisions

Visualization Frontend

Install Required Packages

npm install react-graph-vis

Allow Global To Vite

code ./vite.config.ts
...

export default defineConfig({
  ...

  define: {
    global: {},
  },
});

Fix React Vis Types

code ./packages/web/src/react-graph-vis.d.ts
declare module 'react-graph-vis' {
  import { Network, NetworkEvents, Options, Node, Edge, DataSet } from 'vis';
  import { Component } from 'react';

  export { Network, NetworkEvents, Options, Node, Edge, DataSet } from 'vis';

  export interface graphEvents {
    [event: NetworkEvents]: (params?: any) => void;
  }

  //Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis.
  export interface GraphVisData {
    nodes: Node[];
    edges: Edge[];
  }

  export interface NetworkGraphProps {
    graph: GraphVisData;
    options?: Options;
    events?: graphEvents;
    getNetwork?: (network: Network) => void;
    identifier?: string;
    style?: React.CSSProperties;
    getNodes?: (nodes: DataSet) => void;
    getEdges?: (edges: DataSet) => void;
  }

  export interface NetworkGraphState {
    identifier: string;
  }

  export default class NetworkGraph extends Component<
    NetworkGraphProps,
    NetworkGraphState
  > {
    render();
  }
}

API Helper

code ./packages/web/src/api/visualization.ts
import axios from 'axios';
import { GraphVisData } from 'react-graph-vis';

const url = `${import.meta.env.VITE_API_URL}/visualization`;

export const visualization = {
  get: (search: string) =>
    axios.get<GraphVisData>(`${url}?search=${search}`).then((res) => res.data),
};
code ./packages/web/src/api/index.ts
...
export * from './visualization';

Page

code ./packages/web/src/pages/visualization/index.tsx
import { useState } from 'react';
import { Content } from './Content';

export const Visualization: React.FC = () => {
  const [search, setSearch] = useState<string>('');

  return (
    <div className="page">
      <div className="actions-bar">
        <h2>Graph Visualization</h2>
        <div className="filter">
          <input
            type="text"
            value={search}
            placeholder="Search by movie title or actor name..."
            onChange={(e) => setSearch(e.target.value)}
          />
        </div>
      </div>
      <Content search={search} />
    </div>
  );
};
code ./packages/web/src/pages/visualization/Content.tsx
import { useQuery } from 'react-query';
import { GraphVisData } from 'react-graph-vis';
import { AxiosCustomError } from '@neo4j-crud/shared';
import * as api from '../../api';
import { AlertCombo, GraphVis } from '../../components';
import { useDebounce } from '../../hooks/useDebounce';

type ContentProps = {
  search: string;
};

export const Content: React.FC<ContentProps> = ({ search }) => {
  const debouncedSearch = useDebounce(search, 500);

  const { data, error, isLoading } = useQuery<GraphVisData, AxiosCustomError>(
    ['visualization', debouncedSearch],
    () => api.visualization.get(search)
  );

  const noData = !data || data.nodes.length === 0;

  if (error || isLoading || noData) {
    return <AlertCombo error={error} isLoading={isLoading} noData={noData} />;
  }

  return (
    <div
      style={{
        height: '100%',
      }}
    >
      <GraphVis graph={data} />
    </div>
  );
};
code ./packages/web/src/components/GraphVis.tsx
import ReactGraphVis, { GraphVisData } from 'react-graph-vis';

type GraphVisProps = {
  graph: GraphVisData;
};

export const GraphVis: React.FC<GraphVisProps> = ({ graph }) => {
  return (
    <ReactGraphVis
      graph={graph}
      options={{
        //autoResize: true,
        height: '100%',
        nodes: {
          shape: 'dot',
          font: {
            strokeWidth: 4,
          },
        },
        edges: {
          width: 1,
          color: 'darkorange',
          length: 200,
          font: {
            size: 10,
            strokeWidth: 3,
            //   align: 'middle', // Performance issue.
          },
        },
        physics: {
          // Even though it's disabled the options still apply to network.stabilize().
          enabled: true,
          solver: 'repulsion',
          repulsion: {
            nodeDistance: 200, // Put more distance between the nodes.
          },
          stabilization: {
            iterations: 200,
            //fit: true,
          },
        },
      }}
      getNetwork={(network) => {
        network.fit({
          animation: {
            duration: 1000,
            easingFunction: 'easeInOutQuad',
          },
          nodes: graph.nodes.map((n) => String(n.id)),
        });
      }}
    />
  );
};
code ./packages/web/src/components/index.ts
...
export * from './GraphVis';
code ./packages/web/src/pages/index.ts
...
export * from './visualization';
code ./packages/web/src/app/app.tsx
...
import { ..., Visualization } from './pages';

export function App() {
  return (
    <>
      <Header />
      <Routes>
        ...
        <Route path="/visualization/*" element={<Visualization />} />
      </Routes>
    </>
  );
}

export default App;

Update Header

code ./packages/web/src/components/Header.tsx
...

export const Header: React.FC = () => {
  const navItems: NavItem[] = [
    ...
    { path: '/visualization', label: 'Graph Visualization' },
  ];

  return ...;
};

Test

open http://localhost:4200/visualization
Graph Search

Commit

git add .
git commit -m "Visualization Frontend"