File size: 12,047 Bytes
aa0efbd
deefdbe
 
 
 
 
05e9dc8
deefdbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbf8784
 
deefdbe
cbf8784
deefdbe
 
 
 
 
 
 
 
 
 
 
 
22bbb00
deefdbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174b6c5
deefdbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174b6c5
deefdbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa0efbd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pid-parser showcase</title>
<meta name="description" content="P&ID image to tables and graph with a vision-language model for the symbols and traditional computer vision for the connections: precomputed results on one very dense PID2Graph sheet.">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.2/cytoscape.min.js"></script>
<style>
:root {
  --bg: #f7f7f5; --panel: #ffffff; --fg: #1d1d1b; --muted: #6b6b66; --line: #e2e2dc;
  --accent: #0b6e69; --process: #0b6e69; --signal: #c2410c; --junction: #6b6b66; --sym: #1d4ed8;
}
@media (prefers-color-scheme: dark) {
  :root { --bg: #141413; --panel: #1d1d1b; --fg: #ecece6; --muted: #a3a39b; --line: #33332f;
    --accent: #5eb8b2; --process: #5eb8b2; --signal: #fb923c; --junction: #a3a39b; --sym: #93b4ff; }
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
main { max-width: 1200px; margin: 0 auto; padding: 24px 16px 48px; }
h1 { font-size: 26px; margin: 0 0 4px; }
h2 { font-size: 17px; margin: 0 0 10px; }
p { margin: 0 0 8px; }
a { color: var(--accent); }
.muted { color: var(--muted); }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0 0; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
.row { display: grid; grid-template-columns: 3fr 2fr; gap: 16px; margin-top: 16px; }
@media (max-width: 860px) { .row { grid-template-columns: 1fr; } }
.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
.tabs button, .toggles label { border: 1px solid var(--line); background: var(--panel); color: var(--fg); border-radius: 999px; padding: 6px 14px; font: inherit; cursor: pointer; }
.tabs button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--bg); }
.toggles { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.toggles label { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; padding: 4px 12px; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 16px; }
@media (max-width: 600px) { .stats { grid-template-columns: repeat(2, 1fr); } }
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
.stat b { display: block; font-size: 22px; }
.stat span { font-size: 12px; color: var(--muted); }
#drawing { width: 100%; height: auto; display: block; background: #fff; border-radius: 6px; }
#graph { width: 100%; height: 420px; }
.legend { font-size: 12px; color: var(--muted); margin-top: 8px; display: flex; gap: 14px; flex-wrap: wrap; }
.legend i { display: inline-block; width: 18px; height: 3px; vertical-align: middle; margin-right: 4px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--panel); }
.scroll { max-height: 320px; overflow: auto; }
.files { display: flex; flex-wrap: wrap; gap: 8px; }
.files a { border: 1px solid var(--line); border-radius: 6px; padding: 4px 10px; text-decoration: none; font-size: 13px; }
footer { margin-top: 28px; font-size: 13px; color: var(--muted); }
</style>
</head>
<body>
<main>
  <h1>Your P&amp;ID drawing, now data you can ask</h1>
  <p class="muted">SQL tables and a graph that people and AI agents can query. A vision-language model finds the symbols tile by tile; the connections come from traditional computer vision (thresholding and connected-component labeling, no model); small scripts validate the graph and export it. No GPU, no training.</p>
  <pre>npx skills add dzakybd/pid-parser</pre>
  <p class="muted" style="margin-top:8px">Two versions: a skill for coding agents such as Claude Code and Codex, and a LangChain Deep Agents version for your own agent workflow. Source: <a href="https://github.com/dzakybd/pid-parser">github.com/dzakybd/pid-parser</a>. How it works, step by step: <a href="https://dzaky.ai.id/en/blog/pid-parser-pid-to-graph">the write-up</a>. The sheet is one very dense P&amp;ID (190 symbols, 732 connected symbol pairs); the two tabs are two runs on it. These results were computed ahead of time; this page calls no model.</p>

  <div class="tabs" id="tabs" role="group" aria-label="Sheet"></div>
  <div class="stats" id="stats"></div>

  <div class="row">
    <section class="panel">
      <h2>Drawing and what was read</h2>
      <div class="toggles">
        <label><input type="checkbox" id="showSym" checked> symbols</label>
        <label><input type="checkbox" id="showEdge" checked> connections</label>
      </div>
      <svg id="drawing" role="img" aria-label="P&amp;ID drawing with detected symbols and connections"></svg>
      <div class="legend"><span><i style="background:var(--sym)"></i>symbol box</span><span><i style="background:var(--process)"></i>process pipe</span></div>
    </section>
    <section class="panel">
      <h2>Graph</h2>
      <div id="graph"></div>
      <p class="muted" style="font-size:12px;margin-top:6px">Nodes sit where the symbols are on the sheet. Grey dots are junctions (tees).</p>
    </section>
  </div>

  <div class="row">
    <section class="panel">
      <h2>components</h2>
      <div class="scroll"><table id="nodes"></table></div>
    </section>
    <section class="panel">
      <h2>connections</h2>
      <div class="scroll"><table id="edges"></table></div>
    </section>
  </div>

  <section class="panel" style="margin-top:16px">
    <h2>Download this sheet's output</h2>
    <div class="files" id="files"></div>
    <pre>sqlite3 pid.db &lt; pid.sql          # or: psql -f pid.sql
