diff --git a/src/ui/src/components/chart/graph-utils.ts b/src/ui/src/components/chart/graph-utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..53d57db735f75ae74cf431f1cf2f45515ace1a5d
--- /dev/null
+++ b/src/ui/src/components/chart/graph-utils.ts
@@ -0,0 +1,32 @@
+import * as d3 from 'd3';
+
+export function overflows(parent: DOMRect, child: DOMRect): boolean {
+  return child.height > parent.height || child.width > parent.width;
+}
+
+export function scaleToFit(parent: DOMRect, child: DOMRect): number {
+  if (!overflows(parent, child)) {
+    // Don't scale the child if it doesn't overflow.
+    return 1;
+  }
+  return Math.min(parent.height / child.height, parent.width / child.width);
+}
+
+export function fitDirection(parent: DOMRect, child: DOMRect): 'x' | 'y' {
+  return parent.width / child.width > parent.height / child.height ? 'y' : 'x';
+}
+
+export function centerFit(parent: DOMRect, child: DOMRect): d3.ZoomTransform {
+  if (!overflows(parent, child)) {
+    return d3.zoomIdentity.translate(parent.width / 2 - child.width / 2, parent.height / 2 - child.height / 2);
+  }
+
+  const scale = scaleToFit(parent, child);
+  const direction = fitDirection(parent, child);
+  const translate = {
+    x: direction === 'x' ? 0 : parent.width / 2 - child.width * scale / 2,
+    y: direction === 'y' ? 0 : parent.height / 2 - child.height * scale / 2,
+  };
+  console.log({ translate, parent, child });
+  return d3.zoomIdentity.translate(translate.x, translate.y).scale(scale);
+}
diff --git a/src/ui/src/components/chart/graph.scss b/src/ui/src/components/chart/graph.scss
index 2759f4af4ef6e4361443669012d5d0aaf9ad9846..e5d64b62c8365b4b5cb7ff710203f927a335e5e3 100644
--- a/src/ui/src/components/chart/graph.scss
+++ b/src/ui/src/components/chart/graph.scss
@@ -1,14 +1,14 @@
 @import "../../theme.scss";
 
-#pixie-graph {
-  overflow: auto;
+.pixie-graph-root {
+  height: 100%;
+  width: 100%;
   display: flex;
-  padding: 16px;
+  flex-direction: row;
 }
 
-#pixie-graph svg {
-  margin: auto;
-  overflow: visible;
+.pixie-graph-svg {
+  flex: 1;
 }
 
 svg .cluster rect {
diff --git a/src/ui/src/components/chart/graph.tsx b/src/ui/src/components/chart/graph.tsx
index 5d6801e0611753a55a9a1d2560a77c94305f1ea6..a1941e73da24def9bc01e30585c0fc7e459061ed 100644
--- a/src/ui/src/components/chart/graph.tsx
+++ b/src/ui/src/components/chart/graph.tsx
@@ -3,9 +3,10 @@ import './graph.scss';
 import { WidgetDisplay } from 'containers/live/vis';
 import * as d3 from 'd3';
 import * as dagreD3 from 'dagre-d3';
-import * as dot from 'graphlib-dot';
+import * as graphlibDot from 'graphlib-dot';
 import * as React from 'react';
-import { AutoSizer } from 'react-virtualized';
+
+import { centerFit } from './graph-utils';
 
 interface AdjacencyList {
   toColumn: string;
@@ -38,54 +39,64 @@ interface GraphProps {
   fromCol?: string;
 }
 
-export class Graph extends React.Component<GraphProps, {}> {
-  err: string;
-  constructor(props) {
-    super(props);
-    this.err = '';
-  }
+export const Graph = (props: GraphProps) => {
+  const { dot, toCol, fromCol, data } = props;
+  const [err, setErr] = React.useState('');
+  const svgRef = React.useRef<SVGSVGElement>(null);
+  const ref = React.useRef({
+    svgGroup: null,
+    renderer: null,
+    zoom: null,
+    baseSvg: null,
+  });
 
-  dataToGraph = () => {
+  const dataToGraph = () => {
     const graph = new dagreD3.graphlib.Graph()
       .setGraph({})
       .setDefaultEdgeLabel(() => ({}));
 
-    this.props.data.forEach((rb) => {
+    data.forEach((rb) => {
       // Filter out empty columns, because this will cause dagre to crash.
-      if (this.props.toCol !== '' && this.props.fromCol !== '') {
-        graph.setNode(rb[this.props.toCol], { label: rb[this.props.toCol] });
-        graph.setNode(rb[this.props.fromCol], { label: rb[this.props.fromCol] });
-        graph.setEdge(rb[this.props.fromCol], rb[this.props.toCol]);
+      if (toCol !== '' && fromCol !== '') {
+        graph.setNode(rb[toCol], { label: rb[toCol] });
+        graph.setNode(rb[fromCol], { label: rb[fromCol] });
+        graph.setEdge(rb[fromCol], rb[toCol]);
       }
     });
 
     return graph;
-  }
+  };
 
-  componentDidMount = () => {
-    const graph = this.props.dot ? dot.read(this.props.dot) : this.dataToGraph();
-    const render = new dagreD3.render();
-    const svg = d3.select<SVGGraphicsElement, any>('#pixie-graph svg');
-    const svgGroup = svg.append('g');
+  // Do this once to setup the component.
+  React.useLayoutEffect(() => {
+    const baseSvg = d3.select<SVGGraphicsElement, any>(svgRef.current);
+    const svgGroup = baseSvg.append('g');
+    const zoom = d3.zoom().on('zoom', () => svgGroup.attr('transform', d3.event.transform));
+    const renderer = new dagreD3.render();
+    baseSvg.call(zoom);
+    ref.current = { svgGroup, baseSvg, zoom, renderer };
+  }, []);
+
+  React.useEffect(() => {
+    const graph = dot ? graphlibDot.read(dot) : dataToGraph();
+    const { baseSvg, svgGroup, renderer, zoom } = ref.current;
     try {
-      render(svgGroup, graph);
+      renderer(svgGroup, graph);
+      setErr('');
     } catch (error) {
-      this.err = 'Error rendering graph. Graph may display incorrectly.';
+      setErr('Error rendering graph. Graph may display incorrectly.');
     }
-    const bbox = svg.node().getBBox();
-    svg.style('width', bbox.width)
-      .style('height', bbox.height);
-  }
+    // Center the graph
+    const rootBbox = svgRef.current.getBoundingClientRect();
+    const groupBbox = svgGroup.node().getBBox();
 
-  render() {
-    return (
-      <AutoSizer>{({ height, width }) => {
-        return (<div id='pixie-graph' style={{ height, width }}>
-          <svg />
-          {this.err !== '' ? <p>{this.err}</p> : null}
-        </div>);
-      }}
-      </AutoSizer>
-    );
-  }
-}
+    baseSvg.call(zoom.transform, centerFit(rootBbox, groupBbox));
+  }, [dot, data]);
+
+  return (
+    <div className='pixie-graph-root'>
+      <svg className='pixie-graph-svg' ref={svgRef} />
+      {err !== '' ? <p>{err}</p> : null}
+    </div>
+  );
+};