diff --git a/Makefile b/Makefile
index abe001d1968d1b5f6f6d7fc925d3ef641eb3bc8d..9af6317689253cc9dbda1c8f09bc45b47fbbd540 100644
--- a/Makefile
+++ b/Makefile
@@ -784,7 +784,7 @@ PLUGIN_NAME:=Metrics
 PLUGIN_DISTRIBUTED:=yes
 PLUGIN_DIR:=src/plugins/metrics
 PLUGIN_CMO:= metrics_parameters css_html metrics_base metrics_acsl \
-	     metrics_cabs metrics_cilast metrics_coverage \
+	     metrics_cabs metrics_cilast metrics_coverage metrics_pivot \
 	     register
 PLUGIN_GUI_CMO:= metrics_gui register_gui
 PLUGIN_DEPENDENCIES:=Eva
diff --git a/configure.in b/configure.in
index 54d56f2f332a02e82021d094e5031ac2a9be11d3..faaf96d4eca57a97b791b6d0c7ceeb19c930740c 100644
--- a/configure.in
+++ b/configure.in
@@ -783,6 +783,7 @@ plugin_require(inout,callgraph)
 #########
 
 check_plugin(metrics,src/plugins/metrics,[support for metrics analysis],yes)
+plugin_require(metrics,server)
 plugin_require(metrics,eva)
 plugin_use(metrics,gui)
 
diff --git a/headers/header_spec.txt b/headers/header_spec.txt
index 0e4ca8c9bb2c2a828b8de118b3d1431e3a5fde9d..0d0c951d48b04dbdbd428528415e6281ccd9258c 100644
--- a/headers/header_spec.txt
+++ b/headers/header_spec.txt
@@ -1009,6 +1009,8 @@ src/plugins/metrics/metrics_gui.ml: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/metrics_gui.mli: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/metrics_parameters.ml: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/metrics_parameters.mli: CEA_LGPL_OR_PROPRIETARY
+src/plugins/metrics/metrics_pivot.ml: CEA_LGPL_OR_PROPRIETARY
+src/plugins/metrics/metrics_pivot.mli: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/register.ml: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/register.mli: CEA_LGPL_OR_PROPRIETARY
 src/plugins/metrics/register_gui.ml: CEA_LGPL_OR_PROPRIETARY
diff --git a/ivette/Makefile.distrib b/ivette/Makefile.distrib
index b6773e8d65d31dead54e2e1d96a427e3edee939d..02f7a024dc5194201d7e70a2f91dccd5494d7ee7 100644
--- a/ivette/Makefile.distrib
+++ b/ivette/Makefile.distrib
@@ -147,6 +147,7 @@ DISTRIB_FILES += ivette/src/frama-c/api/generated/kernel/services/index.ts
 DISTRIB_FILES += ivette/src/frama-c/api/generated/plugins/dive/index.ts
 DISTRIB_FILES += ivette/src/frama-c/api/generated/plugins/eva/general/index.ts
 DISTRIB_FILES += ivette/src/frama-c/api/generated/plugins/eva/values/index.ts
+DISTRIB_FILES += ivette/src/frama-c/api/generated/plugins/pivot/general/index.ts
 DISTRIB_FILES += ivette/src/frama-c/api/generated/plugins/studia/studia/index.ts
 DISTRIB_FILES += ivette/src/frama-c/api/generator.ml
 DISTRIB_FILES += ivette/src/frama-c/client.ts
@@ -159,6 +160,7 @@ DISTRIB_FILES += ivette/src/frama-c/kernel/Globals.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/History.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/Locations.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/Messages.tsx
+DISTRIB_FILES += ivette/src/frama-c/kernel/PivotTable.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/Properties.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/SourceCode.tsx
 DISTRIB_FILES += ivette/src/frama-c/kernel/Status.tsx
@@ -189,6 +191,7 @@ DISTRIB_FILES += ivette/src/frama-c/plugins/eva/style.css
 DISTRIB_FILES += ivette/src/frama-c/plugins/eva/summary.css
 DISTRIB_FILES += ivette/src/frama-c/plugins/eva/valueinfos.tsx
 DISTRIB_FILES += ivette/src/frama-c/plugins/eva/valuetable.tsx
+DISTRIB_FILES += ivette/src/frama-c/react-pivottable.d.ts
 DISTRIB_FILES += ivette/src/frama-c/server.ts
 DISTRIB_FILES += ivette/src/frama-c/states.ts
 DISTRIB_FILES += ivette/src/frama-c/utils.ts
diff --git a/ivette/headers/header_spec.txt b/ivette/headers/header_spec.txt
index c89734b28035fc538a00d3b922ea4237fefe1aea..b41ae593231fac45596038d840bcc09628aefa98 100644
--- a/ivette/headers/header_spec.txt
+++ b/ivette/headers/header_spec.txt
@@ -147,6 +147,7 @@ src/frama-c/api/generated/kernel/services/index.ts: CEA_LGPL
 src/frama-c/api/generated/plugins/dive/index.ts: CEA_LGPL
 src/frama-c/api/generated/plugins/eva/general/index.ts: CEA_LGPL
 src/frama-c/api/generated/plugins/eva/values/index.ts: CEA_LGPL
+src/frama-c/api/generated/plugins/pivot/general/index.ts: CEA_LGPL
 src/frama-c/api/generated/plugins/studia/studia/index.ts: CEA_LGPL
 src/frama-c/api/generator.ml: .ignore
 src/frama-c/client.ts: CEA_LGPL
@@ -159,6 +160,7 @@ src/frama-c/kernel/Globals.tsx: CEA_LGPL
 src/frama-c/kernel/History.tsx: CEA_LGPL
 src/frama-c/kernel/Locations.tsx: CEA_LGPL
 src/frama-c/kernel/Messages.tsx: CEA_LGPL
+src/frama-c/kernel/PivotTable.tsx: CEA_LGPL
 src/frama-c/kernel/Properties.tsx: CEA_LGPL
 src/frama-c/kernel/SourceCode.tsx: CEA_LGPL
 src/frama-c/kernel/Status.tsx: CEA_LGPL
@@ -189,6 +191,7 @@ src/frama-c/plugins/eva/style.css: .ignore
 src/frama-c/plugins/eva/summary.css: .ignore
 src/frama-c/plugins/eva/valueinfos.tsx: CEA_LGPL
 src/frama-c/plugins/eva/valuetable.tsx: CEA_LGPL
+src/frama-c/react-pivottable.d.ts: CEA_LGPL
 src/frama-c/server.ts: CEA_LGPL
 src/frama-c/states.ts: CEA_LGPL
 src/frama-c/utils.ts: CEA_LGPL
diff --git a/ivette/package.json b/ivette/package.json
index 52655b7d59c4bec9f1c4207977f1c0733d76dad1..33b452fc4f9efd8d3f9bd8f481787e3c1b8b66af 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -75,6 +75,7 @@
     "react-dom": "^16",
     "react-draggable": "^4.4.4",
     "react-fast-compare": "^3.2.0",
+    "react-pivottable": "^0.11.0",
     "react-virtualized": "^9.22.3",
     "react-window": "",
     "source-map-support": "^0.5.21",
