@petrarca/sonnet-graph 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @petrarca/sonnet-graph
2
+
3
+ Renderer-agnostic property graph visualization, exploration, and enrichment
4
+ for the Petrarca Sonnet component library.
5
+
6
+ ## What's included
7
+
8
+ **GraphVisualizer** — Main visualization component. Renders a property graph
9
+ using the active renderer (Cytoscape or Reagraph). Supports node/edge
10
+ selection, focus mode, hide/show, and graph expansion.
11
+
12
+ **ActionPanel** — Side panel showing selected node/edge details and graph
13
+ exploration actions.
14
+
15
+ **GraphA11yList** — Accessible DOM mirror of the graph's selectable elements.
16
+ Cytoscape (canvas) and Reagraph (WebGL) draw nodes/edges outside the DOM, so they
17
+ are invisible to screen readers, keyboard users, and DOM automation; this renders
18
+ a virtualized list of node/edge `<button>`s that drive the same selection store
19
+ the canvas reads. `sr-only` by default (real apps); pass `visible` for an on-screen
20
+ outline panel.
21
+
22
+ **GraphRendererProvider** — Context provider that selects the active renderer
23
+ (`"cytoscape"` or `"reagraph"`).
24
+
25
+ **Store factories** — Create the Zustand stores the graph components need:
26
+ `createGraphDataStore`, `createGraphFilterStore`, `createGraphExplorationStore`,
27
+ `createSelectionStore`, `createGraphFocusStore`.
28
+
29
+ **Graph utilities** — `enrichNodes`, `enrichEdges`, `buildColorPalette` for
30
+ preparing raw graph data before passing it to stores.
31
+
32
+ ---
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pnpm add @petrarca/sonnet-graph
38
+ ```
39
+
40
+ ### Required peer dependencies
41
+
42
+ ```bash
43
+ pnpm add react@">=19" react-dom@">=19"
44
+ ```
45
+
46
+ ### Renderer peer dependencies
47
+
48
+ Install only the renderers you use:
49
+
50
+ ```bash
51
+ pnpm add cytoscape # Cytoscape.js renderer
52
+ pnpm add reagraph # Reagraph (3D/WebGL) renderer
53
+ ```
54
+
55
+ If you use `@petrarca/sonnet-playground` (which includes the graph demo page),
56
+ both `cytoscape` and `reagraph` are required even if your app only uses one renderer.
57
+
58
+ ### Cytoscape layout plugins (optional)
59
+
60
+ Install the layout algorithms you need. All are optional — the graph works
61
+ without them but with fewer layout options:
62
+
63
+ ```bash
64
+ pnpm add cytoscape-fcose # Force-directed, compound graph
65
+ pnpm add cytoscape-cola # Constraint-based
66
+ pnpm add cytoscape-dagre # DAG / hierarchical
67
+ pnpm add cytoscape-elk # Eclipse Layout Kernel
68
+ pnpm add cytoscape-cise # Circular layout
69
+ pnpm add cytoscape-klay # KLay hierarchical
70
+ pnpm add cytoscape-cxtmenu # Context menu extension
71
+ ```
72
+
73
+ ### Local development with SONNET_UI_LOCAL=1
74
+
75
+ When resolving `@petrarca/sonnet-graph` from the local dist (via Vite aliases),
76
+ the Cytoscape plugins must also be aliased to the consumer's `node_modules` —
77
+ Vite cannot find them relative to the dist file. Add to your `sonnet-aliases.ts`:
78
+
79
+ ```ts
80
+ const nm = path.resolve(baseDir, "node_modules");
81
+ // ...
82
+ "cytoscape-fcose": path.resolve(nm, "cytoscape-fcose"),
83
+ "cytoscape-cola": path.resolve(nm, "cytoscape-cola"),
84
+ "cytoscape-dagre": path.resolve(nm, "cytoscape-dagre"),
85
+ "cytoscape-elk": path.resolve(nm, "cytoscape-elk"),
86
+ "cytoscape-cise": path.resolve(nm, "cytoscape-cise"),
87
+ "cytoscape-klay": path.resolve(nm, "cytoscape-klay"),
88
+ "cytoscape-cxtmenu": path.resolve(nm, "cytoscape-cxtmenu"),
89
+ ```
90
+
91
+ See `sonnet-ui-starter/sonnet-aliases.ts` for a complete reference implementation.
92
+
93
+ ---
94
+
95
+ ## Basic usage
96
+
97
+ ```tsx
98
+ import {
99
+ GraphVisualizer,
100
+ ActionPanel,
101
+ GraphRendererProvider,
102
+ GraphStoresContext,
103
+ createGraphDataStore,
104
+ createGraphFilterStore,
105
+ createGraphExplorationStore,
106
+ createSelectionStore,
107
+ createGraphFocusStore,
108
+ enrichNodes,
109
+ enrichEdges,
110
+ buildColorPalette,
111
+ type RawGraphNode,
112
+ type RawGraphEdge,
113
+ type WorkspaceStores,
114
+ } from "@petrarca/sonnet-graph";
115
+
116
+ // Create stores once (e.g. in a context provider)
117
+ const stores: WorkspaceStores = {
118
+ graphData: createGraphDataStore(),
119
+ graphFilter: createGraphFilterStore(),
120
+ graphExploration: createGraphExplorationStore(),
121
+ selection: createSelectionStore(),
122
+ graphFocus: createGraphFocusStore(),
123
+ };
124
+
125
+ // Render
126
+ function MyGraph() {
127
+ return (
128
+ <GraphStoresContext.Provider value={stores}>
129
+ <GraphRendererProvider defaultRenderer="cytoscape">
130
+ <div style={{ display: "flex", height: 600 }}>
131
+ <GraphVisualizer />
132
+ <ActionPanel />
133
+ </div>
134
+ </GraphRendererProvider>
135
+ </GraphStoresContext.Provider>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## License
143
+
144
+ See [LICENSE.md](../../LICENSE.md).
package/dist/index.d.ts CHANGED
@@ -855,4 +855,15 @@ declare function EdgeInfoCard({ edge }: Props): react_jsx_runtime.JSX.Element;
855
855
 
856
856
  declare function Overview(): react_jsx_runtime.JSX.Element | null;
857
857
 
858
- export { ActionPanel, type ActionPanelState, type ActionPanelStore, type ColorPalette, EMPTY_NORMALIZED, EdgeInfoCard, type EdgeVisual, type GraphDataHook, type GraphDataStore, type GraphDisplayConfig, type GraphExplorationDeps, type GraphExplorationStore, type GraphFilterStore, type GraphFocusAction, type GraphFocusStore, type GraphProperties, type GraphProperty, type GraphRenderer, GraphRendererProvider, type GraphStatistics, type WorkspaceStores as GraphStoreBundle, GraphStoresContext, GraphVisualizer, type GraphVisualizerProps, type LabelDisplayMap, type LiveGraphDataResult, NodeInfoCard, type NodeVisual, type NormalizedGraph, Overview, type RawGraphEdge, type RawGraphNode, type SelectionStore, type StoreFactory, type VisualGraphEdge, type VisualGraphNode, type VisualGraphNormalized, type WorkspaceStores, buildColorPalette, buildColorPaletteFromNodes, createActionPanelStore, createGraphDataStore, createGraphExplorationStore, createGraphFilterStore, createGraphFocusStore, createSelectionStore, enrichEdge, enrichEdges, enrichNode, enrichNodes, extractDisplayLabel, isRawGraphNode, isVisualEdge, isVisualNode, normalizeGraph, toCytoscapeEdge, toCytoscapeNode, toReagraphEdge, toReagraphNode, useActionPanelStore, useCytoscapeData, useGraphFocus, useGraphRenderer, useLiveGraphData, useReagraphData, useWorkspaceGraphData, useWorkspaceGraphExploration, useWorkspaceGraphFilter, useWorkspaceSelection };
858
+ interface GraphA11yListProps {
859
+ /** Render on-screen instead of visually-hidden (still a11y-present). */
860
+ visible?: boolean;
861
+ /** Estimated row height in px (virtualization). Default 28. */
862
+ rowHeight?: number;
863
+ /** Max viewport height in px when visible. Default 320. */
864
+ maxHeight?: number;
865
+ className?: string;
866
+ }
867
+ declare function GraphA11yList({ visible, rowHeight, maxHeight, className, }: GraphA11yListProps): react_jsx_runtime.JSX.Element;
868
+
869
+ export { ActionPanel, type ActionPanelState, type ActionPanelStore, type ColorPalette, EMPTY_NORMALIZED, EdgeInfoCard, type EdgeVisual, GraphA11yList, type GraphA11yListProps, type GraphDataHook, type GraphDataStore, type GraphDisplayConfig, type GraphExplorationDeps, type GraphExplorationStore, type GraphFilterStore, type GraphFocusAction, type GraphFocusStore, type GraphProperties, type GraphProperty, type GraphRenderer, GraphRendererProvider, type GraphStatistics, type WorkspaceStores as GraphStoreBundle, GraphStoresContext, GraphVisualizer, type GraphVisualizerProps, type LabelDisplayMap, type LiveGraphDataResult, NodeInfoCard, type NodeVisual, type NormalizedGraph, Overview, type RawGraphEdge, type RawGraphNode, type SelectionStore, type StoreFactory, type VisualGraphEdge, type VisualGraphNode, type VisualGraphNormalized, type WorkspaceStores, buildColorPalette, buildColorPaletteFromNodes, createActionPanelStore, createGraphDataStore, createGraphExplorationStore, createGraphFilterStore, createGraphFocusStore, createSelectionStore, enrichEdge, enrichEdges, enrichNode, enrichNodes, extractDisplayLabel, isRawGraphNode, isVisualEdge, isVisualNode, normalizeGraph, toCytoscapeEdge, toCytoscapeNode, toReagraphEdge, toReagraphNode, useActionPanelStore, useCytoscapeData, useGraphFocus, useGraphRenderer, useLiveGraphData, useReagraphData, useWorkspaceGraphData, useWorkspaceGraphExploration, useWorkspaceGraphFilter, useWorkspaceSelection };
package/dist/index.js CHANGED
@@ -3616,14 +3616,13 @@ function NodeInfoCard({ node }) {
3616
3616
  return /* @__PURE__ */ jsxs9(
3617
3617
  Card3,
3618
3618
  {
3619
- className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3619
+ className: "w-full rounded-lg overflow-hidden bg-muted/30 border shadow-none p-3",
3620
3620
  style: {
3621
- background: "#f3f4f6",
3622
3621
  borderLeft: `4px solid ${node.visual.color}`
3623
3622
  },
3624
3623
  children: [
3625
- /* @__PURE__ */ jsx10("p", { className: "leading-tight font-semibold mb-0", children: node.label_ }),
3626
- /* @__PURE__ */ jsx10("p", { className: "leading-tight mb-1", children: node.visual.displayName }),
3624
+ /* @__PURE__ */ jsx10("p", { className: "text-sm leading-tight font-medium mb-0", children: node.label_ }),
3625
+ /* @__PURE__ */ jsx10("p", { className: "text-sm leading-tight mb-1", children: node.visual.displayName }),
3627
3626
  /* @__PURE__ */ jsx10(
3628
3627
  "p",
3629
3628
  {
@@ -3636,9 +3635,9 @@ function NodeInfoCard({ node }) {
3636
3635
  /* @__PURE__ */ jsxs9(Collapsible, { open: showProperties, onOpenChange: setShowProperties, children: [
3637
3636
  /* @__PURE__ */ jsx10(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx10("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3638
3637
  /* @__PURE__ */ jsx10(CollapsibleContent, { children: /* @__PURE__ */ jsxs9("ul", { className: "list-none mt-2", children: [
3639
- /* @__PURE__ */ jsx10("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3638
+ /* @__PURE__ */ jsx10("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1", children: "Properties" }),
3640
3639
  Object.entries(node.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs9("li", { className: "mb-2 leading-tight", children: [
3641
- /* @__PURE__ */ jsx10("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3640
+ /* @__PURE__ */ jsx10("p", { className: "text-xs text-muted-foreground uppercase tracking-wide", children: key }),
3642
3641
  /* @__PURE__ */ jsx10(
3643
3642
  "p",
3644
3643
  {
@@ -3673,15 +3672,14 @@ function EdgeInfoCard({ edge }) {
3673
3672
  return /* @__PURE__ */ jsxs10(
3674
3673
  Card4,
3675
3674
  {
3676
- className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3675
+ className: "w-full rounded-lg overflow-hidden bg-muted/30 border shadow-none p-3",
3677
3676
  style: {
3678
- background: "#f3f4f6",
3679
3677
  borderLeft: `4px solid ${edge.visual.color}`
3680
3678
  },
3681
3679
  children: [
3682
- /* @__PURE__ */ jsxs10("p", { className: "leading-tight mb-1", children: [
3683
- /* @__PURE__ */ jsx11(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle" }),
3684
- /* @__PURE__ */ jsx11("strong", { className: "font-semibold align-middle", children: edge.label_ })
3680
+ /* @__PURE__ */ jsxs10("p", { className: "text-sm leading-tight mb-1", children: [
3681
+ /* @__PURE__ */ jsx11(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle h-3.5 w-3.5" }),
3682
+ /* @__PURE__ */ jsx11("strong", { className: "font-medium align-middle", children: edge.label_ })
3685
3683
  ] }),
3686
3684
  edge.id_ && /* @__PURE__ */ jsx11(
3687
3685
  "p",
@@ -3695,9 +3693,9 @@ function EdgeInfoCard({ edge }) {
3695
3693
  edge.properties_ && Object.keys(edge.properties_).length > 0 && /* @__PURE__ */ jsxs10(Collapsible2, { open: showProperties, onOpenChange: setShowProperties, children: [
3696
3694
  /* @__PURE__ */ jsx11(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3697
3695
  /* @__PURE__ */ jsx11(CollapsibleContent2, { children: /* @__PURE__ */ jsxs10("ul", { className: "list-none mt-2", children: [
3698
- /* @__PURE__ */ jsx11("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3696
+ /* @__PURE__ */ jsx11("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1", children: "Properties" }),
3699
3697
  Object.entries(edge.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs10("li", { className: "mb-2 leading-tight", children: [
3700
- /* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3698
+ /* @__PURE__ */ jsx11("p", { className: "text-xs text-muted-foreground uppercase tracking-wide", children: key }),
3701
3699
  /* @__PURE__ */ jsx11(
3702
3700
  "p",
3703
3701
  {
@@ -3756,7 +3754,7 @@ function AllToggleBadge({
3756
3754
  return /* @__PURE__ */ jsxs11(
3757
3755
  Badge,
3758
3756
  {
3759
- className: "cursor-pointer text-sm px-3 py-1",
3757
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3760
3758
  style: {
3761
3759
  textTransform: "none",
3762
3760
  backgroundColor: bgColor,
@@ -3791,7 +3789,7 @@ function NodeLabelBadges({
3791
3789
  return /* @__PURE__ */ jsxs11(
3792
3790
  Badge,
3793
3791
  {
3794
- className: "cursor-pointer text-sm px-3 py-1",
3792
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3795
3793
  style: {
3796
3794
  textTransform: "none",
3797
3795
  backgroundColor: bgColor,
@@ -3829,7 +3827,7 @@ function EdgeLabelBadges({
3829
3827
  return /* @__PURE__ */ jsxs11(
3830
3828
  Badge,
3831
3829
  {
3832
- className: "cursor-pointer text-sm px-3 py-1",
3830
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3833
3831
  style: {
3834
3832
  textTransform: "none",
3835
3833
  backgroundColor: bgColor,
@@ -3972,7 +3970,7 @@ function Overview() {
3972
3970
  if (stats && stats.values != null && stats.values > 0) {
3973
3971
  return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3974
3972
  /* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
3975
- /* @__PURE__ */ jsxs11("p", { className: "text-sm text-slate-700", children: [
3973
+ /* @__PURE__ */ jsxs11("p", { className: "text-sm text-muted-foreground", children: [
3976
3974
  formatNumber(stats.values),
3977
3975
  " Values"
3978
3976
  ] })
@@ -4015,11 +4013,11 @@ function ActionPanel({ actions }) {
4015
4013
  return /* @__PURE__ */ jsxs12(
4016
4014
  Card5,
4017
4015
  {
4018
- className: "h-full min-w-64 flex flex-col bg-gray-50 shadow-xs p-4",
4016
+ className: "h-full min-w-64 flex flex-col bg-card shadow-none p-4",
4019
4017
  style: { maxHeight: "100%", overflow: "hidden" },
4020
4018
  children: [
4021
4019
  actions && /* @__PURE__ */ jsx13("div", { className: "flex-shrink-0 mb-2", children: actions }),
4022
- /* @__PURE__ */ jsx13("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-4", children: [
4020
+ /* @__PURE__ */ jsx13("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-3", children: [
4023
4021
  firstNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: firstNode }),
4024
4022
  hasEdge && selectedEdge && /* @__PURE__ */ jsx13(EdgeInfoCard_default, { edge: selectedEdge }),
4025
4023
  secondNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: secondNode }),
@@ -4030,10 +4028,121 @@ function ActionPanel({ actions }) {
4030
4028
  );
4031
4029
  }
4032
4030
  var ActionPanel_default = ActionPanel;
4031
+
4032
+ // src/components/GraphA11yList/GraphA11yList.tsx
4033
+ import { useMemo as useMemo6, useRef as useRef3 } from "react";
4034
+ import { useVirtualizer } from "@tanstack/react-virtual";
4035
+ import { cn as cn2 } from "@petrarca/sonnet-core";
4036
+ import { jsx as jsx14 } from "react/jsx-runtime";
4037
+ var SR_ONLY = "absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)] [clip-path:inset(50%)] -m-px";
4038
+ function GraphA11yList({
4039
+ visible = false,
4040
+ rowHeight = 28,
4041
+ maxHeight = 320,
4042
+ className
4043
+ }) {
4044
+ const live = useLiveGraphData();
4045
+ const useSelection3 = useWorkspaceSelection();
4046
+ const selectedNodeIds = useSelection3(
4047
+ (s) => s.selectedNodeIds
4048
+ );
4049
+ const selectedEdge = useSelection3((s) => s.selectedEdge);
4050
+ const setSelectedNodeIds = useSelection3(
4051
+ (s) => s.setSelectedNodeIds
4052
+ );
4053
+ const setSelectedEdge = useSelection3(
4054
+ (s) => s.setSelectedEdge
4055
+ );
4056
+ const rows = useMemo6(() => {
4057
+ const nodeRows = live.nodes.map((n) => ({
4058
+ kind: "node",
4059
+ id: String(n.id_),
4060
+ label: n.visual?.displayName || n.label_ || String(n.id_),
4061
+ node: n
4062
+ }));
4063
+ const edgeRows = live.edges.map((e) => ({
4064
+ kind: "edge",
4065
+ id: String(e.id_),
4066
+ label: e.label_ || String(e.id_),
4067
+ edge: e
4068
+ }));
4069
+ return [...nodeRows, ...edgeRows];
4070
+ }, [live.nodes, live.edges]);
4071
+ const parentRef = useRef3(null);
4072
+ const virtualizer = useVirtualizer({
4073
+ count: rows.length,
4074
+ getScrollElement: () => parentRef.current,
4075
+ estimateSize: () => rowHeight,
4076
+ overscan: 8
4077
+ });
4078
+ const selectRow = (row) => {
4079
+ if (row.kind === "node") {
4080
+ setSelectedEdge(null);
4081
+ setSelectedNodeIds([row.id]);
4082
+ } else {
4083
+ setSelectedNodeIds([]);
4084
+ setSelectedEdge(row.edge);
4085
+ }
4086
+ };
4087
+ const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
4088
+ return /* @__PURE__ */ jsx14(
4089
+ "div",
4090
+ {
4091
+ role: "listbox",
4092
+ "aria-label": "Graph elements",
4093
+ className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
4094
+ children: /* @__PURE__ */ jsx14(
4095
+ "div",
4096
+ {
4097
+ ref: parentRef,
4098
+ style: { maxHeight: visible ? maxHeight : void 0, overflow: "auto" },
4099
+ children: /* @__PURE__ */ jsx14(
4100
+ "div",
4101
+ {
4102
+ style: {
4103
+ height: `${virtualizer.getTotalSize()}px`,
4104
+ width: "100%",
4105
+ position: "relative"
4106
+ },
4107
+ children: virtualizer.getVirtualItems().map((vi) => {
4108
+ const row = rows[vi.index];
4109
+ if (!row) return null;
4110
+ const selected = isSelected(row);
4111
+ return /* @__PURE__ */ jsx14(
4112
+ "button",
4113
+ {
4114
+ type: "button",
4115
+ role: "option",
4116
+ "aria-selected": selected,
4117
+ "aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
4118
+ "data-testid": `graph-${row.kind}-${row.id}`,
4119
+ ...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
4120
+ onClick: () => selectRow(row),
4121
+ className: cn2(
4122
+ "absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
4123
+ visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
4124
+ ),
4125
+ style: {
4126
+ height: `${vi.size}px`,
4127
+ transform: `translateY(${vi.start}px)`
4128
+ },
4129
+ children: /* @__PURE__ */ jsx14("span", { className: "truncate", children: row.label })
4130
+ },
4131
+ vi.key
4132
+ );
4133
+ })
4134
+ }
4135
+ )
4136
+ }
4137
+ )
4138
+ }
4139
+ );
4140
+ }
4033
4141
  export {
4034
4142
  ActionPanel_default as ActionPanel,
4035
4143
  EMPTY_NORMALIZED,
4036
4144
  EdgeInfoCard_default as EdgeInfoCard,
4145
+ GraphA11yList,
4037
4146
  GraphRendererProvider,
4038
4147
  GraphStoresContext,
4039
4148
  GraphVisualizer_default as GraphVisualizer,