diff --git a/ivette/src/frama-c/plugins/dive/dive.css b/ivette/src/frama-c/plugins/dive/dive.css
new file mode 100644
index 0000000000000000000000000000000000000000..9cf41c0888e3f71613cd848124a0377f56710ae4
--- /dev/null
+++ b/ivette/src/frama-c/plugins/dive/dive.css
@@ -0,0 +1,55 @@
+.legend {
+    position: absolute;
+    z-index: 4;
+    right: 10px;
+    top: 30px;
+    width: 170px;
+    height: 420px;
+    box-shadow: 3px 3px 2px rgba(0, 0, 0, .4);
+    background-color: var(--background-profound);
+}
+
+.diveTree {
+    overflow: auto;
+}
+
+.diveTree ul {
+    list-style: none;
+    border-left: 1px dotted black;
+    margin-left: 8px;
+    padding-left: 12px;
+}
+
+.diveTree .folder {
+    width: 16px;
+    height: 16px;
+    cursor: pointer;
+    /* border: 1px solid var(--text); */
+}
+
+.diveTree .exploration {
+    width: 20px;
+    height: 20px;
+    vertical-align: middle;
+}
+
+.diveTree .marker > .label {
+    text-overflow: ellipsis;
+    width: 240px;
+    white-space: nowrap;
+    display: inline-block;
+    overflow: hidden;
+    vertical-align: middle;
+}
+
+.diveTree .selected {
+    background: var(--code-select);
+}
+
+.diveTree .hovered {
+    background: var(--code-hover);
+}
+
+.diveTree .marker {
+    cursor: pointer;
+}
diff --git a/ivette/src/frama-c/plugins/dive/graph.tsx b/ivette/src/frama-c/plugins/dive/graph.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..160e228eba10f4257be125ce5e143d71a6d3a71b
--- /dev/null
+++ b/ivette/src/frama-c/plugins/dive/graph.tsx
@@ -0,0 +1,762 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React, { useState, useEffect, useImperativeHandle, useRef } from 'react';
+import _ from 'lodash';
+import { renderToString } from 'react-dom/server';
+import * as Dome from 'dome';
+import * as Ivette from 'ivette';
+import * as Server from 'frama-c/server';
+import * as States from 'frama-c/states';
+
+import * as API from './api';
+
+import Cytoscape from 'cytoscape';
+import CytoscapeComponent from 'react-cytoscapejs';
+import './cytoscape_libs';
+import 'cytoscape-panzoom/cytoscape.js-panzoom.css';
+
+import tippy, * as Tippy from 'tippy.js';
+import 'tippy.js/dist/tippy.css';
+import 'tippy.js/themes/light-border.css';
+import 'tippy.js/animations/shift-away.css';
+import './tippy.css';
+
+import { IconButton } from 'dome/controls/buttons';
+import { Space } from 'dome/frame/toolbars';
+
+import '@fortawesome/fontawesome-free/js/all';
+
+import EvaReady from 'frama-c/plugins/eva/EvaReady';
+import Legend from './legend';
+import style from './style.json';
+import layouts from './layouts.json';
+import './dive.css';
+
+const Debug = new Dome.Debug('dive');
+
+interface Cxtcommand {
+  content: string;
+  select: () => void;
+  enabled: boolean;
+}
+
+interface CytoscapeExtended extends Cytoscape.Core {
+  cxtmenu(options: unknown): void;
+  panzoom(options: unknown): void;
+}
+
+type Identified = { id: string }
+type NodeData = Cytoscape.NodeDataDefinition & Identified
+type EdgeData = { id: string;[key: string]: unknown }
+type NodeOrId = string | Cytoscape.NodeSingular
+
+function callstackToString(callstack: API.callstack): string {
+  return callstack.map((cs) => `${cs.fun}:${cs.instr}`).join('/');
+}
+
+function buildCxtMenu(
+  commands: Cxtcommand[],
+  content?: JSX.Element,
+  action?: () => void,
+): void {
+  commands.push({
+    content: content ? renderToString(content) : '',
+    select: action || (() => { /* Do nothing */ }),
+    enabled: !!action,
+  });
+}
+
+/* double click events for Cytoscape */
+
+function enableDoubleClickEvents(cy: Cytoscape.Core, delay = 350): void {
+  let last: Cytoscape.EventObject | undefined;
+  cy.on('click', (e) => {
+    if (last && last.target === e.target &&
+      e.timeStamp - last.timeStamp < delay) {
+      e.target.trigger('double-click', e);
+    }
+    last = e;
+  });
+}
+
+/* The Dive class handles the selection of nodes according to user actions.
+   To prevent cytoscape to automatically select (and unselect) nodes wrongly,
+   we make some nodes unselectable. We then use the functions below to make
+   the nodes selectable before (un)selecting them. */
+
+function select(node: Cytoscape.NodeSingular): void {
+  node.selectify();
+  node.select();
+}
+
+function unselect(node: Cytoscape.NodeSingular): void {
+  node.selectify();
+  node.unselect();
+}
+
+export type mode = 'explore' | 'overview';
+
+class Dive {
+  headless: boolean;
+  cy: Cytoscape.Core;
+  mode: mode = 'explore';
+  _layout = '';
+  layoutOptions: Cytoscape.LayoutOptions | undefined;
+  currentSelection: string | null = null;
+  onSelect: ((_: States.Location[]) => void) | null = null;
+  selectedLocation: (States.Location | undefined) = undefined;
+
+  constructor(cy: Cytoscape.Core | null = null) {
+    this.cy = cy || Cytoscape();
+    this.headless = this.cy.container() === null;
+    this.cy.elements().remove();
+
+    this.cy.minZoom(1e-1);
+    this.cy.maxZoom(1.0);
+
+    // Remove previous listeners
+    this.cy.off('click');
+    this.cy.off('double-click');
+
+    // Add new listeners
+    enableDoubleClickEvents(this.cy);
+    this.cy.on('click', 'node', (event) => this.clickNode(event.target));
+    this.cy.on('click', 'edge', (event) => this.clickEdge(event.target));
+    this.cy.on('double-click', '$node > node', // compound nodes
+      (event) => this.doubleClickNode(event.target));
+
+    // Set zoom limits
+    const panzoomDefaults = {
+      minZoom: this.cy.minZoom(),
+      maxZoom: this.cy.maxZoom(),
+    };
+    (this.cy as CytoscapeExtended).panzoom(panzoomDefaults);
+
+    // Default layout
+    this.layout = 'dagre';
+
+    // Contextual menu
+    if (!this.headless) {
+      this.cy.scratch('cxtmenu')?.destroy?.(); // Remove previous menu
+      this.cy.scratch('cxtmenu', (this.cy as CytoscapeExtended).cxtmenu({
+        selector: 'node',
+        commands: (node: Cytoscape.NodeSingular) => this.onCxtMenu(node),
+      }));
+    }
+
+    // Node width hack
+    const padding = 10;
+    const min = 50;
+    /* eslint-disable @typescript-eslint/no-explicit-any */
+    if (this.cy.style() && !this.headless) {
+      const canvas = document.querySelector('canvas[data-id="layer2-node"]');
+      if (canvas instanceof HTMLCanvasElement) {
+        const context = canvas.getContext('2d');
+        if (context) {
+          (this.cy.style() as any).selector('node').style('width',
+            (node: any) => {
+              const fStyle = node.pstyle('font-style').strValue;
+              const weight = node.pstyle('font-weight').strValue;
+              const size = node.pstyle('font-size').pfValue;
+              const family = node.pstyle('font-family').strValue;
+              context.font = `${fStyle} ${weight} ${size}px ${family}`;
+              const width = context.measureText(node.data('label')).width;
+              return `${Math.max(min, width + padding)}px`;
+            });
+        }
+      }
+    }
+    /* eslint-enable @typescript-eslint/no-explicit-any */
+  }
+
+  onCxtMenu(node: Cytoscape.NodeSingular): Cxtcommand[] {
+    const data = node.data();
+    const commands = [] as Cxtcommand[];
+    buildCxtMenu(commands,
+      <><div className="fas fa-binoculars fa-2x" />Explore</>,
+      () => { this.explore(node); });
+    if (data.nkind === 'composite')
+      buildCxtMenu(commands,
+        <><div className="fa fa-expand-arrows-alt fa-2x" />Unfold</>);
+    else
+      buildCxtMenu(commands);
+    if (data.backward_explored === 'no')
+      buildCxtMenu(commands,
+        <div><div className="fa fa-eye fa-2x" />Show</div>,
+        () => this.show(node));
+    else
+      buildCxtMenu(commands,
+        <><div className="fa fa-eye-slash fa-2x" />Hide</>,
+        () => this.hide(node));
+    return commands;
+  }
+
+  remove(node: Cytoscape.NodeSingular): void {
+    const parent = node.parent();
+    node.remove();
+    this.cy.$id(`${node.id()}-more`).remove();
+    if (parent.nonempty() && parent.children().empty())
+      this.remove(parent as Cytoscape.NodeSingular); // Recursively remove parents
+  }
+
+  referenceFile(fileName: string): Cytoscape.NodeSingular {
+    const id = `file_${fileName}`;
+    const node = this.cy.$id(id);
+    if (node.nonempty()) {
+      return node;
+    }
+
+    const nodeDefinition = {
+      data: { id, label: fileName },
+      classes: 'file',
+      pannable: true,
+    };
+    // cytoscape.add type declaration is missing the 'pannable' field
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    return this.cy.add(nodeDefinition);
+  }
+
+  referenceCallstack(callstack: API.callstack): Cytoscape.NodeSingular | null {
+    const name = callstackToString(callstack);
+    const elt = callstack.shift();
+
+    if (!elt)
+      return null;
+
+    const id = `callstack_${name}`;
+    const node = this.cy.$id(id);
+    if (node.nonempty()) {
+      return node;
+    }
+
+    const parentNode = this.referenceCallstack(callstack);
+    const nodeDefinition = {
+      data: { id, label: elt.fun, parent: parentNode?.id() },
+      classes: 'function',
+      pannable: true
+    };
+    // cytoscape.add type declaration is missing the 'pannable' field
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    return this.cy.add(nodeDefinition);
+  }
+
+  createTips(node: Cytoscape.NodeSingular): Tippy.Instance[] {
+    const container = this.cy.container();
+    if (!container)
+      return [];
+
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const ref = (node as any).popperRef();
+    const common = {
+      getReferenceClientRect: ref.getBoundingClientRect,
+      interactive: true,
+      animation: 'shift-away',
+      duration: 500,
+      trigger: 'manual',
+      appendTo: document.body,
+    };
+
+    const tips = [];
+
+    if (node.data().values) {
+      tips.push(tippy(container, {
+        ...common,
+        content: node.data().values,
+        placement: 'top',
+        offset: [0, 20],
+        arrow: true,
+      }));
+    }
+
+    if (node.data().type) {
+      tips.push(tippy(container, {
+        ...common,
+        content: node.data().type,
+        placement: 'bottom',
+        offset: [0, 20],
+        theme: 'light-border',
+        arrow: false,
+      }));
+    }
+
+    return tips;
+  }
+
+  addTips(node: Cytoscape.NodeSingular): void {
+    let timeout: NodeJS.Timeout;
+    let tips: Tippy.Instance[] | null = null;
+
+    node.on('mouseover', () => {
+      if (tips === null) {
+        tips = this.createTips(node);
+        tips.forEach((tip) => tip.props.onHidden = (() => tip.destroy()));
+      }
+      clearTimeout(timeout);
+      timeout = setTimeout(() => tips?.forEach((tip) => {
+        tip.show();
+      }), 200);
+    });
+
+    node.on('mouseout', () => {
+      clearTimeout(timeout);
+      timeout = setTimeout(() => {
+        tips?.forEach((tip) => tip.hide());
+        tips = null; // Force rebuilding tips in case they changed
+      }, 0);
+    });
+  }
+
+  updateElement(group: Cytoscape.ElementGroup, data: NodeData | EdgeData):
+    Cytoscape.CollectionReturnValue {
+    let element = this.cy.$id(data.id);
+    if (element.nonempty()) {
+      element.removeData();
+      element.data(data);
+    }
+    else {
+      element = this.cy.add({ group, data });
+      element.addClass("new");
+    }
+    element.removeClass("stale");
+    return element;
+  }
+
+  updateNode(data: NodeData):
+    Cytoscape.NodeSingular {
+      const element = this.updateElement("nodes", data);
+      element.addClass('node');
+      return element;
+  }
+
+  updateEdge(src: NodeOrId, dst: NodeOrId, data: EdgeData):
+    Cytoscape.EdgeSingular {
+    const source = typeof src === "string" ? src : src.id();
+    const target = typeof dst === "string" ? dst : dst.id();
+    data = { ...data, source, target };
+    const element = this.updateElement("edges", data);
+    element.addClass('dependency');
+    return element;
+  }
+
+  updateNodeData(node: API.node): void {
+    const data = { ...node, id: `${node.id}` } as NodeData;
+
+    // Interval range visualization (see cytoscape stops style property)
+    if (typeof node.range === 'number')
+      data.stops = `0% ${node.range}% ${node.range}% 100%`;
+
+    // Build clusters for this node if needed
+    if (node.locality.callstack)
+      data.parent = this.referenceCallstack(node.locality.callstack)?.id();
+    else
+      data.parent = this.referenceFile(node.locality.file).id();
+
+    // Add new node or update existing node
+    const ele = this.updateNode(data);
+    if (ele.hasClass("new"))
+      this.addTips(ele);
+
+    // Add a node for the user to ask for more dependencies
+    const idmore = `${node.id}-more`;
+    if (node.backward_explored === 'partial') {
+      const elemore = this.updateNode({ id: idmore, parent: data.parent });
+      elemore.addClass("more");
+      this.updateEdge(elemore, ele, { id: `e${node.id}-more` });
+    }
+    else {
+      this.cy.$id(idmore).remove();
+    }
+  }
+
+  updateEdgeData(edge: API.dependency): void {
+    const data = { ...edge, id: `e${edge.id}` } as EdgeData;
+    this.updateEdge(`${edge.src}`, `${edge.dst}`, data);
+  }
+
+  updateGraph(data: API.graphData[]): void {
+    this.cy.startBatch();
+    this.cy.$('.node, .dependency').addClass('stale');
+
+    // Update vertices
+    for (const d of data) {
+      if ('nkind' in d.element) // Node
+        this.updateNodeData(d.element);
+    }
+
+    // Edges must be updated after vertices since their sources and destination
+    // must have been created beforhand
+    for (const d of data) {
+      if ('dkind' in d.element) // Dependency
+        this.updateEdgeData(d.element);
+    }
+
+    // Remove nodes that are not present anymore
+    this.cy.$('.stale').forEach(n => this.remove(n));
+
+    this.cy.endBatch();
+
+    this.recomputeLayout(this.cy.$('node.new'));
+  }
+
+  get layout(): string {
+    return this._layout;
+  }
+
+  set layout(layout: string) {
+    if (layout === this._layout)
+      return;
+    let extendedOptions = {};
+    if (layout in layouts)
+      extendedOptions = (layouts as { [key: string]: object })[layout];
+    this._layout = layout;
+    this.layoutOptions = {
+      name: layout,
+      fit: true,
+      animate: true,
+      randomize: false, /* Keep previous positions if layout supports it */
+      ...extendedOptions,
+    };
+
+    this.recomputeLayout();
+  }
+
+  recomputeLayout(newNodes?: Cytoscape.Collection): void {
+    if (this.layoutOptions && this.cy.container() &&
+      (newNodes === undefined || !newNodes.empty())) {
+      this.cy.layout({
+        animationEasing: 'ease-in-out-quad',
+        /* Do not move new nodes */
+        animateFilter: (node: Cytoscape.Singular) => newNodes === undefined ||
+          !newNodes.contains(node),
+        stop: () => {
+          this.cy.$('.new').addClass('old').removeClass('new');
+        },
+        ...this.layoutOptions,
+      } as unknown as Cytoscape.LayoutOptions).run();
+    }
+  }
+
+  async exec<In>(
+    request: Server.ExecRequest<In, number | undefined | null>,
+    param: In): Promise<Cytoscape.NodeSingular | undefined> {
+    try {
+      if (Server.isRunning()) {
+        await this.setMode();
+        const r = await Server.send(request, param);
+        if (r) {
+          return undefined;
+        }
+      }
+    }
+    catch (err) {
+      Debug.error(err);
+    }
+
+    return undefined;
+  }
+
+  static async setWindow(window: API.explorationWindow): Promise<void> {
+    if (Server.isRunning())
+      await Server.send(API.window, window);
+  }
+
+  async setMode(): Promise<void> {
+    switch (this.mode) {
+      case 'explore':
+        await Dive.setWindow({
+          perception: { backward: 3, forward: 1 },
+          horizon: { backward: 3, forward: 1 },
+        });
+        break;
+      case 'overview':
+        await Dive.setWindow({
+          perception: { backward: 4, forward: 1 },
+          horizon: { backward: undefined, forward: undefined },
+        });
+        break;
+      default: /* This is useless and impossible if the program is correctly
+                  typed, but the linter wants it */
+    }
+  }
+
+  clear(): void {
+    this.cy.elements().remove();
+    this.exec(API.clear, null);
+  }
+
+  async add(marker: string): Promise<void> {
+    const node = await this.exec(API.add, marker);
+    if (node)
+      this.updateNodeSelection(node);
+  }
+
+  async explore(node: Cytoscape.NodeSingular): Promise<void> {
+    const id = parseInt(node.id(), 10);
+    if (id)
+      await this.exec(API.explore, id);
+  }
+
+  show(node: Cytoscape.NodeSingular): void {
+    const id = parseInt(node.id(), 10);
+    if (id)
+      this.exec(API.show, id);
+  }
+
+  hide(node: Cytoscape.NodeSingular): void {
+    const id = parseInt(node.id(), 10);
+    if (id)
+      this.exec(API.hide, id);
+  }
+
+  async clickNode(node: Cytoscape.NodeSingular): Promise<void> {
+    this.updateNodeSelection(node);
+    await this.explore(node);
+
+    const writes = node.data()?.writes;
+    if (writes && writes.length)
+      this.onSelect?.(writes);
+
+    /* Cytoscape automatically selects the node clicked, and unselects all other
+       nodes and edges. As we want some incoming edges to remain selected, we
+       make the node unselectable, preventing cytoscape to select it. */
+    node.unselectify();
+  }
+
+  async clickEdge(edge: Cytoscape.EdgeSingular): Promise<void> {
+    // Unselect everything
+    this.cy.$(':selected').forEach(unselect);
+    this.cy.$('.multiple-selection').removeClass('multiple-selection');
+    this.cy.$('.selection').removeClass('selection');
+
+    // Update Ivette selection
+    const origins = edge.data()?.origins;
+    if (origins && origins.length)
+      this.onSelect?.(origins);
+  }
+
+  doubleClickNode(node: Cytoscape.NodeSingular): void {
+    this.cy.animate({ fit: { eles: node, padding: 10 } });
+  }
+
+  selectLocation(
+    location: States.Location | undefined,
+    doExplore: boolean): void {
+    if (!location) {
+      // Reset whole graph if no location is selected.
+      this.clear();
+    } else if (location !== this.selectedLocation) {
+      this.selectedLocation = location;
+      const selectNode = this.cy.$('node:selected');
+      const writes = selectNode?.data()?.writes;
+      if (doExplore && location.marker && !_.some(writes, location)) {
+        this.add(location.marker);
+      }
+      else {
+        this.updateNodeSelection(selectNode);
+      }
+    }
+  }
+
+  updateNodeSelection(node: Cytoscape.NodeSingular): void {
+    const hasOrigin = (ele: Cytoscape.NodeSingular): boolean => (
+      _.some(ele.data().origins, this.selectedLocation)
+    );
+    this.cy.$(':selected').forEach(unselect);
+    this.cy.$('.multiple-selection').removeClass('multiple-selection');
+    this.cy.$('.selection').removeClass('selection');
+    select(node);
+    const edges = node.incomers('edge');
+    const relevantEdges = edges.filter(hasOrigin);
+    edges.addClass('multiple-selection');
+    relevantEdges.addClass('selection');
+  }
+}
+
+
+type GraphViewProps = {
+  addSelection: boolean;
+  grabbable: boolean;
+  layout: string;
+  selectionMode: string;
+}
+
+type GraphViewRef = {
+  clear: () => void;
+}
+
+const GraphView = React.forwardRef<GraphViewRef | undefined, GraphViewProps>(
+  (props: GraphViewProps, ref) => {
+  const { addSelection, grabbable, layout, selectionMode } = props;
+
+  const [dive, setDive] = useState(() => new Dive());
+  const [selection, updateSelection] = States.useSelection();
+  const graph = States.useSyncArrayData(API.graph);
+
+  function setCy(cy: Cytoscape.Core): void {
+    if (cy !== dive.cy)
+      setDive(new Dive(cy));
+  }
+
+  useImperativeHandle(ref, () => ({ clear: () => dive.clear() }));
+
+  useEffect(() => {
+    setDive(new Dive(dive.cy)); // On hot reload, setup a new instance
+  }, [Dive]); // eslint-disable-line react-hooks/exhaustive-deps
+
+  useEffect(() => {
+    dive.layout = layout;
+  }, [dive, layout]);
+
+  useEffect(() => {
+    dive.updateGraph(graph);
+  }, [dive, graph]);
+
+  // Follow mode
+  useEffect(() => {
+    dive.mode = selectionMode === 'follow' ? 'explore' : 'overview';
+  }, [dive, selectionMode]);
+
+  useEffect(() => {
+    /* When clicking on a node, select its writes locations as a multiple
+       selection. If these locations were already selected, select the next
+       location in the multiple selection. */
+    dive.onSelect = (locations) => {
+      if (_.isEqual(locations, selection?.multiple?.allSelections))
+        updateSelection('MULTIPLE_CYCLE');
+      else {
+        const name = "Dive graph";
+        updateSelection({ name, locations, index: 0 });
+      }
+    };
+
+    // Updates the graph according to the selected marker.
+    dive.selectLocation(selection?.current, addSelection);
+  }, [dive, addSelection, selection, updateSelection]);
+
+  return (
+    <CytoscapeComponent
+      stylesheet={style}
+      cy={setCy}
+      autoungrabify={!grabbable}
+      style={{ width: '100%', height: '100%' }}
+    />);
+});
+
+GraphView.displayName = "GraphView";
+
+
+export default function GraphComponent(): JSX.Element {
+  const graph = useRef<GraphViewRef>();
+  const [addSelection, flipAddSelection] =
+    Dome.useFlipSettings('dive.addSelection', true);
+  const [grabbable, flipGrabbable] =
+    Dome.useFlipSettings('dive.grabbable', true);
+  const [selectionMode, setSelectionMode] =
+    Dome.useStringSettings('dive.selectionMode', 'follow');
+  const [layout, setLayout] =
+    Dome.useStringSettings('dive.layout', 'dagre');
+  const [showLegend, flipShowLegend] =
+    Dome.useFlipSettings('dive.legend', true);
+
+  // Selection mode
+  const selectMode = (id?: string) => void (id && setSelectionMode(id));
+  const modes = [
+    { id: 'follow', label: 'Follow selection' },
+    { id: 'add', label: 'Add selection to the graph' },
+  ];
+  const checkMode =
+    (item: { id: string; label: string }): Dome.PopupMenuItem => (
+      { checked: item.id === selectionMode, ...item }
+    );
+  const modeMenu = (): void => {
+    Dome.popupMenu(modes.map(checkMode), selectMode);
+  };
+
+  // Layout selection
+  const selectLayout = (layout?: string) => void (layout && setLayout(layout));
+  const layoutsNames = ['cose-bilkent', 'dagre', 'cola', 'klay'];
+  const layoutItem = (id: string): Dome.PopupMenuItem =>
+    ({ id, label: id, checked: (id === layout) });
+  const layoutMenu = () =>
+    void Dome.popupMenu(layoutsNames.map(layoutItem), selectLayout);
+
+  // Component
+  return (
+    <>
+      <Ivette.TitleBar>
+        <IconButton
+          icon="PIN"
+          onClick={flipAddSelection}
+          kind={addSelection ? 'positive' : 'negative'}
+          title={addSelection ?
+            'Do not add selected AST elements into the graph' :
+            'Add selected AST elements into the graph'}
+        />
+        <IconButton
+          icon="LOCK"
+          onClick={flipGrabbable}
+          kind={grabbable ? 'positive' : 'negative'}
+          title={grabbable ?
+            'Disallow nodes to be moved' :
+            'Allow nodes to be moved'}
+        />
+        <IconButton
+          icon="SETTINGS"
+          onClick={modeMenu}
+          title="Choose the selection mode"
+        />
+        <IconButton
+          icon="DISPLAY"
+          onClick={layoutMenu}
+          title="Choose the graph layout"
+        />
+        <Space />
+        <IconButton
+          icon="HELP"
+          onClick={flipShowLegend}
+          kind={showLegend ? 'positive' : 'default'}
+          title={showLegend ?
+            'Hide legend' :
+            'Show legend'}
+        />
+        <Space />
+        <IconButton
+          icon="TRASH"
+          onClick={() => graph.current?.clear()}
+          title="Clear the graph"
+        />
+      </Ivette.TitleBar>
+      <EvaReady>
+        <GraphView
+          addSelection={addSelection}
+          grabbable={grabbable}
+          layout={layout}
+          selectionMode={selectionMode}
+          ref={graph}/>
+        { showLegend ? <Legend /> : null }
+      </EvaReady>
+    </>
+  );
+}
diff --git a/ivette/src/frama-c/plugins/dive/index.tsx b/ivette/src/frama-c/plugins/dive/index.tsx
index 77dd2a9f9aa1316c467648d7493450d287645c96..26df8fda493b559391bcbf21b8a98cf6fec5fb6d 100644
--- a/ivette/src/frama-c/plugins/dive/index.tsx
+++ b/ivette/src/frama-c/plugins/dive/index.tsx
@@ -20,715 +20,38 @@
 /*                                                                          */
 /* ************************************************************************ */
 