diff --git a/ivette/src/frama-c/api/generated/plugins/pivot/general/index.ts b/ivette/src/frama-c/api/generated/plugins/pivot/general/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16efe72359dd0a0c80f98eab75fc0e2ffae21baf
--- /dev/null
+++ b/ivette/src/frama-c/api/generated/plugins/pivot/general/index.ts
@@ -0,0 +1,89 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2021                                                */
+/*     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).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* --- Generated Frama-C Server API --- */
+
+/**
+   Pivot Table Services
+   @packageDocumentation
+   @module frama-c/api/plugins/pivot/general
+*/
+
+//@ts-ignore
+import * as Json from 'dome/data/json';
+//@ts-ignore
+import * as Compare from 'dome/data/compare';
+//@ts-ignore
+import * as Server from 'frama-c/server';
+//@ts-ignore
+import * as State from 'frama-c/states';
+
+
+/** State of the pivot table source data. */
+export type tableStateType = string[][];
+
+/** Safe decoder for `tableStateType` */
+export const jTableStateTypeSafe: Json.Safe<tableStateType> =
+  Json.jArray(Json.jArray(Json.jFail(Json.jString,'String expected')));
+
+/** Loose decoder for `tableStateType` */
+export const jTableStateType: Json.Loose<tableStateType> =
+  Json.jTry(jTableStateTypeSafe);
+
+/** Natural order for `tableStateType` */
+export const byTableStateType: Compare.Order<tableStateType> =
+  Compare.array(Compare.array(Compare.string));
+
+/** Signal for state [`pivotState`](#pivotstate)  */
+export const signalPivotState: Server.Signal = {
+  name: 'plugins.pivot.general.signalPivotState',
+};
+
+const getPivotState_internal: Server.GetRequest<null,tableStateType> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.pivot.general.getPivotState',
+  input:  Json.jNull,
+  output: jTableStateType,
+  signals: [],
+};
+/** Getter for state [`pivotState`](#pivotstate)  */
+export const getPivotState: Server.GetRequest<null,tableStateType>= getPivotState_internal;
+
+const pivotState_internal: State.Value<tableStateType> = {
+  name: 'plugins.pivot.general.pivotState',
+  signal: signalPivotState,
+  getter: getPivotState,
+};
+/** State of the pivot table source data. */
+export const pivotState: State.Value<tableStateType> = pivotState_internal;
+
+const compute_internal: Server.ExecRequest<null,null> = {
+  kind: Server.RqKind.EXEC,
+  name:   'plugins.pivot.general.compute',
+  input:  Json.jNull,
+  output: Json.jNull,
+  signals: [],
+};
+/** Computes the pivot table. */
+export const compute: Server.ExecRequest<null,null>= compute_internal;
+
+/* ------------------------------------- */
diff --git a/ivette/src/frama-c/index.tsx b/ivette/src/frama-c/index.tsx
index ecdd4f2fd82278d3e7e1e09a2aa92189dc0518c9..cbd203691f69193bd4f1eb1e0dc2002be378c02c 100644
--- a/ivette/src/frama-c/index.tsx
+++ b/ivette/src/frama-c/index.tsx
@@ -33,6 +33,7 @@ import Status from 'frama-c/kernel/Status';
 import ASTview from 'frama-c/kernel/ASTview';
 import ASTinfo from 'frama-c/kernel/ASTinfo';
 import SourceCode from 'frama-c/kernel/SourceCode';
+import PivotTable from 'frama-c/kernel/PivotTable';
 import Locations from 'frama-c/kernel/Locations';
 import Properties from 'frama-c/kernel/Properties';
 import Messages from 'frama-c/kernel/Messages';
@@ -90,6 +91,12 @@ Ivette.registerGroup({
     title: 'Messages emitted by Frama-C',
     children: <Messages />,
   });
+  Ivette.registerComponent({
+    id: 'frama-c.pivottable',
+    label: 'Pivot Table',
+    title: 'Pivot Table',
+    children: <PivotTable />,
+  });
 });
 
 /* --------------------------------------------------------------------------*/
@@ -115,4 +122,13 @@ Ivette.registerView({
   ],
 });
 
+Ivette.registerView({
+  id: 'pivot-table',
+  rank: 2,
+  label: 'Pivot Table',
+  layout: [
+    ['frama-c.pivottable'],
+  ],
+});
+
 /* --------------------------------------------------------------------------*/
diff --git a/ivette/src/frama-c/kernel/PivotTable.tsx b/ivette/src/frama-c/kernel/PivotTable.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..28d3865a677283ca852d45949ea1313e97810195
--- /dev/null
+++ b/ivette/src/frama-c/kernel/PivotTable.tsx
@@ -0,0 +1,129 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2021                                                */
+/*     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).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+// --------------------------------------------------------------------------
+// --- Pivot Table
+// --------------------------------------------------------------------------
+
+import React from 'react';
+import { TitleBar } from 'ivette';
+import * as Server from 'frama-c/server';
+import { Button } from 'dome/controls/buttons';
+import { LED } from 'dome/controls/displays';
+import { Scroll } from 'dome/layout/boxes';
+import * as Status from 'frama-c/kernel/Status';
+import * as States from 'frama-c/states';
+import * as PivotState from 'frama-c/api/plugins/pivot/general';
+import PivotTableUI from 'react-pivottable/PivotTableUI';
+import 'react-pivottable/pivottable.css';
+
+// --------------------------------------------------------------------------
+// --- Pivot Table for Properties
+// --------------------------------------------------------------------------
+
+interface PivotTableProps {
+  data: string[][];
+}
+
+export function Pivot(props: PivotTableProps) : JSX.Element {
+  const [state, setState] = React.useState({});
+  return (
+    <PivotTableUI
+      data={props.data}
+      onChange={setState}
+      {...state}
+    />
+  );
+}
+
+function PivotTable (rawData: PivotState.tableStateType): JSX.Element {
+  const data = new Array(rawData.length > 0 ? rawData.length - 1 : 0);
+  if (rawData.length > 0) {
+    const headers = rawData[0];
+    for (let i = 1; i < rawData.length; i++) {
+      const src = rawData[i];
+      data[i - 1] = {};
+      for (let j = 0; j < headers.length; j++) {
+        data[i - 1][headers[j]] = src[j];
+      }
+    }
+  }
+  return (<Pivot data={data} />);
+}
+
+function PivotTableBuild () : JSX.Element {
+  const rawData = States.useSyncValue(PivotState.pivotState);
+  const [computing, setComputing] = React.useState(false);
+  const [error, setError] = React.useState('');
+  async function handleError (err: string) : Promise<void> {
+    const msg =
+      `The pivot table could not be built: an error has occured (${err}).`;
+    setError(msg);
+    Status.setMessage({ text: msg, kind: 'error' });
+  }
+  async function compute () : Promise<void> {
+    setComputing(true);
+    setError('');
+    Server.send(PivotState.compute, [])
+      .then(() => setComputing(false))
+      .catch((err) => { setComputing(false); handleError(err); });
+  }
+  if (rawData && rawData.length > 0) {
+    return (PivotTable(rawData));
+  }
+  if (computing) {
+    return (
+      <div className="pivot-centered">
+        <div>
+          <LED status="active" blink /> Computing…
+        </div>
+      </div>
+    );
+  }
+  const err = error ? <div className="part"> {error} </div> : undefined;
+  return (
+    <div className="pivot-centered">
+      {err}
+      <div className="part">
+        <Button
+          icon="EXECUTE"
+          label="Compute"
+          title="Builds the pivot table. This may take a few moments."
+          onClick={ compute }
+        />
+      </div>
+    </div>
+  );
+}
+
+export default function PivotTableComponent () : JSX.Element {
+  return (
+    <>
+      <TitleBar />
+      <Scroll>
+        <PivotTableBuild />
+      </Scroll>
+    </>
+  );
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/kernel/style.css b/ivette/src/frama-c/kernel/style.css
index 8b8b0435238982f4201588f9a930c83850d3f9a2..295008738903d77b302382ab92371a4d20bf5ecb 100644
--- a/ivette/src/frama-c/kernel/style.css
+++ b/ivette/src/frama-c/kernel/style.css
@@ -103,5 +103,30 @@
     white-space: pre-line;
 }
 
+/* -------------------------------------------------------------------------- */
+/* --- Pivot table                                                        --- */
+/* -------------------------------------------------------------------------- */
+
+.pivot-centered {
+    user-select: text;
+    display: flex;
+    flex-wrap: wrap;
+    flex-direction: column;
+    justify-content: center;
+    align-content: center;
+    height: 80%;
+}
+
+.pivot-centered .part {
+    user-select: text;
+    margin-top: 10px;
+    margin-bottom: 10px;
+    text-align: center;
+}
+
+.pivot-centered .dome-xButton-led {
+    display: inline-block;
+    vertical-align: baseline;
+}
 
 /* -------------------------------------------------------------------------- */
