Spaces:
Running
Running
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&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&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&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 < pid.sql # or: psql -f pid.sql
cypher-shell -f graph.cypher # Neo4j</pre>
</section>
<footer>
<p>Drawing: sheet 269 of the “Dataset PID” 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 => ({ "&": "&", "<": "<", ">": ">" })[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>
|