graph_view.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import * as React from "react";
  2. import * as Mantine from "@mantine/core";
  3. import { GraphvizComponent } from '../graphviz';
  4. import {D3Graph, D3GraphData, D3NodeData, defaultGraphForces} from "../d3graph/d3graph";
  5. import {InfoHoverCardOverlay} from "../info_hover_card";
  6. function esc(str) {
  7. return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  8. }
  9. function esc2(str) {
  10. return encodeURIComponent(str);
  11. }
  12. // I know global variables are bad, but a GraphViz URL can only call a global function:
  13. const graphvizMap = new Map();
  14. (window as any).graphvizClicked = function(nodeId, button) {
  15. const callback = graphvizMap.get(nodeId);
  16. if (callback) callback(button);
  17. }
  18. export type Renderer = 'd3' | 'graphviz';
  19. interface GraphViewProps<N,L> {
  20. graphData: D3GraphData<N,L>;
  21. help: JSX.Element;
  22. graphvizHelp?: JSX.Element;
  23. mouseUpHandler: (event, mouse:{x:number, y:number}, node?: D3NodeData<N>) => void;
  24. defaultRenderer?: Renderer;
  25. children?: any;
  26. }
  27. export function GraphView<N,L>(props: GraphViewProps<N,L>) {
  28. const {graphData, help, mouseUpHandler, defaultRenderer} = props;
  29. const [renderer, setRenderer] = React.useState<Renderer>(defaultRenderer || "d3");
  30. const id = React.useId();
  31. if (mouseUpHandler) {
  32. for (const node of graphData.nodes) {
  33. graphvizMap.set(id+node.id, (button) => mouseUpHandler({button}, {x:0, y:0}, node));
  34. }
  35. }
  36. const dot1 = `digraph {
  37. bgcolor="transparent";
  38. ${graphData.nodes.map(node => `"${esc(node.id)}" [${node.label===""?`shape=circle, width=0.3, height=0.3`:`shape=box`}, label="${esc(node.label)}", fillcolor="${node.color}", style="filled,rounded", ${node.bold?`penwidth=4.0,`:``} URL="javascript:${esc2(`graphvizClicked('${id+node.id}', 2)`)}"]`).join('\n')}
  39. ${graphData.links.map(link => `"${esc(link.source.id || link.source)}" -> "${esc(link.target.id || link.target)}" [label="${esc(link.label)}", fontcolor="${link.color}", color="${link.color}"${link.bidirectional?`, dir=none`:``}]`).join('\n')}
  40. }`;
  41. const dot2 = `digraph {
  42. bgcolor="transparent";
  43. node[shape=record];
  44. ${graphData.nodes.filter(node => !node.id.startsWith("V")).map(node => {
  45. const outgoing = graphData.links.filter(link => (link.source.id || link.source) === node.id);
  46. return `"${esc(node.id)}" [label="{ ${["", ...outgoing.map(o => `${o.label}`)].join('|')} } | { ${[esc(node.label), ...outgoing.map(o => (o.target.id || o.target).startsWith("V") ? `${esc(o.target.label)}` : `<L${o.label}>`)].join('|')} }", fillcolor="${node.color}", style="filled,rounded", ${node.bold?`penwidth=4.0,`:``} URL="javascript:${esc2(`graphvizClicked('${id+node.id}', 2)`)}"]`;
  47. }).join('\n')}
  48. ${graphData.links.filter(link => !link.target.id.startsWith("V")).map(link => `"${esc(link.source.id || link.source)}":"L${link.label}" -> "${esc(link.target.id || link.target)}" [label="${esc(link.label)}", fontcolor="${link.color}", color="${link.color}"${link.bidirectional?`, dir=none`:``}]`).join('\n')}
  49. }`;
  50. // @ts-ignore:
  51. const graphviz = <Mantine.ScrollArea style={{backgroundColor:"#eee"}}>
  52. <GraphvizComponent dot={dot2}
  53. />
  54. </Mantine.ScrollArea>;
  55. return <Mantine.Stack>
  56. <Mantine.Group position="center">
  57. <Mantine.Tooltip withArrow openDelay={300} label={<Mantine.Text><b>D3</b>: Fast, poor layout<br/><b>Graphviz</b>: Slow, nice layout</Mantine.Text>}>
  58. <Mantine.SegmentedControl
  59. data={[
  60. { label: 'D3', value: 'd3' },
  61. { label: 'Graphviz', value: 'graphviz' },
  62. ]}
  63. value={renderer}
  64. onChange={setRenderer}
  65. />
  66. </Mantine.Tooltip>
  67. {props.children}
  68. <Mantine.Button onClick={()=>alert(dot2)}>Get Graphviz dot</Mantine.Button>
  69. </Mantine.Group>
  70. {renderer==="d3"?
  71. <InfoHoverCardOverlay contents={help}>
  72. <D3Graph graph={graphData} forces={defaultGraphForces} mouseUpHandler={mouseUpHandler} />
  73. </InfoHoverCardOverlay>
  74. :
  75. props.graphvizHelp ?
  76. <InfoHoverCardOverlay contents={props.graphvizHelp}>{graphviz}</InfoHoverCardOverlay>
  77. : graphviz
  78. }
  79. </Mantine.Stack>;
  80. }