From c3d7fdd83fbb033df084c880a2ef60bb8cd17458 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Sat, 13 Jan 2024 18:11:33 +0200 Subject: [PATCH] add colors --- app/graph/model.ts | 114 +++++++++++++++++ app/graph/page.tsx | 238 +++++++++++++++-------------------- app/layout.tsx | 12 +- components/custom/navbar.tsx | 2 +- components/ui/resizable.tsx | 45 +++++++ components/ui/tooltip.tsx | 30 +++++ package-lock.json | 196 +++++++++++++++++++++++++++++ package.json | 2 + 8 files changed, 500 insertions(+), 139 deletions(-) create mode 100644 app/graph/model.ts create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/tooltip.tsx diff --git a/app/graph/model.ts b/app/graph/model.ts new file mode 100644 index 00000000..e25e70fd --- /dev/null +++ b/app/graph/model.ts @@ -0,0 +1,114 @@ + +export interface Category { + name: string, + index: number +} + +export interface Node { + id: string, + name: string, + value: any, + color: string, +} + +export interface Edge { + id: number, + source: number, + target: number, + label: string, + value: any, +} + +const COLORS = [ + "#ff0000", // red + "#0000ff", // blue + "#00ff00", // green + "#ffff00", // yellow + "#ff00ff", // magenta + "#00ffff", // cyan + "#ffffff", // white + "#000000", // black + "#800000", // maroon + "#808000", // olive +] + +interface GraphResult { + data: any[], + metadata: any +} + +export interface ExtractedData { + data: any[][], + columns: string[], + categories: Map, + nodes: Map, + edges: Map, +} + +export function extractData(results: GraphResult | null): ExtractedData { + let columns: string[] = [] + let data: any[][] = [] + if (results?.data?.length) { + if (results.data[0] instanceof Object) { + columns = Object.keys(results.data[0]) + } + data = results.data + } + + let nodes = new Map() + let categories = new Map() + categories.set("default", { name: "default", index: 0 }) + + let edges = new Map() + + data.forEach((row: any[]) => { + Object.values(row).forEach((cell: any) => { + if (cell instanceof Object) { + if (cell.relationshipType) { + + let edge = edges.get(cell.id) + if (!edge) { + let sourceId = cell.sourceId.toString(); + let destinationId = cell.destinationId.toString() + edges.set(cell.id, { id: cell.id, source: sourceId, target: destinationId, label: cell.relationshipTyp, value: {} }) + + // creates a fakeS node for the source and target + let source = nodes.get(cell.sourceId) + if (!source) { + source = { id: cell.sourceId.toString(), name: cell.sourceId.toString(), value: "", color: COLORS[0] } + nodes.set(cell.sourceId, source) + } + + let destination = nodes.get(cell.destinationId) + if (!destination) { + destination = { id: cell.destinationId.toString(), name: cell.destinationId.toString(), value: "", color: COLORS[0] } + nodes.set(cell.destinationId, destination) + } + } + } else if (cell.labels) { + + // check if category already exists in categories + let category = categories.get(cell.labels[0]) + if (!category) { + category = { name: cell.labels[0], index: categories.size } + categories.set(category.name, category) + } + + // check if node already exists in nodes or fake node was created + let node = nodes.get(cell.id) + if (!node || node.value === "") { + node = { + id: cell.id.toString(), + name: cell.id.toString(), + value: JSON.stringify(cell), + color: category.index < COLORS.length ? COLORS[category.index] : COLORS[0] + } + nodes.set(cell.id, node) + } + } + } + }) + }) + + return { data, columns, categories, nodes, edges } +} \ No newline at end of file diff --git a/app/graph/page.tsx b/app/graph/page.tsx index bc15f1c4..ebd75193 100644 --- a/app/graph/page.tsx +++ b/app/graph/page.tsx @@ -5,13 +5,60 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { toast } from "@/components/ui/use-toast"; import CytoscapeComponent from 'react-cytoscapejs' -import { useState } from "react"; +import { useRef, useState } from "react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { XCircle, ZoomIn, ZoomOut } from "lucide-react"; +import { Edge, Node, extractData } from "./model"; + +// The stylesheet for the graph +const STYLESHEET: cytoscape.Stylesheet[] = [ + { + selector: "node", + style: { + label: "data(name)", + "text-valign": "center", + "text-halign": "center", + shape: "ellipse", + height: 10, + width: 10, + "background-color": "data(color)", + "font-size": "3", + "overlay-padding": "2px", + }, + }, + { + selector: "edge", + style: { + width: 0.5, + "line-color": "#ccc", + "arrow-scale": 0.3, + "target-arrow-shape": "triangle", + label: "data(label)", + 'curve-style': 'straight', + "text-background-color": "#ffffff", + "text-background-opacity": 1, + "font-size": "3", + "overlay-padding": "2px", + + }, + }, +] + +const LAYOUT = { + name: "cose", + fit: true, + padding: 30, + avoidOverlap: true, +} export default function Page() { const [query, setQuery] = useState(''); const [graph, setGraph] = useState(''); const [elements, setElements] = useState([] as any); + // A reference to the chart container to allowing zooming and editing + const chartRef = useRef(null) + function updateQuery(event: React.ChangeEvent) { setQuery(event.target.value) } @@ -47,10 +94,10 @@ export default function Page() { .then((data) => { let elements: any[] = [] - data.nodes.forEach((node: GraphData) => { + data.nodes.forEach((node: Node) => { elements.push({ data: node }) }) - data.edges.forEach((node: GraphLink) => { + data.edges.forEach((node: Edge) => { elements.push({ data: node }) }) @@ -59,6 +106,21 @@ export default function Page() { }) } + function handleZoomClick(changefactor: number) { + let chart = chartRef.current + if (chart) { + chart.zoom(chart.zoom() * changefactor) + } + } + + function handleCenterClick() { + let chart = chartRef.current + if (chart) { + chart.fit() + chart.center() + } + } + return (
@@ -70,142 +132,50 @@ export default function Page() {
{elements.length > 0 && -
+
+
+ + + handleZoomClick(1.1)}> + +

Zoom In

+
+
+ + handleZoomClick(0.9)}> + +

Zoom Out

+
+
+ + + +

Center

+
+
+
+
{ + chartRef.current = cy + + // Make sure no previous listeners are attached + cy.removeAllListeners(); + + // Listen to the click event on nodes for expanding the node + cy.on('dbltap', 'node', function (evt) { + var node: Node = evt.target.json().data; + // TODO: + // parmas.onFetchNode(node); + }); }} + stylesheet={STYLESHEET} + elements={elements} + layout={LAYOUT} className="w-full h-full" /> -
+ }
) } - -export interface Category { - name: string, - index: number -} - -export interface GraphData { - id: number, - name: string, - value: string, - label: string -} - -export interface GraphLink { - id: number, - source: string, - target: string, - label: string -} - -interface GraphResult { - data: any[], - metadata: any -} - -interface ExtractedData { - data: any[][], - columns: string[], - categories: Map, - nodes: Map, - edges: Map, -} - -function extractData(results: GraphResult | null): ExtractedData { - let columns: string[] = [] - let data: any[][] = [] - if (results?.data?.length) { - if (results.data[0] instanceof Object) { - columns = Object.keys(results.data[0]) - } - data = results.data - } - - let nodes = new Map() - let categories = new Map() - categories.set("default", { name: "default", index: 0 }) - - let edges = new Map() - - data.forEach((row: any[]) => { - Object.values(row).forEach((cell: any) => { - if (cell instanceof Object) { - if (cell.relationshipType) { - - let edge = edges.get(cell.id) - if(!edge) { - let sourceId = cell.sourceId.toString(); - let destinationId = cell.destinationId.toString() - edges.set(cell.id, { id: cell.id, source: sourceId, target: destinationId, label: cell.relationshipType }) - - // creates a fakeS node for the source and target - let source = nodes.get(cell.sourceId) - if (!source) { - source = { id: cell.sourceId.toString(), name: cell.sourceId.toString(), value: "", label: "" } - nodes.set(cell.sourceId, source) - } - - let destination = nodes.get(cell.destinationId) - if (!destination) { - destination = { id: cell.destinationId.toString(), name: cell.destinationId.toString(), value: "", label: "" } - nodes.set(cell.destinationId, destination) - } - } - } else if (cell.labels) { - - // check if category already exists in categories - let category = categories.get(cell.labels[0]) - if (!category) { - category = { name: cell.labels[0], index: categories.size } - categories.set(category.name, category) - } - - // check if node already exists in nodes or fake node was created - let node = nodes.get(cell.id) - if (!node || node.value === "") { - node = { id: cell.id.toString(), name: cell.id.toString(), value: JSON.stringify(cell), label: category.name } - nodes.set(cell.id, node) - } - } - } - }) - }) - - return { data, columns, categories, nodes, edges } -} diff --git a/app/layout.tsx b/app/layout.tsx index c4cbe8de..307db4fc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import Navbar from '@/components/custom/navbar' +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable' const inter = Inter({ subsets: ['latin'] }) @@ -19,10 +20,13 @@ export default function RootLayout({
- -
- {children} -
+ + + + + + {children} +
diff --git a/components/custom/navbar.tsx b/components/custom/navbar.tsx index d363abc2..e4298486 100644 --- a/components/custom/navbar.tsx +++ b/components/custom/navbar.tsx @@ -2,7 +2,7 @@ import { AirVentIcon } from "lucide-react"; export default function Navbar() { return ( -