diff --git a/ivette/src/frama-c/react-pivottable.d.ts b/ivette/src/frama-c/react-pivottable.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69405ebe30afd6ccfd34a8248511096bf09ad711
--- /dev/null
+++ b/ivette/src/frama-c/react-pivottable.d.ts
@@ -0,0 +1,23 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2021                                                */
+/*     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).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+declare module 'react-pivottable/PivotTableUI';
diff --git a/ivette/yarn.lock b/ivette/yarn.lock
index cf9d9194ab96324c64e326719d726c7346dcf010..c423bf7fe1d48d7ea40479de19ef7cbb61633836 100644
--- a/ivette/yarn.lock
+++ b/ivette/yarn.lock
@@ -2571,6 +2571,11 @@ class-utils@^0.3.5:
     isobject "^3.0.0"
     static-extend "^0.1.1"
 
+classnames@^2.2.5:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+  integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
 clean-css@^4.2.3:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
@@ -5054,6 +5059,13 @@ immediate@~3.0.5:
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
   integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
 
+immutability-helper@^2.3.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.9.1.tgz#71c423ba387e67b6c6ceba0650572f2a2a6727df"
+  integrity sha512-r/RmRG8xO06s/k+PIaif2r5rGc3j4Yhc01jSBfwPCXDLYZwp/yxralI37Df1mwmuzcCsen/E/ITKcTEvc1PQmQ==
+  dependencies:
+    invariant "^2.2.0"
+
 immutable@:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
@@ -5150,6 +5162,13 @@ interpret@^1.4.0:
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
   integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
 
+invariant@^2.2.0:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+  dependencies:
+    loose-envify "^1.0.0"
+
 ip-regex@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -5843,7 +5862,7 @@ loglevel@^1.6.8:
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
   integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==
 
-loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -6988,6 +7007,15 @@ promise-inflight@^1.0.1:
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
   integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
 
+prop-types@>=15.0.0, prop-types@^15.5.10:
+  version "15.8.1"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+  integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.13.1"
+
 prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -7170,6 +7198,14 @@ react-dom@^16:
     prop-types "^15.6.2"
     scheduler "^0.19.1"
 
+react-draggable@^3.0.3:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.3.2.tgz#966ef1d90f2387af3c2d8bd3516f601ea42ca359"
+  integrity sha512-oaz8a6enjbPtx5qb0oDWxtDNuybOylvto1QLydsXgKmwT7e3GXC2eMVDwEMIUYJIFqVG72XpOv673UuuAq6LhA==
+  dependencies:
+    classnames "^2.2.5"
+    prop-types "^15.6.0"
+
 react-draggable@^4.4.4:
   version "4.4.4"
   resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f"
@@ -7197,7 +7233,7 @@ react-hot-loader@^4:
     shallowequal "^1.1.0"
     source-map "^0.7.3"
 
-react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7207,6 +7243,24 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
+react-pivottable@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/react-pivottable/-/react-pivottable-0.11.0.tgz#2ac9a28db6bdcefdb7bdeeb1ed067df229bb9dd8"
+  integrity sha512-hFU10XYL28NWVRnm+RRyHrzsAn/xIJzPgwPebUE2Hx5JZQl/Zxty3WUL/5cAcdK4AmTb9FQBRopLKLwuiI6VlA==
+  dependencies:
+    immutability-helper "^2.3.1"
+    prop-types "^15.5.10"
+    react-draggable "^3.0.3"
+    react-sortablejs "^1.3.4"
+    sortablejs "^1.6.1"
+
+react-sortablejs@^1.3.4:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/react-sortablejs/-/react-sortablejs-1.5.1.tgz#ba05a554548cf9733cc2d5411ece9b97f911b63e"
+  integrity sha512-bKIc1UVhjZt55Nb6WZFxZ8Jwyngg8CTt+w+iG1pA5k9LQsg1J0X6nLppHatSSDZDECtRZKp2z47tmmhPRJNj4g==
+  dependencies:
+    prop-types ">=15.0.0"
+
 react-virtualized@^9.22.3:
   version "9.22.3"
   resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
@@ -7943,6 +7997,11 @@ sort-keys@^1.0.0:
   dependencies:
     is-plain-obj "^1.0.0"
 
+sortablejs@^1.6.1:
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
+  integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
+
 source-list-map@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
diff --git a/src/kernel_internals/parsing/logic_lexer.mll b/src/kernel_internals/parsing/logic_lexer.mll
index 044cf5f450289b8bdbb8b30ca552da416397558c..6a73eae995e2013ed87e6509821403728bd6d297 100644
--- a/src/kernel_internals/parsing/logic_lexer.mll
+++ b/src/kernel_internals/parsing/logic_lexer.mll
@@ -283,7 +283,7 @@
       end
 
   (* Update lexer buffer. *)
-  let update_line_loc lexbuf line =
+  let update_line_pos lexbuf line =
     let pos = lexbuf.Lexing.lex_curr_p in
     lexbuf.Lexing.lex_curr_p <-
       { pos with
@@ -291,10 +291,7 @@
 	Lexing.pos_bol = pos.Lexing.pos_cnum;
       }
 
-  let update_newline_loc lexbuf =
-    update_line_loc lexbuf (lexbuf.Lexing.lex_curr_p.Lexing.pos_lnum + 1)
-
-  let update_file_loc lexbuf file =
+  let update_file_pos lexbuf file =
    let pos = lexbuf.Lexing.lex_curr_p in
     lexbuf.Lexing.lex_curr_p <- { pos with Lexing.pos_fname = file }
 
@@ -349,8 +346,8 @@ let utf8_char = ['\128'-'\254']+
 
 rule token = parse
   | space+ { token lexbuf }
-  | '\n' { update_newline_loc lexbuf; token lexbuf }
-  | comment_line '\n' { update_newline_loc lexbuf; token lexbuf }
+  | '\n' { Lexing.new_line lexbuf; token lexbuf }
+  | comment_line '\n' { Lexing.new_line lexbuf; token lexbuf }
   | comment_line eof { token lexbuf }
   | "*/" { lex_error lexbuf "unexpected block-comment closing" }
   | "/*" { if !accept_c_comments_into_acsl_spec
@@ -502,7 +499,7 @@ and chr buffer = parse
   | _  { Buffer.add_string buffer (lexeme lexbuf); chr buffer lexbuf }
 
 and hash = parse
-  '\n'		{ update_newline_loc lexbuf; token lexbuf}
+  '\n'		{ Lexing.new_line lexbuf; token lexbuf}
 | [' ''\t']		{ hash lexbuf}
 | rD+	        { (* We are seeing a line number. This is the number for the
                    * next line *)
@@ -517,14 +514,14 @@ and hash = parse
                        "Bad line number in preprocessed file: %s"  s;
                      (-1)
                  in
-                 update_line_loc lexbuf (lineno - 1);
+                 update_line_pos lexbuf (lineno - 1);
                   (* A file name may follow *)
 		  file lexbuf }
 | "line"        { hash lexbuf } (* MSVC line number info *)
 | _	        { endline lexbuf}
 
 and file =  parse
-        '\n'		        { update_newline_loc lexbuf; token lexbuf}
+        '\n'		        { Lexing.new_line lexbuf; token lexbuf}
 |	[' ''\t''\r']			{ file lexbuf}
 |	'"' ([^ '\012' '\t' '"']|"\\\"")* '"' {
     let n = Lexing.lexeme lexbuf in
@@ -532,38 +529,36 @@ and file =  parse
         ((String.length n) - 2) in
     let unescape = Str.regexp_string "\\\"" in
     let n1 = Str.global_replace unescape "\"" n1 in
-    update_file_loc lexbuf n1;
+    update_file_pos lexbuf n1;
     endline lexbuf
   }
 
 |	_			{ endline lexbuf}
 
 and endline = parse
-        '\n' 			{ update_newline_loc lexbuf; token lexbuf}
+        '\n' 			{ Lexing.new_line lexbuf; token lexbuf}
 |   eof                         { EOF }
 |	_			{ endline lexbuf}
 
 and comment = parse
-    '\n' { update_newline_loc lexbuf; comment lexbuf}
+    '\n' { Lexing.new_line lexbuf; comment lexbuf}
   | "*/" { token lexbuf}
   | eof  { lex_error lexbuf "non-terminating block-comment" }
   | _    { comment lexbuf}
 
 {
-  let set_initial_location dest_lexbuf src_loc =
-    Lexing.(
-      dest_lexbuf.lex_curr_p <-
-        { src_loc with
-          pos_bol = src_loc.pos_bol - src_loc.pos_cnum;
-          pos_cnum = 0; };
-    )
-
-  let parse_from_location f (loc, s : Filepath.position * string) =
+  (* When Ocaml 4.11+ becomes mandatory, we can probably replace this with
+     Lexing.set_position. *)
+  let set_initial_position dest_lexbuf src_pos =
+    dest_lexbuf.Lexing.lex_curr_p <- src_pos;
+    dest_lexbuf.lex_abs_pos <- src_pos.pos_cnum
+
+  let parse_from_position f (pos, s : Filepath.position * string) =
     let finally _ = Logic_utils.exit_kw_c_mode () in
     let output = Kernel.logwith finally ~wkey:Kernel.wkey_annot_error
     in
     let lb = from_string s in
-    set_initial_location lb (Cil_datatype.Position.to_lexing_pos loc);
+    set_initial_position lb (Cil_datatype.Position.to_lexing_pos pos);
     try
       let res = f token lb in
       Some (Cil_datatype.Position.of_lexing_pos lb.Lexing.lex_curr_p, res)
@@ -584,11 +579,11 @@ and comment = parse
         Kernel.fatal ~source:(Cil_datatype.Position.of_lexing_pos lb.lex_curr_p) "Unknown error (%s)"
           (Printexc.to_string exn)
 
-  let lexpr = parse_from_location Logic_parser.lexpr_eof
+  let lexpr = parse_from_position Logic_parser.lexpr_eof
 
-  let annot = parse_from_location Logic_parser.annot
+  let annot = parse_from_position Logic_parser.annot
 
-  let spec = parse_from_location Logic_parser.spec
+  let spec = parse_from_position Logic_parser.spec
 
   let ext_spec lexbuf = try
       accept_c_comments_into_acsl_spec := true ;
diff --git a/src/kernel_internals/typing/cabs2cil.ml b/src/kernel_internals/typing/cabs2cil.ml
index f958082d4b04d4d907094cd9ea3fefcb985d5284..264239138bb9d1bc00fe45a8bac7a0e0fd433de1 100644
--- a/src/kernel_internals/typing/cabs2cil.ml
+++ b/src/kernel_internals/typing/cabs2cil.ml
@@ -59,6 +59,31 @@ let valid_sid = false
 open Cil_types
 open Cil_datatype
 
+(* Maps the start and end positions of a function declaration or definition
+   (including its possible contract) to its name. *)
+module FuncLocs = struct
+  include
+    State_builder.List_ref
+      (Datatype.Triple
+         (Cil_datatype.Position)(Cil_datatype.Position)(Datatype.String)
+      )
+      (struct
+        let name = "FuncLocs"
+        let dependencies = [ Kernel.Files.self ]
+      end)
+
+  let add_loc ?spec loc1 loc2 funcname =
+    let startpos =
+      match spec with
+      | None -> fst loc1
+      | Some (_, spec_loc) -> fst spec_loc
+    in
+    let endpos = snd loc2 in
+    add (startpos, endpos, funcname)
+end
+
+let func_locs () = FuncLocs.get ()
+
 let stripUnderscore s =
   if String.length s = 1 then begin
     if s = "_" then
@@ -8613,12 +8638,14 @@ and createGlobal ghost logic_spec ((t,s,b,attr_list) : (typ * storage * bool * C
     List.exists (fun (_,el) -> List.exists is_fc_stdlib el) a
   in
   (* Make a first version of the varinfo *)
+  let vi_loc = convLoc cloc in
   let vi = makeVarInfoCabs ~ghost ~isformal:false ~referenced:islibc
-      ~isglobal:true ~isgenerated (convLoc cloc) (t,s,b,attr_list) (n,ndt,a)
+      ~isglobal:true ~isgenerated vi_loc (t,s,b,attr_list) (n,ndt,a)
   in
   (* Add the variable to the environment before doing the initializer
    * because it might refer to the variable itself *)
   if isFunctionType vi.vtype then begin
+    FuncLocs.add_loc ?spec:logic_spec (CurrentLoc.get ()) vi_loc n;
     if inite != Cabs.NO_INIT  then
       Kernel.error ~once:true ~current:true
         "Function declaration with initializer (%s)\n" vi.vname;
@@ -9240,6 +9267,7 @@ and doDecl local_env (isglobal: bool) : Cabs.definition -> chunk = function
       Kernel.debug ~dkey:Kernel.dkey_typing_global
         "Definition of %s at %a\n" n Cil_printer.pp_location idloc;
       CurrentLoc.set idloc;
+      FuncLocs.add_loc ?spec loc1 endloc n;
       IH.clear callTempVars;
 
       (* Make the fundec right away, and we'll populate it later. We
diff --git a/src/kernel_internals/typing/cabs2cil.mli b/src/kernel_internals/typing/cabs2cil.mli
index 44ea2ddf74785720d77ef8af20e9510925f8866b..f15e3a97b31a45f205d88aad2d1c1a375f4b208a 100644
--- a/src/kernel_internals/typing/cabs2cil.mli
+++ b/src/kernel_internals/typing/cabs2cil.mli
@@ -280,6 +280,22 @@ val stmtFallsThrough: Cil_types.stmt -> bool
 
 val fieldsToInit: Cil_types.compinfo -> string option -> Cil_types.offset list
 
+(**
+   Returns a mapping (pos_start, pos_end, funcname) for each function
+   declaration and definition, spanning the entire function, including
+   its specification (if it has one).
+
+   Currently, for function declarations, the location ends at the function
+   name, before the formal parameter list. For definitions, it spans until the
+   closing brace.
+
+   Note that the list is _not_ sorted, and must be further processed
+   for efficient data retrieval.
+
+   @since Frama-C+dev
+*)
+val func_locs : unit -> (Filepath.position * Filepath.position * string) list
+
 (*
 Local Variables:
 compile-command: "make -C ../../.."
diff --git a/src/kernel_services/ast_queries/cil_datatype.ml b/src/kernel_services/ast_queries/cil_datatype.ml
index ee6d4ef5573f305005e6e8ca321218f73bf56986..ef12ae7e9f1918b0e6d6f610be9b26642b78176d 100644
--- a/src/kernel_services/ast_queries/cil_datatype.ml
+++ b/src/kernel_services/ast_queries/cil_datatype.ml
@@ -206,6 +206,10 @@ module Position =  struct
   let pp_with_col fmt pos =
     Format.fprintf fmt "%a char %d" pretty pos
       (pos.Filepath.pos_cnum - pos.Filepath.pos_bol)
+  let pretty_debug fmt pos =
+    Format.fprintf fmt "%a:%d:%d"
+      Datatype.Filepath.pretty pos.Filepath.pos_path
+      pos.Filepath.pos_lnum pos.Filepath.pos_cnum
 end
 
 module Location = struct
@@ -242,11 +246,8 @@ module Location = struct
       Format.fprintf fmt "generated"
 
   let pretty_debug fmt loc =
-    Format.fprintf fmt "(%a:%d:%d,%a:%d:%d)"
-      Datatype.Filepath.pretty (fst loc).Filepath.pos_path
-      (fst loc).Filepath.pos_lnum (fst loc).Filepath.pos_cnum
-      Datatype.Filepath.pretty (snd loc).Filepath.pos_path
-      (snd loc).Filepath.pos_lnum (snd loc).Filepath.pos_cnum
+    Format.fprintf fmt "(%a,%a)"
+      Position.pretty_debug (fst loc) Position.pretty_debug (snd loc)
 
   let of_lexing_loc (pos1, pos2) =
     Position.of_lexing_pos pos1, Position.of_lexing_pos pos2
diff --git a/src/kernel_services/ast_queries/cil_datatype.mli b/src/kernel_services/ast_queries/cil_datatype.mli
index df5138305b7a051784a19e7f9c6e893383ac0648..f1064f288f97e0a6192087f5def008d9a4ea80ca 100644
--- a/src/kernel_services/ast_queries/cil_datatype.mli
+++ b/src/kernel_services/ast_queries/cil_datatype.mli
@@ -58,6 +58,11 @@ module Position: sig
   val pp_with_col : Format.formatter -> t -> unit
   val of_lexing_pos : Lexing.position -> t
   val to_lexing_pos : t -> Lexing.position
+
+  (** Pretty-print file, line and character offset.
+      @since Frama-C+dev
+  *)
+  val pretty_debug: t Pretty_utils.formatter
 end
 
 (** Cil locations. *)
diff --git a/src/plugins/metrics/metrics_pivot.ml b/src/plugins/metrics/metrics_pivot.ml
new file mode 100644
index 0000000000000000000000000000000000000000..78cc4db8fb9090341dae3e3b3f7ec0a13f1661ed
--- /dev/null
+++ b/src/plugins/metrics/metrics_pivot.ml
@@ -0,0 +1,544 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2021                                               *)
+(*    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).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+(* Data for a GUI-based pivot table *)
+
+open Cil_types
+
+module PivotSourceState =
+  State_builder.List_ref
+    (Datatype.List(Datatype.String))
+    (struct
+      let name = "PivotSourceState"
+      let dependencies = [ Ast.self; Db.Value.self; Property_status.self;
+                           Messages.self ]
+    end)
+
+type syntax_domain =
+  | Code
+  | Declaration
+  | Annotation
+
+type message_domain = Log.kind
+
+type domain =
+  | Syntax of syntax_domain
+  | Property
+  | Message of message_domain
+
+let string_of_string_list sep =
+  Format.asprintf "%a"
+    (Pretty_utils.pp_list ~pre:"" ~suf:"" ~sep Format.pp_print_string)
+
+let split_loc (loc : Cil_types.location) =
+  let fp = (fst loc).pos_path in
+  let line = string_of_int (fst loc).pos_lnum in
+  let fp_pretty = Filepath.Normalized.to_pretty_string fp in
+  let dir = Filename.dirname fp_pretty in
+  let name = Filename.basename fp_pretty in
+  let ext = Filename.extension name in
+  dir, name, ext, line
+
+let node_of_instr = function
+  | Set _ -> "assignment"
+  | Call _ -> "call"
+  | Local_init _ -> "local_init"
+  | Asm _ -> "asm"
+  | Skip _ -> "nop"
+  | Code_annot _ ->
+    (* should be done by the annotations visitor *)
+    assert false
+
+let opt_node_of_stmtkind = function
+  | Instr _ -> None
+  | Return (_, _) -> Some "return"
+  | Goto (_, _) -> Some "goto"
+  | Break _ -> Some "break"
+  | Continue _ -> Some "continue"
+  | If (_, _, _, _) -> Some "if"
+  | Switch (_, _, _, _) -> Some "switch"
+  | Loop (_, _, _, _, _) -> Some "loop"
+  | Block _ -> None
+  | UnspecifiedSequence _ -> Some "unspecified_sequence"
+  | Throw (_, _) -> Some "throw"
+  | TryCatch (_, _, _) -> Some "try_catch"
+  | TryFinally (_, _, _) -> Some "try_finally"
+  | TryExcept (_, _, _, _) -> Some "try_except"
+
+let node_of_code_annotation_node = function
+  | AAssert _ -> "assert"
+  | AStmtSpec _ -> "stmt_spec"
+  | AInvariant _ -> "invariant"
+  | AVariant _ -> "variant"
+  | AAssigns _ -> "assigns"
+  | AAllocation _ -> "allocation"
+  | APragma _ -> "ca_pragma"
+  | AExtended _ -> "extended"
+
+let node_of_global_annotation = function
+  | Dfun_or_pred _ -> "logic_fun_or_pred"
+  | Dvolatile _ ->  "logic_volatile"
+  | Daxiomatic _ -> "axiomatic"
+  | Dtype _ -> "logic_type"
+  | Dlemma _ -> "logic_lemma"
+  | Dinvariant _ -> "invariant"
+  | Dtype_annot _ -> "type_annot"
+  | Dmodel_annot _ -> "model_annot"
+  | Dextended _ -> "ga_extended"
+
+let domain_and_node_of_global = function
+  | GType _ -> Declaration, "gtype"
+  | GCompTag _ -> Declaration, "gcomptag"
+  | GCompTagDecl _ -> Declaration, "gcomptagdecl"
+  | GEnumTag _ -> Declaration, "genumtag"
+  | GEnumTagDecl _ -> Declaration, "genumtagdecl"
+  | GVarDecl _ -> Declaration, "global_var"
+  | GFunDecl _ -> Declaration, "gfundecl"
+  | GVar _ -> Declaration, "gvar"
+  | GFun _ -> Declaration, "gfun"
+  | GAsm _ -> Code, "gasm"
+  | GPragma _ -> Code, "gpragma"
+  | GText _ -> Code, "gtext"
+  | GAnnot _ -> Code, "gannot"
+
+let names_of_global = function
+  | GType (_, _) -> []
+  | GCompTag (_, _) -> []
+  | GCompTagDecl (_, _) -> []
+  | GEnumTag (_, _) -> []
+  | GEnumTagDecl (_, _) -> []
+  | GVarDecl (vi, _) -> [vi.vname]
+  | GFunDecl (_, vi, _) -> [vi.vname]
+  | GVar (vi, _, _) -> [vi.vname]
+  | GFun ({svar}, _) -> [svar.vname]
+  | GAsm (_, _) -> []
+  | GPragma (_, _) -> []
+  | GText _ -> []
+  | GAnnot (_, _) -> []
+
+let name_of_logic_info li = li.l_var_info.lv_name
+
+let name_of_global_annotation = function
+  | Dvolatile (_, _, _, _, _) -> None
+  | Dfun_or_pred (li, _)
+  | Dinvariant (li, _)
+  | Dtype_annot (li, _) -> Some (name_of_logic_info li)
+  | Daxiomatic (name, _, _, _)
+  | Dtype ({lt_name = name}, _)
+  | Dlemma (name, _, _, _, _, _)
+  | Dmodel_annot ({mi_name = name}, _)
+  | Dextended ({ext_name = name}, _, _) -> Some name
+
+let node_of_predicate_kind = function
+  | Assert -> "assert"
+  | Check -> "check"
+  | Admit -> "admit"
+
+let node_of_property = function
+  | Property.IPPredicate idp ->
+    Format.asprintf "%a" Property.pretty_predicate_kind idp.ip_kind
+  | IPCodeAnnot ica -> node_of_code_annotation_node ica.ica_ca.annot_content
+  | IPAxiomatic _ -> "axiomatic"
+  | IPLemma _ -> "lemma"
+  | IPBehavior _ -> "behavior"
+  | IPComplete _ -> "complete"
+  | IPDisjoint _ -> "disjoint"
+  | IPAllocation _ -> "allocates"
+  | IPAssigns _ -> "assigns"
+  | IPFrom _ -> "from"
+  | IPDecrease _ -> "loop variant"
+  | IPExtended _ -> "extended"
+  | IPReachable _ -> "reachable"
+  | IPPropertyInstance _ -> "instance"
+  | IPTypeInvariant _ -> "type_invariant"
+  | IPGlobalInvariant _ -> "global_invariant"
+  | IPOther _ -> "other"
+
+let names_of_property = function
+  | Property.IPPredicate idp -> idp.ip_pred.ip_content.tp_statement.pred_name
+  | IPCodeAnnot _ -> []
+  | IPAxiomatic _ -> []
+  | IPLemma _ -> []
+  | IPBehavior _ -> []
+  | IPComplete _ -> []
+  | IPDisjoint _ -> []
+  | IPAllocation _ -> []
+  | IPAssigns _ -> []
+  | IPFrom _ -> []
+  | IPDecrease _ -> []
+  | IPExtended _ -> []
+  | IPReachable _ -> []
+  | IPPropertyInstance _ -> []
+  | IPTypeInvariant _ -> []
+  | IPGlobalInvariant _ -> []
+  | IPOther _ -> []
+
+let plugin_of_emitters el =
+  match el with
+  | []
+  | ["Call Preconditions"]
+  | ["Frama-C kernel"] -> "kernel"
+  | ["Eva"] -> "eva"
+  | _ -> "<unknown emitters (" ^ string_of_string_list "," el ^ ")>"
+
+let string_of_syntax_domain = function
+  | Code -> "code"
+  | Declaration -> "decl"
+  | Annotation -> "annot"
+
+let string_of_message_domain = function
+  | Log.Result -> "result"
+  | Feedback -> "feedback"
+  | Debug -> "debug"
+  | Warning ->  "warning"
+  | Error -> "error"
+  | Failure -> "failure"
+
+let split_domain = function
+  | Syntax d -> "syntax", string_of_syntax_domain d
+  | Property -> "property", ""
+  | Message d -> "message", string_of_message_domain d
+
+module FunctionAtPos = struct
+  let tbl :
+    (Filepath.Normalized.t,
+     (Filepath.position * Filepath.position * string) Array.t)
+      Hashtbl.t =
+    Hashtbl.create 16
+
+  let binary_search a pos : string option =
+    let cmp = Cil_datatype.Position.compare in
+    let rec aux lo hi =
+      if lo > hi then None
+      else
+        let mid = (hi + lo) / 2 in
+        let (mstart, mend, mfunc) = a.(mid) in
+        if cmp pos mstart >= 0 then
+          if cmp pos mend <= 0 then Some mfunc
+          else aux (mid + 1) hi
+        else aux lo (mid - 1)
+    in
+    aux 0 (Array.length a - 1)
+
+  let compute () =
+    let tmp = Hashtbl.create 16 in
+    let files =
+      List.fold_left (fun acc ((pos1, _, _) as triple) ->
+          let fp = pos1.Filepath.pos_path in
+          Hashtbl.add tmp fp triple;
+          Datatype.Filepath.Set.add fp acc
+        ) Datatype.Filepath.Set.empty (Cabs2cil.func_locs ())
+    in
+    Hashtbl.clear tbl;
+    Datatype.Filepath.Set.iter (fun fp ->
+        let l =
+          List.sort (fun (start1, _, _) (start2, _, _) ->
+              Cil_datatype.Position.compare start1 start2
+            ) (Hashtbl.find_all tmp fp)
+        in
+        Hashtbl.replace tbl fp (Array.of_list l)
+      ) files
+
+  let find pos =
+    let fp = pos.Filepath.pos_path in
+    Option.bind (Hashtbl.find_opt tbl fp)
+      (fun a -> binary_search a pos)
+
+end
+
+type entry = {
+  loc: Cil_datatype.Location.t;
+  func: string option;
+  domain: domain;
+  plugin: string;
+  status: string;
+  node: string;
+  names: string list;
+  text: string;
+}
+
+let headers = ["Directory"; "Filename"; "Extension"; "Line Number";
+               "Function"; "Domain"; "Kind"; "Plugin"; "Status"; "Node";
+               "Names"; "Text"]
+
+let add_entry_str entry =
+  let dir, fname, ext, line = split_loc entry.loc in
+  let funcname = Option.fold ~none:"" ~some:Stdlib.Fun.id entry.func in
+  let domain, kind = split_domain entry.domain in
+  let names = string_of_string_list ":" entry.names in
+  let entry = [dir; fname; ext; line; funcname;
+               domain; kind; entry.plugin; entry.status; entry.node;
+               names; entry.text]
+  in
+  if PivotSourceState.get () = [] then
+    PivotSourceState.add headers;
+  PivotSourceState.add entry
+
+let new_entry ?func ?(plugin="") ?(status="") ?(node="") ?(names=[]) ?(text="") loc domain =
+  { loc; func; domain; plugin; status; node; names; text }
+
+let add_entry ?func ?plugin ?status ?node ?names ?text loc domain =
+  let entry = new_entry ?func ?plugin ?status ?node ?names ?text loc domain in
+  add_entry_str entry
+
+class full_visitor = object(self)
+  inherit Cil.nopCilVisitor
+  val mutable cur_func = None
+  method add ?func ?node ?names domain =
+    let loc = Cil.CurrentLoc.get () in
+    let func = if func <> None && func <> Some "" then func else cur_func in
+    add_entry ?func ?node ?names loc domain
+  method add_code ?func ?names node =
+    self#add ?func ~node ?names (Syntax Code)
+  method add_decl ?func ?names node =
+    self#add ?func ~node ?names (Syntax Declaration)
+  method add_annot ?names node =
+    self#add ~node ?names (Syntax Annotation)
+
+  method! vvrbl (_v:varinfo) = DoChildren
+  method! vvdec (_v:varinfo) = DoChildren
+  method! vexpr (_e:exp) = DoChildren
+  method! vlval (_l:lval) = DoChildren
+  method! voffs (_o:offset) = DoChildren
+  method! vinitoffs (_o:offset) = DoChildren
+
+  method! vinst (i:instr) =
+    let node = node_of_instr i in
+    self#add_code node;
+    DoChildren
+
+  method! vstmt (s:stmt) =
+    begin
+      match opt_node_of_stmtkind s.skind with
+      | None -> ()
+      | Some node -> self#add_code node
+    end;
+    DoChildren
+
+  method! vfunc (f:fundec) =
+    cur_func <- Some f.svar.vname;
+    Cil.DoChildrenPost (fun fd ->
+        cur_func <- None;
+        fd
+      )
+
+  method! vglob (g:global) =
+    let domain, node = domain_and_node_of_global g in
+    let names = names_of_global g in
+    let func = "<global>" in
+    if domain = Declaration then
+      self#add_decl ~func ~names node
+    else
+      self#add_code ~func ~names node;
+    DoChildren
+
+  method! vblock _ = DoChildren
+  method! vinit _ _ _ = DoChildren
+  method! vlocal_init _ _ = DoChildren
+  method! vtype _ = DoChildren
+  method! vcompinfo _ = DoChildren
+  method! venuminfo _ = DoChildren
+  method! vfieldinfo _ = DoChildren
+  method! venumitem _ = DoChildren
+  method! vattr _ = DoChildren
+  method! vattrparam _ = DoChildren
+  method! vmodel_info _ = DoChildren
+  method! vlogic_type _ = DoChildren
+  method! videntified_term _ = DoChildren
+  method! vterm _ = DoChildren
+  method! vterm_node _ = DoChildren
+  method! vterm_lval _ = DoChildren
+  method! vterm_lhost _ = DoChildren
+  method! vterm_offset _ = DoChildren
+  method! vlogic_label _ = DoChildren
+  method! vlogic_info_decl _ = DoChildren
+  method! vlogic_info_use _ = DoChildren
+  method! vlogic_type_info_decl _ = DoChildren
+  method! vlogic_type_info_use _ = DoChildren
+  method! vlogic_type_def _ = DoChildren
+  method! vlogic_ctor_info_decl _ = DoChildren
+  method! vlogic_ctor_info_use _ = DoChildren
+  method! vlogic_var_use _ = DoChildren
+  method! vlogic_var_decl _ = DoChildren
+  method! vquantifiers _ = DoChildren
+  method! videntified_predicate _ =
+    (* should be done by the annotations visitor *)
+    assert false
+  method! vpredicate_node _ = DoChildren
+  method! vpredicate _ = DoChildren
+  method! vbehavior _ = DoChildren
+  method! vassigns _ = DoChildren
+  method! vfrees _ = DoChildren
+  method! vallocates _ = DoChildren
+  method! vallocation _ = DoChildren
+  method! vloop_pragma _ = DoChildren
+  method! vslice_pragma _ = DoChildren
+  method! vimpact_pragma _ = DoChildren
+  method! vdeps _ = DoChildren
+  method! vfrom _ = DoChildren
+  method! vcode_annot _ =
+    (* should be done by the annotations visitor *)
+    assert false
+  method! vannotation ga =
+    let node = node_of_global_annotation ga in
+    let names = Option.to_list (name_of_global_annotation ga) in
+    self#add_annot ~names node;
+    DoChildren
+end
+
+let ca_visitor_cur_func : string option ref = ref None
+let ca_visitor_cur_emitter = ref "<unknown>"
+class code_annot_visitor = object(self)
+  inherit Cil.nopCilVisitor
+
+  method add_annot ?(names=[]) node =
+    let loc = Cil.CurrentLoc.get () in
+    let func = !ca_visitor_cur_func in
+    let plugin = !ca_visitor_cur_emitter in
+    let domain = Syntax Annotation in
+    add_entry ?func ~plugin ~node ~names loc domain
+
+  method! videntified_predicate {ip_content = {tp_kind}} =
+    let node = node_of_predicate_kind tp_kind in
+    self#add_annot node;
+    DoChildren
+
+  method! vcode_annot ca =
+    let content = ca.annot_content in
+    self#add_annot (node_of_code_annotation_node content);
+    DoChildren
+
+  method! vannotation ga =
+    let node = node_of_global_annotation ga in
+    let names = Option.to_list (name_of_global_annotation ga) in
+    self#add_annot ~names node;
+    DoChildren
+end
+
+let visit_annots () =
+  let vis = new code_annot_visitor in
+  Annotations.iter_all_code_annot (
+    fun stmt emitter ca ->
+      let kf = Kernel_function.find_englobing_kf stmt in
+      ca_visitor_cur_func := Some (Kernel_function.get_name kf);
+      ca_visitor_cur_emitter := Emitter.get_name emitter;
+      ignore (Cil.visitCilCodeAnnotation (vis :> Cil.cilVisitor) ca)
+  )
+
+let visit_properties () =
+  Property_status.iter
+    (fun ip ->
+       let loc = Property.location ip in
+       let func = Option.map Kernel_function.get_name (Property.get_kf ip) in
+       let emitters =
+         Property_status.fold_on_statuses (fun emitter _status acc ->
+             Emitter.Usable_emitter.get_name emitter.emitter :: acc
+           ) ip []
+       in
+       let plugin = plugin_of_emitters emitters in
+       let domain = Property in
+       let status =
+         Format.asprintf "%a"
+           Property_status.Feedback.pretty (Property_status.Feedback.get ip)
+       in
+       let node = node_of_property ip in
+       let names = names_of_property ip in
+       add_entry ?func ~node ~status ~plugin ~names loc domain
+    )
+
+let visit_messages () =
+  FunctionAtPos.compute ();
+  Messages.iter (fun ev ->
+      let plugin = ev.evt_plugin in
+      let loc_of_pos p = (p, p) in
+      let loc, func =
+        match ev.evt_source with
+        | None -> Cil_datatype.Location.unknown, "<global>"
+        | Some pos ->
+          let funcname =
+            match FunctionAtPos.find pos with
+            | None -> Metrics_parameters.warning
+                        "FUNCTION NOT FOUND FOR NON-GLOBAL MESSAGE POS: %a"
+                        Cil_datatype.Position.pretty_debug pos;
+              "<unknown>"
+            | Some name -> name
+          in
+          loc_of_pos pos, funcname
+      in
+      let domain = Message (ev.evt_kind) in
+      let text = ev.evt_message in
+      add_entry ~func ~plugin ~text loc domain
+    )
+
+(* Useful mainly for debugging *)
+let _pp_as_csv (data : string list list) =
+  let pp_list = string_of_string_list "," in
+  List.iter (fun ls -> Format.printf "%s@\n" (pp_list ls)) (List.rev data)
+
+(* Server / Ivette stuff *)
+
+open Server
+
+let package =
+  Package.package
+    ~plugin:"pivot"
+    ~name:"general"
+    ~title:"Pivot Table Services"
+    ~readme:"pivot.md"
+    ()
+
+module TableState = struct
+  type t = string list list
+  let jtype =
+    Data.declare ~package
+      ~name:"tableStateType"
+      ~descr:(Markdown.plain "State of the pivot table source data.")
+      Package.(Jarray (Jarray Jstring))
+  let to_json ll =
+    `List (List.rev_map (fun l -> `List (List.map (fun s -> `String s) l)) ll)
+end
+
+let pivot_signal =
+  States.register_value ~package
+    ~name:"pivotState"
+    ~descr:(Markdown.plain "State of the pivot table source data.")
+    ~output:(module TableState)
+    ~get:PivotSourceState.get
+    ~add_hook:PivotSourceState.add_hook_on_update
+    ()
+
+let compute () =
+  let ast = Ast.get () in
+  let vis = new full_visitor in
+  ignore (Cil.visitCilFile (vis :> Cil.cilVisitor) ast);
+  visit_annots ();
+  visit_properties ();
+  visit_messages ();
+  (*_pp_as_csv (PivotSourceState.get ());*)
+  (* Signals that the pivot table has been updated. *)
+  Server.Request.emit pivot_signal
+
+let _compute =
+  Server.Request.register ~package
+    ~kind:`EXEC ~name:"compute"
+    ~descr:(Markdown.plain "Computes the pivot table.")
+    ~input:(module Data.Junit) ~output:(module Data.Junit) compute
diff --git a/src/plugins/metrics/metrics_pivot.mli b/src/plugins/metrics/metrics_pivot.mli
new file mode 100644
index 0000000000000000000000000000000000000000..98ffe7ef308729877bd4ce7bf50dd0584b275cde
--- /dev/null
+++ b/src/plugins/metrics/metrics_pivot.mli
@@ -0,0 +1,23 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2021                                               *)
+(*    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).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+(** Nothing is exported. *)
diff --git a/tests/syntax/func_locs.i b/tests/syntax/func_locs.i
new file mode 100644
index 0000000000000000000000000000000000000000..9bde496168fe2aa66a3b12a9963fbfa61147fbb0
--- /dev/null
+++ b/tests/syntax/func_locs.i
@@ -0,0 +1,36 @@
+/* run.config
+ MODULE: @PTEST_NAME@
+   STDOPT: +"-no-print"
+*/
+
+
+//@ assigns \nothing;
+void with_spec() {
+}
+
+int gx;
+
+/*@
+  requires req: a >= 0 || gx == 0;
+  assigns \result \from a;
+  ensures ens: \result == a;
+ */
+int id(int a);
+
+/*@
+  requires req: a >= 0;
+*/
+int id(int a);
+
+int decl_no_spec(); int id(int a);
+
+int def_no_spec() {
+  return 2;
+}
+
+/*@
+  requires \true;
+*/
+int main() {
+  return 0;
+}
diff --git a/tests/syntax/func_locs.ml b/tests/syntax/func_locs.ml
new file mode 100644
index 0000000000000000000000000000000000000000..e28f0429b68ca220b12e37f15c4417edc32179c6
--- /dev/null
+++ b/tests/syntax/func_locs.ml
@@ -0,0 +1,9 @@
+let run () =
+  List.iter (fun (pos1, pos2, fname) ->
+      Format.printf "%a - %a -> %s@."
+        Cil_datatype.Position.pp_with_col pos1
+        Cil_datatype.Position.pp_with_col pos2
+        fname
+    ) (Cabs2cil.func_locs ())
+
+let () = Db.Main.extend run
diff --git a/tests/syntax/oracle/func_locs.res.oracle b/tests/syntax/oracle/func_locs.res.oracle
new file mode 100644
index 0000000000000000000000000000000000000000..8fd4ca099e2fa45737df2beec4c827b0473d0f65
--- /dev/null
+++ b/tests/syntax/oracle/func_locs.res.oracle
@@ -0,0 +1,10 @@
+[kernel] Parsing func_locs.i (no preprocessing)
+[kernel] func_locs.i:20: Warning: 
+  found two contracts (old location: func_locs.i:13). Merging them
+func_locs.i:31 char 3 - func_locs.i:36 char 1 -> main
+func_locs.i:27 char 4 - func_locs.i:29 char 1 -> def_no_spec
+func_locs.i:25 char 20 - func_locs.i:25 char 26 -> id
+func_locs.i:25 char 0 - func_locs.i:25 char 16 -> decl_no_spec
+func_locs.i:20 char 3 - func_locs.i:23 char 6 -> id
+func_locs.i:13 char 3 - func_locs.i:18 char 6 -> id
+func_locs.i:7 char 3 - func_locs.i:9 char 1 -> with_spec