cypher-shell -f graph.cypher       # Neo4j</pre>
  </section>

  <footer>
    <p>Drawing: sheet 269 of the &ldquo;Dataset PID&rdquo; part of <a href="https://zenodo.org/records/14803338">PID2Graph</a> (<a href="https://arxiv.org/abs/2411.13929">paper</a>), CC BY-SA 4.0. The drawings and the outputs derived from them on this page are shared under CC BY-SA 4.0. Code: MIT.</p>
    <p>Scores compare against the PID2Graph ground truth. Symbols: one-to-one match by box center. Connections: symbol pairs on the same pipe network.</p>
  </footer>
</main>
<script>
const NS = "http://www.w3.org/2000/svg";
const css = name => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const pct = v => (v * 100).toFixed(0) + "%";
let sheets = [], cy = null;

function el(tag, attrs, parent) {
  const e = document.createElementNS(NS, tag);
  for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
  parent.appendChild(e);
  return e;
}
const center = b => [(b[0] + b[2]) / 2, (b[1] + b[3]) / 2];

function drawSvg(sheet, graph) {
  const svg = document.getElementById("drawing");
  svg.innerHTML = "";
  svg.setAttribute("viewBox", `0 0 ${graph.image.width} ${graph.image.height}`);
  el("image", { href: `data/${sheet.id}/drawing.jpg`, x: 0, y: 0, width: graph.image.width, height: graph.image.height }, svg);
  const stroke = graph.image.width / 600;
  const byId = Object.fromEntries(graph.nodes.map(n => [n.id, n]));
  const edges = el("g", { id: "edgeLayer" }, svg);
  for (const e of graph.edges) {
    const [x1, y1] = center(byId[e.source].bbox), [x2, y2] = center(byId[e.target].bbox);
    el("line", { x1, y1, x2, y2, stroke: e.kind === "signal" ? css("--signal") : css("--process"),
      "stroke-width": stroke, "stroke-dasharray": e.kind === "signal" ? `${stroke * 3} ${stroke * 2}` : "", opacity: 0.8 }, edges);
  }
  const syms = el("g", { id: "symLayer" }, svg);
  for (const n of graph.nodes) {
    const [x1, y1, x2, y2] = n.bbox;
    if (n.type === "junction") {
      el("circle", { cx: (x1 + x2) / 2, cy: (y1 + y2) / 2, r: stroke * 2.5, fill: css("--junction") }, syms);
      continue;
    }
    const r = el("rect", { x: x1, y: y1, width: x2 - x1, height: y2 - y1, fill: "none", stroke: css("--sym"), "stroke-width": stroke }, syms);
    el("title", {}, r).textContent = `${n.id} ${n.type}${n.tag ? " " + n.tag : ""}`;
  }
  applyToggles();
}