-import React, { useState, useEffect, useImperativeHandle, useRef } from 'react';
-import _ from 'lodash';
-import { renderToString } from 'react-dom/server';
-import * as Dome from 'dome';
+import React from 'react';
 import * as Ivette from 'ivette';
-import * as Server from 'frama-c/server';
-import * as States from 'frama-c/states';
 
-import * as API from './api';
+import GraphComponent from './graph';
+import TreeComponent from './tree';
 
-import Cytoscape from 'cytoscape';
-import CytoscapeComponent from 'react-cytoscapejs';
-import './cytoscape_libs';
-import 'cytoscape-panzoom/cytoscape.js-panzoom.css';
-
-import tippy, * as Tippy from 'tippy.js';
-import 'tippy.js/dist/tippy.css';
-import 'tippy.js/themes/light-border.css';
-import 'tippy.js/animations/shift-away.css';
-import './tippy.css';
-
-import { IconButton } from 'dome/controls/buttons';
-import { Space } from 'dome/frame/toolbars';
-
-import '@fortawesome/fontawesome-free/js/all';
-
-import EvaReady from 'frama-c/plugins/eva/EvaReady';
-import style from './style.json';
-import layouts from './layouts.json';
-
-const Debug = new Dome.Debug('dive');
-
-interface Cxtcommand {
-  content: string;
-  select: () => void;
-  enabled: boolean;
-}
-
-interface CytoscapeExtended extends Cytoscape.Core {
-  cxtmenu(options: unknown): void;
-  panzoom(options: unknown): void;
-}
-
-type Identified = { id: string }
-type NodeData = Cytoscape.NodeDataDefinition & Identified
-type EdgeData = { id: string;[key: string]: unknown }
-type NodeOrId = string | Cytoscape.NodeSingular
-
-function callstackToString(callstack: API.callstack): string {
-  return callstack.map((cs) => `${cs.fun}:${cs.instr}`).join('/');
-}
-
-function buildCxtMenu(
-  commands: Cxtcommand[],
-  content?: JSX.Element,
-  action?: () => void,
-): void {
-  commands.push({
-    content: content ? renderToString(content) : '',
-    select: action || (() => { /* Do nothing */ }),
-    enabled: !!action,
-  });
-}
-
-/* double click events for Cytoscape */
-
-function enableDoubleClickEvents(cy: Cytoscape.Core, delay = 350): void {
-  let last: Cytoscape.EventObject | undefined;
-  cy.on('click', (e) => {
-    if (last && last.target === e.target &&
-      e.timeStamp - last.timeStamp < delay) {
-      e.target.trigger('double-click', e);
-    }
-    last = e;
+Ivette.registerComponent({
+    id: 'frama-c.plugins.dive.graph',
+    label: 'Dive Dataflow Graph',
+    group: 'frama-c.plugins',
+    rank: 2,
+    title:
+      'Data dependency graph according to an Eva analysis.',
+    children: <GraphComponent />,
   });
-}
-
-/* The Dive class handles the selection of nodes according to user actions.
-   To prevent cytoscape to automatically select (and unselect) nodes wrongly,
-   we make some nodes unselectable. We then use the functions below to make
-   the nodes selectable before (un)selecting them. */
-
-function select(node: Cytoscape.NodeSingular): void {
-  node.selectify();
-  node.select();
-}
-
-function unselect(node: Cytoscape.NodeSingular): void {
-  node.selectify();
-  node.unselect();
-}
-
-export type mode = 'explore' | 'overview';
-
-class Dive {
-  headless: boolean;
-  cy: Cytoscape.Core;
-  mode: mode = 'explore';
-  _layout = '';
-  layoutOptions: Cytoscape.LayoutOptions | undefined;
-  currentSelection: string | null = null;
-  onSelect: ((_: States.Location[]) => void) | null = null;
-  selectedLocation: (States.Location | undefined) = undefined;
-
-  constructor(cy: Cytoscape.Core | null = null) {
-    this.cy = cy || Cytoscape();
-    this.headless = this.cy.container() === null;
-    this.cy.elements().remove();
-
-    this.cy.minZoom(1e-1);
-    this.cy.maxZoom(1.0);
-
-    // Remove previous listeners
-    this.cy.off('click');
-    this.cy.off('double-click');
-
-    // Add new listeners
-    enableDoubleClickEvents(this.cy);
-    this.cy.on('click', 'node', (event) => this.clickNode(event.target));
-    this.cy.on('double-click', '$node > node', // compound nodes
-      (event) => this.doubleClickNode(event.target));
-
-    // Set zoom limits
-    const panzoomDefaults = {
-      minZoom: this.cy.minZoom(),
-      maxZoom: this.cy.maxZoom(),
-    };
-    (this.cy as CytoscapeExtended).panzoom(panzoomDefaults);
-
-    // Default layout
-    this.layout = 'dagre';
-
-    // Contextual menu
-    if (!this.headless) {
-      this.cy.scratch('cxtmenu')?.destroy?.(); // Remove previous menu
-      this.cy.scratch('cxtmenu', (this.cy as CytoscapeExtended).cxtmenu({
-        selector: 'node',
-        commands: (node: Cytoscape.NodeSingular) => this.onCxtMenu(node),
-      }));
-    }
-
-    // Node width hack
-    const padding = 10;
-    const min = 50;
-    /* eslint-disable @typescript-eslint/no-explicit-any */
-    if (this.cy.style() && !this.headless) {
-      const canvas = document.querySelector('canvas[data-id="layer2-node"]');
-      if (canvas instanceof HTMLCanvasElement) {
-        const context = canvas.getContext('2d');
-        if (context) {
-          (this.cy.style() as any).selector('node').style('width',
-            (node: any) => {
-              const fStyle = node.pstyle('font-style').strValue;
-              const weight = node.pstyle('font-weight').strValue;
-              const size = node.pstyle('font-size').pfValue;
-              const family = node.pstyle('font-family').strValue;
-              context.font = `${fStyle} ${weight} ${size}px ${family}`;
-              const width = context.measureText(node.data('label')).width;
-              return `${Math.max(min, width + padding)}px`;
-            });
-        }
-      }
-    }
-    /* eslint-enable @typescript-eslint/no-explicit-any */
-  }
-
-  onCxtMenu(node: Cytoscape.NodeSingular): Cxtcommand[] {
-    const data = node.data();
-    const commands = [] as Cxtcommand[];
-    buildCxtMenu(commands,
-      <><div className="fas fa-binoculars fa-2x" />Explore</>,
-      () => { this.explore(node); });
-    if (data.nkind === 'composite')
-      buildCxtMenu(commands,
-        <><div className="fa fa-expand-arrows-alt fa-2x" />Unfold</>);
-    else
-      buildCxtMenu(commands);
-    if (data.backward_explored === 'no')
-      buildCxtMenu(commands,
-        <div><div className="fa fa-eye fa-2x" />Show</div>,
-        () => this.show(node));
-    else
-      buildCxtMenu(commands,
-        <><div className="fa fa-eye-slash fa-2x" />Hide</>,
-        () => this.hide(node));
-    return commands;
-  }
-
-  remove(node: Cytoscape.NodeSingular): void {
-    const parent = node.parent();
-    node.remove();
-    this.cy.$id(`${node.id()}-more`).remove();
-    if (parent.nonempty() && parent.children().empty())
-      this.remove(parent as Cytoscape.NodeSingular); // Recursively remove parents
-  }
-
-  referenceFile(fileName: string): Cytoscape.NodeSingular {
-    const id = `file_${fileName}`;
-    const node = this.cy.$id(id);
-    if (node.nonempty()) {
-      return node;
-    }
-
-    return this.cy.add({ data: { id, label: fileName }, classes: 'file' });
-  }
-
-  referenceCallstack(callstack: API.callstack): Cytoscape.NodeSingular | null {
-    const name = callstackToString(callstack);
-    const elt = callstack.shift();
-
-    if (!elt)
-      return null;
-
-    const id = `callstack_${name}`;
-    const node = this.cy.$id(id);
-    if (node.nonempty()) {
-      return node;
-    }
-
-    const parentNode = this.referenceCallstack(callstack);
-    const parent = parentNode?.id();
-    const label = elt.fun;
-    return this.cy.add({ data: { id, label, parent }, classes: 'function' });
-  }
-
-  createTips(node: Cytoscape.NodeSingular): Tippy.Instance[] {
-    const container = this.cy.container();
-    if (!container)
-      return [];
-
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const ref = (node as any).popperRef();
-    const common = {
-      getReferenceClientRect: ref.getBoundingClientRect,
-      interactive: true,
-      animation: 'shift-away',
-      duration: 500,
-      trigger: 'manual',
-      appendTo: document.body,
-    };
-
-    const tips = [];
-
-    if (node.data().values) {
-      tips.push(tippy(container, {
-        ...common,
-        content: node.data().values,
-        placement: 'top',
-        offset: [0, 20],
-        arrow: true,
-      }));
-    }
-
-    if (node.data().type) {
-      tips.push(tippy(container, {
-        ...common,
-        content: node.data().type,
-        placement: 'bottom',
-        offset: [0, 20],
-        theme: 'light-border',
-        arrow: false,
-      }));
-    }
-
-    return tips;
-  }
-
-  addTips(node: Cytoscape.NodeSingular): void {
-    let timeout: NodeJS.Timeout;
-    let tips: Tippy.Instance[] | null = null;
-
-    node.on('mouseover', () => {
-      if (tips === null) {
-        tips = this.createTips(node);
-        tips.forEach((tip) => tip.props.onHidden = (() => tip.destroy()));
-      }
-      clearTimeout(timeout);
-      timeout = setTimeout(() => tips?.forEach((tip) => {
-        tip.show();
-      }), 200);
-    });
-
-    node.on('mouseout', () => {
-      clearTimeout(timeout);
-      timeout = setTimeout(() => {
-        tips?.forEach((tip) => tip.hide());
-        tips = null; // Force rebuilding tips in case they changed
-      }, 0);
-    });
-  }
-
-  updateElement(group: Cytoscape.ElementGroup, data: NodeData | EdgeData):
-    Cytoscape.CollectionReturnValue {
-    let element = this.cy.$id(data.id);
-    if (element.nonempty()) {
-      element.removeData();
-      element.data(data);
-    }
-    else {
-      element = this.cy.add({ group, data });
-      element.addClass("new");
-    }
-    element.removeClass("stale");
-    return element;
-  }
-
-  updateNode(data: NodeData):
-    Cytoscape.NodeSingular {
-      const element = this.updateElement("nodes", data);
-      element.addClass('node');
-      return element;
-  }
-
-  updateEdge(src: NodeOrId, dst: NodeOrId, data: EdgeData):
-    Cytoscape.EdgeSingular {
-    const source = typeof src === "string" ? src : src.id();
-    const target = typeof dst === "string" ? dst : dst.id();
-    data = { ...data, source, target };
-    const element = this.updateElement("edges", data);
-    element.addClass('dependency');
-    return element;
-  }
-
-  updateNodeData(node: API.node): void {
-    const data = { ...node, id: `${node.id}` } as NodeData;
-
-    // Interval range visualization (see cytoscape stops style property)
-    if (typeof node.range === 'number')
-      data.stops = `0% ${node.range}% ${node.range}% 100%`;
-
-    // Build clusters for this node if needed
-    if (node.locality.callstack)
-      data.parent = this.referenceCallstack(node.locality.callstack)?.id();
-    else
-      data.parent = this.referenceFile(node.locality.file).id();
-
-    // Add new node or update existing node
-    const ele = this.updateNode(data);
-    if (ele.hasClass("new"))
-      this.addTips(ele);
-
-    // Add a node for the user to ask for more dependencies
-    const idmore = `${node.id}-more`;
-    if (node.backward_explored === 'partial') {
-      const elemore = this.updateNode({ id: idmore, parent: data.parent });
-      elemore.addClass("more");
-      this.updateEdge(elemore, ele, { id: `e${node.id}-more` });
-    }
-    else {
-      this.cy.$id(idmore).remove();
-    }
-  }
-
-  updateEdgeData(edge: API.dependency): void {
-    const data = { ...edge, id: `e${edge.id}` } as EdgeData;
-    this.updateEdge(`${edge.src}`, `${edge.dst}`, data);
-  }
-
-  updateGraph(data: API.graphData[]): void {
-    this.cy.startBatch();
-    this.cy.$('.node, .dependency').addClass('stale');
-
-    // Update vertices
-    for (const d of data) {
-      if ('nkind' in d.element) // Node
-        this.updateNodeData(d.element);
-    }
-
-    // Edges must be updated after vertices since their sources and destination
-    // must have been created beforhand
-    for (const d of data) {
-      if ('dkind' in d.element) // Dependency
-        this.updateEdgeData(d.element);
-    }
-
-    // Remove nodes that are not present anymore
-    this.cy.$('.stale').forEach(n => this.remove(n));
-
-    this.cy.endBatch();
-
-    this.recomputeLayout(this.cy.$('node.new'));
-  }
-
-  get layout(): string {
-    return this._layout;
-  }
-
-  set layout(layout: string) {
-    if (layout === this._layout)
-      return;
-    let extendedOptions = {};
-    if (layout in layouts)
-      extendedOptions = (layouts as { [key: string]: object })[layout];
-    this._layout = layout;
-    this.layoutOptions = {
-      name: layout,
-      fit: true,
-      animate: true,
-      randomize: false, /* Keep previous positions if layout supports it */
-      ...extendedOptions,
-    };
-
-    this.recomputeLayout();
-  }
-
-  recomputeLayout(newNodes?: Cytoscape.Collection): void {
-    if (this.layoutOptions && this.cy.container() &&
-      (newNodes === undefined || !newNodes.empty())) {
-      this.cy.layout({
-        animationEasing: 'ease-in-out-quad',
-        /* Do not move new nodes */
-        animateFilter: (node: Cytoscape.Singular) => newNodes === undefined ||
-          !newNodes.contains(node),
-        stop: () => {
-          this.cy.$('.new').addClass('old').removeClass('new');
-        },
-        ...this.layoutOptions,
-      } as unknown as Cytoscape.LayoutOptions).run();
-    }
-  }
-
-  async exec<In>(
-    request: Server.ExecRequest<In, number | undefined | null>,
-    param: In): Promise<Cytoscape.NodeSingular | undefined> {
-    try {
-      if (Server.isRunning()) {
-        await this.setMode();
-        const r = await Server.send(request, param);
-        if (r) {
-          return undefined;
-        }
-      }
-    }
-    catch (err) {
-      Debug.error(err);
-    }
-
-    return undefined;
-  }
-
-  static async setWindow(window: API.explorationWindow): Promise<void> {
-    if (Server.isRunning())
-      await Server.send(API.window, window);
-  }
-
-  async setMode(): Promise<void> {
-    switch (this.mode) {
-      case 'explore':
-        await Dive.setWindow({
-          perception: { backward: 3, forward: 1 },
-          horizon: { backward: 3, forward: 1 },
-        });
-        break;
-      case 'overview':
-        await Dive.setWindow({
-          perception: { backward: 4, forward: 1 },
-          horizon: { backward: undefined, forward: undefined },
-        });
-        break;
-      default: /* This is useless and impossible if the program is correctly
-                  typed, but the linter wants it */
-    }
-  }
-
-  clear(): void {
-    this.cy.elements().remove();
-    this.exec(API.clear, null);
-  }
-
-  async add(marker: string): Promise<void> {
-    const node = await this.exec(API.add, marker);
-    if (node)
-      this.updateNodeSelection(node);
-  }
-
-  async explore(node: Cytoscape.NodeSingular): Promise<void> {
-    const id = parseInt(node.id(), 10);
-    if (id)
-      await this.exec(API.explore, id);
-  }
-
-  show(node: Cytoscape.NodeSingular): void {
-    const id = parseInt(node.id(), 10);
-    if (id)
-      this.exec(API.show, id);
-  }
-
-  hide(node: Cytoscape.NodeSingular): void {
-    const id = parseInt(node.id(), 10);
-    if (id)
-      this.exec(API.hide, id);
-  }
-
-  async clickNode(node: Cytoscape.NodeSingular): Promise<void> {
-    this.updateNodeSelection(node);
-    await this.explore(node);
-
-    const writes = node.data()?.writes;
-    if (writes && writes.length)
-      this.onSelect?.(writes);
-
-    /* Cytoscape automatically selects the node clicked, and unselects all other
-       nodes and edges. As we want some incoming edges to remain selected, we
-       make the node unselectable, preventing cytoscape to select it. */
-    node.unselectify();
-  }
-
-  doubleClickNode(node: Cytoscape.NodeSingular): void {
-    this.cy.animate({ fit: { eles: node, padding: 10 } });
-  }
-
-  selectLocation(
-    location: States.Location | undefined,
-    doExplore: boolean): void {
-    if (!location) {
-      // Reset whole graph if no location is selected.
-      this.clear();
-    } else if (location !== this.selectedLocation) {
-      this.selectedLocation = location;
-      const selectNode = this.cy.$('node:selected');
-      const writes = selectNode?.data()?.writes;
-      if (doExplore && location.marker && !_.some(writes, location)) {
-        this.add(location.marker);
-      }
-      else {
-        this.updateNodeSelection(selectNode);
-      }
-    }
-  }
-
-  updateNodeSelection(node: Cytoscape.NodeSingular): void {
-    const hasOrigin = (ele: Cytoscape.NodeSingular): boolean => (
-      _.some(ele.data().origins, this.selectedLocation)
-    );
-    this.cy.$(':selected').forEach(unselect);
-    this.cy.$('.multiple-selection').removeClass('multiple-selection');
-    this.cy.$('.selection').removeClass('selection');
-    select(node);
-    const edges = node.incomers('edge');
-    const relevantEdges = edges.filter(hasOrigin);
-    edges.addClass('multiple-selection');
-    relevantEdges.addClass('selection');
-  }
-}
-
-
-type GraphViewProps = {
-  lock: boolean;
-  layout: string;
-  selectionMode: string;
-}
-
-type GraphViewRef = {
-  clear: () => void;
-}
-
-const GraphView = React.forwardRef<GraphViewRef | undefined, GraphViewProps>(
-  (props: GraphViewProps, ref) => {
-  const { lock, layout, selectionMode } = props;
-
-  const [dive, setDive] = useState(() => new Dive());
-  const [selection, updateSelection] = States.useSelection();
-  const graph = States.useSyncArrayData(API.graph);
-
-  function setCy(cy: Cytoscape.Core): void {
-    if (cy !== dive.cy)
-      setDive(new Dive(cy));
-  }
-
-  useImperativeHandle(ref, () => ({ clear: () => dive.clear() }));
-
-  useEffect(() => {
-    setDive(new Dive(dive.cy)); // On hot reload, setup a new instance
-  }, [Dive]); // eslint-disable-line react-hooks/exhaustive-deps
-
-  useEffect(() => {
-    dive.layout = layout;
-  }, [dive, layout]);
-
-  useEffect(() => {
-    dive.updateGraph(graph);
-  }, [dive, graph]);
-
-  // Follow mode
-  useEffect(() => {
-    dive.mode = selectionMode === 'follow' ? 'explore' : 'overview';
-  }, [dive, selectionMode]);
-
-  useEffect(() => {
-    /* When clicking on a node, select its writes locations as a multiple
-       selection. If these locations were already selected, select the next
-       location in the multiple selection. */
-    dive.onSelect = (locations) => {
-      if (_.isEqual(locations, selection?.multiple?.allSelections))
-        updateSelection('MULTIPLE_CYCLE');
-      else
-        updateSelection({ locations, index: 0 });
-    };
-
-    // Updates the graph according to the selected marker.
-    dive.selectLocation(selection?.current, !lock);
-  }, [dive, lock, selection, updateSelection]);
-
-  return (
-    <CytoscapeComponent
-      stylesheet={style}
-      cy={setCy}
-      style={{ width: '100%', height: '100%' }}
-    />);
-});
-
-GraphView.displayName = "GraphView";
-
-
-function DiveView(): JSX.Element {
-  const graph = useRef<GraphViewRef>();
-  const [lock, flipLock] =
-    Dome.useFlipSettings('dive.lock');
-  const [selectionMode, setSelectionMode] =
-    Dome.useStringSettings('dive.selectionMode', 'follow');
-  const [layout, setLayout] =
-    Dome.useStringSettings('dive.layout', 'dagre');
-
-  // Selection mode
-  const selectMode = (id?: string) => void (id && setSelectionMode(id));
-  const modes = [
-    { id: 'follow', label: 'Follow selection' },
-    { id: 'add', label: 'Add selection to the graph' },
-  ];
-  const checkMode =
-    (item: { id: string; label: string }): Dome.PopupMenuItem => (
-      { checked: item.id === selectionMode, ...item }
-    );
-  const modeMenu = (): void => {
-    Dome.popupMenu(modes.map(checkMode), selectMode);
-  };
-
-  // Layout selection
-  const selectLayout = (layout?: string) => void (layout && setLayout(layout));
-  const layoutsNames = ['cose-bilkent', 'dagre', 'cola', 'klay'];
-  const layoutItem = (id: string): Dome.PopupMenuItem =>
-    ({ id, label: id, checked: (id === layout) });
-  const layoutMenu = () =>
-    void Dome.popupMenu(layoutsNames.map(layoutItem), selectLayout);
-
-  // Component
-  return (
-    <>
-      <Ivette.TitleBar>
-        <IconButton
-          icon="LOCK"
-          onClick={flipLock}
-          kind={lock ? 'negative' : 'positive'}
-          title={lock ?
-            'Unlock the graph: update the graph with the selection' :
-            'Lock the graph: do not update the graph with the selection'}
-        />
-        <IconButton
-          icon="SETTINGS"
-          onClick={modeMenu}
-          title="Choose the selection mode"
-        />
-        <IconButton
-          icon="DISPLAY"
-          onClick={layoutMenu}
-          title="Choose the graph layout"
-        />
-        <Space />
-        <IconButton
-          icon="TRASH"
-          onClick={() => graph.current?.clear()}
-          title="Clear the graph"
-        />
-      </Ivette.TitleBar>
-      <EvaReady>
-        <GraphView
-          lock={lock}
-          layout={layout}
-          selectionMode={selectionMode}
-          ref={graph}/>
-      </EvaReady>
-    </>
-  );
-
-}
-
-// --------------------------------------------------------------------------
-// --- Export Component
-// --------------------------------------------------------------------------
 
 Ivette.registerComponent({
-  id: 'frama-c.plugins.dive',
-  label: 'Dive Dataflow',
-  group: 'frama-c.plugins',
-  rank: 2,
-  title:
-    'Data dependency graph according to an Eva analysis.\nNodes color ' +
-    'represents the precision of the values inferred by Eva.',
-  children: <DiveView />,
-});
+    id: 'frama-c.plugins.dive.tree',
+    label: 'Dive Dataflow Tree',
+    group: 'frama-c.plugins',
+    rank: 3,
+    title:
+        'Data dependency tree according to an Eva analysis.',
+    children: <TreeComponent />,
+  });
 
 Ivette.registerView({
-  id: 'dive',
-  label: 'Dive Dataflow',
-  rank: 5,
-  layout: [
-    ['frama-c.astview', 'frama-c.plugins.dive'],
-    ['frama-c.properties', 'frama-c.locations'],
-  ],
-});
-
-// --------------------------------------------------------------------------
+    id: 'dive',
+    label: 'Dive Dataflow',
+    rank: 5,
+    layout: [
+        ['frama-c.astview', 'frama-c.plugins.dive.graph'],
+        ['frama-c.properties', 'frama-c.locations'],
+    ],
+  });
diff --git a/ivette/src/frama-c/plugins/dive/legend.tsx b/ivette/src/frama-c/plugins/dive/legend.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff8918adeb7633a407c426d0dd4fc956850dee6e
--- /dev/null
+++ b/ivette/src/frama-c/plugins/dive/legend.tsx
@@ -0,0 +1,187 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React from 'react';
+import _ from 'lodash';
+
+import CytoscapeComponent from 'react-cytoscapejs';
+import stylesheet from './style.json';
+
+const elements = [
+  {
+    data: {
+      id: "shape",
+      label: "Node shape: memory"
+    },
+    classes: ["group"],
+  },
+  {
+    data: {
+      parent: "shape",
+      label: "constant",
+      nkind: 'const'
+    },
+    position: { x: 0, y: 0 }
+  },
+  {
+    data: {
+      parent: "shape",
+      label: "scalar type"
+    },
+    position: { x: 0, y: 40 }
+  },
+  {
+    data: {
+      parent: "shape",
+      label: "aggregate type",
+      nkind: 'composite'
+    },
+    position: { x: 0, y: 80 }
+  },
+  {
+    data: {
+      parent: "shape",
+      label: "set of addresses",
+      nkind: 'scattered'
+    },
+    position: { x: 0, y: 120 }
+  },
+  {
+    data: {
+      parent: "shape",
+      label: "analysis alarm",
+      nkind: 'alarm'
+    },
+    position: { x: 0, y: 160 }
+  },
+  {
+    data: {
+      id: "color",
+      label: "Node color: value cardinality"
+    },
+    classes: ["group"]
+  },
+  {
+    data: {
+      parent: "color",
+      label: "unique value",
+      range: 'singleton'
+    },
+    position: { x: 0, y: 230 }
+  },
+  {
+    data: {
+      parent: "color",
+      label: "small range of values",
+      stops: '0% 20% 20% 100%'
+    },
+    position: { x: 0, y: 270 }
+  },
+  {
+    data: {
+      parent: "color",
+      label: "large range of values",
+      stops: '0% 80% 80% 100%'
+    },
+    position: { x: 0, y: 310 }
+  },
+  {
+    data: {
+      parent: "color",
+      label: "extreme range of values",
+      range: 'wide'
+    },
+    position: { x: 0, y: 350 }
+  },
+  {
+    data: {
+      id: "outline",
+      label: "Node outline color: taint analysis",
+    },
+    classes: ["group"]
+  },
+  {
+    data: {
+      parent: "outline",
+      label: "directly tainted",
+      taint: 'direct'
+    },
+    position: { x: 0, y: 420 }
+  },
+  {
+    data: {
+      parent: "outline",
+      label: "indirectly tainted",
+      taint: 'indirect'
+    },
+    position: { x: 0, y: 480 }
+  },
+];
+
+const layout = {
+  name: 'preset',
+  fit: true,
+  padding: 0,
+};
+
+const completeStylecheet = [
+  ...stylesheet,
+  {
+    "selector": "node",
+    "style": {
+      "width": '140',
+      "height": '20',
+      "font-size": "14px",
+    }
+  },
+  {
+    "selector": ".group",
+    "style": {
+      "shape": "rectangle",
+      "background-opacity": 0.0,
+      "text-valign": "top",
+      "text-halign": "center",
+      "padding": "4px",
+      "border-width": 0,
+    }
+  },
+  {
+    "selector": "#outline",
+    "style": { "padding": "10px", }
+  },
+];
+
+function Legend() : JSX.Element {
+  return (
+    <CytoscapeComponent
+      elements={elements}
+      layout={layout}
+      className="legend"
+      stylesheet={completeStylecheet}
+      userPanningEnabled={false} /* No panning */
+      userZoomingEnabled={false} /* No zoom */
+      autounselectify={true} /* No node selection */
+      autoungrabify={true} /* No node grab */
+    />);
+}
+
+export default Legend;
diff --git a/ivette/src/frama-c/plugins/dive/style.json b/ivette/src/frama-c/plugins/dive/style.json
index 79a9907c1e06fd4d0f2841f0d5a4e2fd8e17f16a..331ea3a2bc361ac636420453cacbe4fd6b8863e0 100644
--- a/ivette/src/frama-c/plugins/dive/style.json
+++ b/ivette/src/frama-c/plugins/dive/style.json
@@ -168,17 +168,15 @@
   {
     "selector": "node[nkind='composite']",
     "style": {
-      "ghost": "yes",
-      "ghost-offset-x": "6px",
-      "ghost-offset-y": "6px",
-      "ghost-opacity": "0.7"
+      "border-style": "double",
+      "border-width": "5px"
     }
   },
   {
     "selector": "node[taint='direct']",
     "style": {
       "underlay-color": "#882288",
-      "underlay-padding": "14px",
+      "underlay-padding": "12px",
       "underlay-opacity": 0.4
     }
   },