function applyToggles() {
  const s = document.getElementById("symLayer"), e = document.getElementById("edgeLayer");
  if (s) s.style.display = document.getElementById("showSym").checked ? "" : "none";
  if (e) e.style.display = document.getElementById("showEdge").checked ? "" : "none";
}

function drawGraph(graph) {
  const scale = 600 / graph.image.width;
  if (cy) cy.destroy();
  cy = cytoscape({
    container: document.getElementById("graph"),
    elements: [
      ...graph.nodes.map(n => { const [x, y] = center(n.bbox);
        return { data: { id: n.id, label: n.type === "junction" ? "" : (n.tag || n.type), j: n.type === "junction" }, position: { x: x * scale, y: y * scale } }; }),
      ...graph.edges.map((e, i) => ({ data: { id: "e" + i, source: e.source, target: e.target, kind: e.kind } })),
    ],
    layout: { name: "preset", fit: true, padding: 16 },
    style: [
      { selector: "node", style: { "background-color": css("--sym"), width: 10, height: 10, label: "data(label)", "font-size": 7, color: css("--fg"), "text-valign": "top", "text-margin-y": -2 } },
      { selector: "node[?j]", style: { "background-color": css("--junction"), width: 5, height: 5 } },
      { selector: "edge", style: { width: 1.5, "line-color": css("--process"), "curve-style": "straight" } },
      { selector: "edge[kind = 'signal']", style: { "line-color": css("--signal"), "line-style": "dashed" } },
    ],
  });
}

function fillTables(graph) {
  const esc = s => String(s ?? "").replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
  document.getElementById("nodes").innerHTML = "<tr><th>id</th><th>type</th><th>tag</th></tr>" +
    graph.nodes.map(n => `<tr><td>${esc(n.id)}</td><td>${esc(n.type)}</td><td>${esc(n.tag)}</td></tr>`).join("");
  document.getElementById("edges").innerHTML = "<tr><th>source</th><th>target</th><th>kind</th><th>line</th></tr>" +
    graph.edges.map(e => `<tr><td>${esc(e.source)}</td><td>${esc(e.target)}</td><td>${esc(e.kind)}</td><td>${esc(e.line)}</td></tr>`).join("");
}

async function show(i) {
  const sheet = sheets[i];
  document.querySelectorAll("#tabs button").forEach((b, j) => b.setAttribute("aria-pressed", String(i === j)));
  const graph = await (await fetch(`data/${sheet.id}/graph.json`)).json();
  const sc = sheet.score;
  document.getElementById("stats").innerHTML = [
    [pct(sc.symbols.f1), `symbol F1 (${sc.symbols.matched}/${sc.symbols.gt} found)`],
    [pct(sc.connections.f1), `connection F1 (P ${pct(sc.connections.precision)}, R ${pct(sc.connections.recall)})`],
    [graph.nodes.length + " / " + graph.edges.length, "nodes / edges"],
    [sheet.seconds + " s", `${sheet.model}${sheet.effort ? ", effort " + sheet.effort : ""}`],
  ].map(([b, s]) => `<div class="stat"><b>${b}</b><span>${s}</span></div>`).join("");
  drawSvg(sheet, graph);
  drawGraph(graph);
  fillTables(graph);
  document.getElementById("files").innerHTML = ["graph.json", "out/pid.sql", "out/nodes.csv", "out/edges.csv", "out/graph.graphml", "out/graph.cypher"]
    .map(f => `<a href="data/${sheet.id}/${f}" download>${f.replace("out/", "")}</a>`).join("");
}

document.getElementById("showSym").addEventListener("change", applyToggles);
document.getElementById("showEdge").addEventListener("change", applyToggles);

fetch("data/sheets.json").then(r => r.json()).then(list => {
  sheets = list;
  const tabs = document.getElementById("tabs");
  list.forEach((s, i) => {
    const b = document.createElement("button");
    b.textContent = s.title;
    b.onclick = () => show(i);
    tabs.appendChild(b);
  });
  show(0);
});
</script>
</body>
</html>