@@ -186,7 +184,7 @@
     "selector": "node[taint='indirect']",
     "style": {
       "underlay-color": "#73BBBB",
-      "underlay-padding": "14px",
+      "underlay-padding": "12px",
       "underlay-opacity": 0.8
     }
   },
diff --git a/ivette/src/frama-c/plugins/dive/tree.tsx b/ivette/src/frama-c/plugins/dive/tree.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..78be7a54daa2d52fbac8e53b2f69abdfe046d1e9
--- /dev/null
+++ b/ivette/src/frama-c/plugins/dive/tree.tsx
@@ -0,0 +1,232 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React, { useEffect } from 'react';
+
+import { classes } from 'dome/misc/utils';
+import { IconButton } from 'dome/controls/buttons';
+import * as Ivette from 'ivette';
+
+import * as Server from 'frama-c/server';
+import * as States from 'frama-c/states';
+
+import EvaReady from 'frama-c/plugins/eva/EvaReady';
+import * as API from './api';
+import type { marker, location } from 'frama-c/kernel/api/ast';
+import gearsIcon from '../eva/images/gears.svg';
+
+import './dive.css';
+
+const window = {
+  perception: { backward: 3, forward: 0 },
+  horizon: { backward: undefined, forward: undefined },
+};
+
+async function exec<I, O>(rq: Server.ExecRequest<I, O>, i: I):
+    Promise<O | undefined> {
+  if (Server.isRunning()) {
+    await Server.send(API.window, window);
+    return await Server.send(rq, i);
+  }
+
+  return undefined;
+}
+
+async function requestLocation(location: States.Location):
+    Promise<number | undefined> {
+  return await exec(API.add, location.marker);
+}
+
+async function explore(node: API.node): Promise<null | undefined> {
+  return await exec(API.explore, node.id);
+}
+
+function isDependency(el: API.element): el is API.dependency {
+  return 'dst' in el;
+}
+
+function Folder(props: {unfolded: boolean, onclick?: () => void}): JSX.Element {
+  return <IconButton
+      icon={props.unfolded ? 'MINUS' : 'PLUS'}
+      title="Fold / Unfold the dependencies"
+      className="folder"
+      onClick={props.onclick}
+    />;
+}
+
+function Exploring(): JSX.Element {
+  return <><img src={gearsIcon} className="exploration" />Exploring...</>;
+}
+
+function Marker(props: {location: location}): JSX.Element {
+  const location = props.location;
+  const marker = location.marker;
+  const { descr } = States.useMarker(marker);
+  const [selection, updateSelection] = States.useSelection();
+  const [hovering, updateHovered] = States.useHovered();
+  const classeName = classes(
+    'marker',
+    selection.current?.marker === marker && 'selected',
+    hovering?.marker === marker && 'hovered'
+  );
+
+  return <span
+    className={classeName}
+    onClick={() => updateSelection({ location: location })}
+    onMouseEnter={() => updateHovered(location)}
+    onMouseLeave={() => updateHovered(undefined)}>
+      {descr}
+  </span>;
+}
+
+
+type WithKey<P> = P & {key: string | number | null}
+
+type WithItemChildren<P> =
+  P & {
+    children?: WithKey<React.ReactElement> | WithKey<React.ReactElement>[]
+  }
+
+type TreeNodeProps = {
+  label: React.ReactNode,
+  className?: string,
+  unfolded?: boolean,
+  onunfold?: () => void,
+}
+
+function TreeNode(props: WithItemChildren<TreeNodeProps>): JSX.Element {
+  const [unfolded, setUnfolded] = React.useState(props.unfolded === true);
+  const children =
+    props.children ?
+      (Array.isArray(props.children) ? props.children : [props.children]) :
+      [];
+
+  const toggle = (): void => {
+    unfolded === false && props.onunfold && props.onunfold();
+    setUnfolded(!unfolded);
+  };
+
+  return <div className={props.className}>
+    <span className="label">
+      {children.length > 0 ?
+        <Folder unfolded={unfolded} onclick={toggle} /> : <></>
+      }
+      {props.label}
+    </span>
+    {unfolded ?
+      <ul>
+        {children.map((element) => <li key={element.key}>{element}</li>)}
+      </ul> :
+        null
+    }
+    </div>;
+}
+
+function MarkerNode(props: WithItemChildren<{location: location}>):
+    JSX.Element {
+  return <TreeNode
+    unfolded={true}
+    label={<Marker location={props.location} />}
+    className="marker">
+      {props.children}
+  </TreeNode>;
+}
+
+function GraphNode(props: {nodeId: number, unfolded?: boolean}): JSX.Element {
+  const graphData = States.useSyncArrayElt(API.graph, `n${props.nodeId}`);
+  const graph = States.useSyncArrayData(API.graph);
+
+  if (graphData && 'label' in graphData.element) {
+    const node = graphData.element;
+    const deps = graph
+      .map((data) => data.element)
+      .filter(isDependency)
+      .filter((d) => d.dst === node.id);
+
+    // Transform dependencies into a map 'statement' -> 'memory location read
+    // at this statement'
+    const map = new Map<marker, [location, Set<API.nodeId>]>();
+    deps.forEach((d) => {
+      d.origins.forEach((loc) => {
+        let entry = map.get(loc.marker);
+        if (entry === undefined) {
+          entry = [loc, new Set()];
+          map.set(loc.marker, entry);
+        }
+        entry[1].add(d.src);
+     });
+    });
+
+    return <TreeNode
+        unfolded={props.unfolded}
+        onunfold={() => explore(node)}
+        label={node.label}>
+          {node.backward_explored ?
+            Array.from(map.entries()).map(([m, [loc, sources]]) =>
+              <MarkerNode location={loc} key={m}>
+                {Array.from(sources.values()).map((src) =>
+                  <GraphNode nodeId={src} key={src} />
+                ) }
+              </MarkerNode>
+            ) :
+            <Exploring key={null} />
+          }
+      </TreeNode>;
+  }
+
+  return <>Error while building the tree</>;
+}
+
+export default function TreeComponent(): JSX.Element {
+  const [root, setRoot] = React.useState<number | null>(null);
+  const [selection] = States.useSelection();
+
+  useEffect(() => {
+    const update = async (): Promise<void> => {
+      if (selection.current) {
+        const node =  await requestLocation(selection.current);
+        root === null && node !== null && node !== undefined && setRoot(node);
+      }
+    };
+    update();
+  }, [selection, root]);
+
+  return <>
+    <Ivette.TitleBar>
+      <IconButton
+            icon="TRASH"
+            onClick={() => setRoot(null)}
+            title="Clear the graph"
+          />
+    </Ivette.TitleBar>
+    <EvaReady>
+      {
+        root === null ?
+          <>Select an expression to investigate</>
+        :
+          <div className="diveTree">
+            <GraphNode nodeId={root} unfolded={true} />
+          </div>
+      }
+    </EvaReady>
+  </>;
+}