mirror of
https://github.com/penpot/penpot.git
synced 2026-08-23 21:28:38 +00:00
✨ Style Component nodes and IsInstanceOf edges in graph console
Slice-3 export sends edges with a rel field. Derive tree ranking, combo derivation and fold-ability from IsChildOf only; draw other rels as overlay edges with per-rel styles (EDGE_STYLES: IsInstanceOf violet dashed, matching the new Component diamond in NODE_STYLES). Legend now lists only displayed node tables and rels, re-rendered per redraw; help text trimmed to essentials. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
parent
65d8402953
commit
7aa8e4c19c
@ -166,13 +166,9 @@ Graph Console
|
|||||||
</label>
|
</label>
|
||||||
</legend>
|
</legend>
|
||||||
<desc>
|
<desc>
|
||||||
Renders the in-memory Ladybug graph with AntV G6 (CDN). Node color
|
Live view of the in-memory Ladybug graph (AntV G6); the legend
|
||||||
and glyph encode the node table; edges are <code>IsChildOf</code>
|
shows what is displayed. Double-click folds containers when
|
||||||
(arrow points to parent). With "fold containers" on, anything with
|
folding is on.
|
||||||
children (except the root) renders as a foldable box: double-click
|
|
||||||
to collapse or expand. Redraws automatically after live changes
|
|
||||||
(fold state survives redraws). Very large graphs need an explicit
|
|
||||||
"Render anyway".
|
|
||||||
</desc>
|
</desc>
|
||||||
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
<div id="graph-legend" style="font-size: 12px; margin: 4px 0;"></div>
|
||||||
<div id="graph-canvas"
|
<div id="graph-canvas"
|
||||||
@ -336,9 +332,19 @@ Graph Console
|
|||||||
"Circle": { color: "#1baf7a", glyph: "circle", size: 14 },
|
"Circle": { color: "#1baf7a", glyph: "circle", size: 14 },
|
||||||
"Path": { color: "#e34948", glyph: "triangle", size: 14 },
|
"Path": { color: "#e34948", glyph: "triangle", size: 14 },
|
||||||
"Text": { color: "#e87ba4", glyph: "circle", size: 14 },
|
"Text": { color: "#e87ba4", glyph: "circle", size: 14 },
|
||||||
"Image": { color: "#eda100", glyph: "star", size: 14 }
|
"Image": { color: "#eda100", glyph: "star", size: 14 },
|
||||||
|
"Component": { color: "#8250df", glyph: "diamond", size: 20 }
|
||||||
};
|
};
|
||||||
const FALLBACK_STYLE = { color: "#767470", glyph: "circle", size: 14 };
|
const FALLBACK_STYLE = { color: "#767470", glyph: "circle", size: 14 };
|
||||||
|
// Edge style keyed on rel (wire field since slice-3); future edge
|
||||||
|
// attributes can feed styling the same way. IsChildOf stays the
|
||||||
|
// recessive default; IsInstanceOf echoes the Component color, dashed
|
||||||
|
// (line style is the secondary encoding, color validated vs the gray).
|
||||||
|
const EDGE_STYLES = {
|
||||||
|
"IsChildOf": { stroke: "#b3b0a8" },
|
||||||
|
"IsInstanceOf": { stroke: "#8250df", lineDash: [4, 3] }
|
||||||
|
};
|
||||||
|
const FALLBACK_EDGE_STYLE = { stroke: "#b3b0a8" };
|
||||||
// Legend glyph characters mirroring the G6 node types above.
|
// Legend glyph characters mirroring the G6 node types above.
|
||||||
const GLYPH_CHARS = { diamond: "◆", rect: "■", hexagon: "⬢",
|
const GLYPH_CHARS = { diamond: "◆", rect: "■", hexagon: "⬢",
|
||||||
circle: "●", triangle: "▲", star: "★" };
|
circle: "●", triangle: "▲", star: "★" };
|
||||||
@ -397,6 +403,10 @@ Graph Console
|
|||||||
return NODE_STYLES[table] || FALLBACK_STYLE;
|
return NODE_STYLES[table] || FALLBACK_STYLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function edgeStyle(rel) {
|
||||||
|
return EDGE_STYLES[rel] || FALLBACK_EDGE_STYLE;
|
||||||
|
}
|
||||||
|
|
||||||
function comboIdFor(nodeId) {
|
function comboIdFor(nodeId) {
|
||||||
return "combo:" + nodeId;
|
return "combo:" + nodeId;
|
||||||
}
|
}
|
||||||
@ -441,9 +451,13 @@ Graph Console
|
|||||||
const edges = (data.edges || [])
|
const edges = (data.edges || [])
|
||||||
.slice()
|
.slice()
|
||||||
.sort(function (a, b) { return (a.position || 0) - (b.position || 0); });
|
.sort(function (a, b) { return (a.position || 0) - (b.position || 0); });
|
||||||
|
// Hierarchy (tree ranking, combo derivation, fold-ability) comes from
|
||||||
|
// IsChildOf only — still a tree by construction. Other rels
|
||||||
|
// (IsInstanceOf, …) are overlay edges drawn between positioned nodes.
|
||||||
const parentOf = {};
|
const parentOf = {};
|
||||||
const childrenOf = {};
|
const childrenOf = {};
|
||||||
edges.forEach(function (e) {
|
edges.forEach(function (e) {
|
||||||
|
if ((e.rel || "IsChildOf") !== "IsChildOf") return;
|
||||||
parentOf[e.source] = e.target;
|
parentOf[e.source] = e.target;
|
||||||
(childrenOf[e.target] = childrenOf[e.target] || []).push(e.source);
|
(childrenOf[e.target] = childrenOf[e.target] || []).push(e.source);
|
||||||
});
|
});
|
||||||
@ -484,7 +498,8 @@ Graph Console
|
|||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
const g6edges = edges.map(function (e) {
|
const g6edges = edges.map(function (e) {
|
||||||
return { source: e.source, target: e.target, data: { position: e.position } };
|
return { source: e.source, target: e.target,
|
||||||
|
data: { position: e.position, rel: e.rel || "IsChildOf" } };
|
||||||
});
|
});
|
||||||
return { nodes: g6nodes, edges: g6edges, combos: combos };
|
return { nodes: g6nodes, edges: g6edges, combos: combos };
|
||||||
}
|
}
|
||||||
@ -538,7 +553,12 @@ Graph Console
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
edge: {
|
edge: {
|
||||||
style: { stroke: "#b3b0a8", endArrow: true, endArrowSize: 6 }
|
style: {
|
||||||
|
stroke: function (d) { return edgeStyle(d.data.rel).stroke; },
|
||||||
|
lineDash: function (d) { return edgeStyle(d.data.rel).lineDash; },
|
||||||
|
endArrow: true,
|
||||||
|
endArrowSize: 6
|
||||||
|
}
|
||||||
},
|
},
|
||||||
combo: {
|
combo: {
|
||||||
type: "rect",
|
type: "rect",
|
||||||
@ -583,17 +603,39 @@ Graph Console
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGraphLegend() {
|
// Hand-rolled on purpose: the G6 legend plugin substitutes its own
|
||||||
|
// marker set (hexagon→circle, star→cross), breaking glyph identity.
|
||||||
|
// Entries reflect the *displayed* data only: node tables in roster
|
||||||
|
// order (unknown tables appended with the fallback style), then rels.
|
||||||
|
function renderGraphLegend(data) {
|
||||||
const el = document.getElementById("graph-legend");
|
const el = document.getElementById("graph-legend");
|
||||||
el.innerHTML = Object.keys(NODE_STYLES).map(function (table) {
|
if (!el) return;
|
||||||
const s = NODE_STYLES[table];
|
const tables = {};
|
||||||
|
(data.nodes || []).forEach(function (n) { tables[n.table] = true; });
|
||||||
|
const rels = {};
|
||||||
|
(data.edges || []).forEach(function (e) { rels[e.rel || "IsChildOf"] = true; });
|
||||||
|
function nodeItem(table, s) {
|
||||||
return '<span style="margin-right: 1em; white-space: nowrap;">'
|
return '<span style="margin-right: 1em; white-space: nowrap;">'
|
||||||
+ '<span style="color: ' + s.color + '; font-size: 14px;">'
|
+ '<span style="color: ' + s.color + '; font-size: 14px;">'
|
||||||
+ (GLYPH_CHARS[s.glyph] || "●") + '</span> '
|
+ (GLYPH_CHARS[s.glyph] || "●") + '</span> '
|
||||||
+ escapeHtml(table) + "</span>";
|
+ escapeHtml(table) + "</span>";
|
||||||
// join with a space: the spans are nowrap, so the separator is the
|
}
|
||||||
// only soft-wrap opportunity in the legend row
|
const items = Object.keys(NODE_STYLES)
|
||||||
}).join(" ");
|
.filter(function (t) { return tables[t]; })
|
||||||
|
.map(function (t) { return nodeItem(t, NODE_STYLES[t]); });
|
||||||
|
Object.keys(tables).sort().forEach(function (t) {
|
||||||
|
if (!NODE_STYLES[t]) items.push(nodeItem(t, FALLBACK_STYLE));
|
||||||
|
});
|
||||||
|
Object.keys(rels).sort().forEach(function (rel) {
|
||||||
|
const s = edgeStyle(rel);
|
||||||
|
items.push('<span style="margin-right: 1em; white-space: nowrap;">'
|
||||||
|
+ '<span style="color: ' + s.stroke + '; font-size: 14px;">'
|
||||||
|
+ (s.lineDash ? "⇢" : "→") + '</span> '
|
||||||
|
+ escapeHtml(rel) + "</span>");
|
||||||
|
});
|
||||||
|
// join with a space: the spans are nowrap, so the separator is the
|
||||||
|
// only soft-wrap opportunity in the legend row
|
||||||
|
el.innerHTML = items.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphStatusText(data) {
|
function graphStatusText(data) {
|
||||||
@ -634,6 +676,7 @@ Graph Console
|
|||||||
function renderCurrent() {
|
function renderCurrent() {
|
||||||
if (!lastGraphData) return;
|
if (!lastGraphData) return;
|
||||||
const data = filteredGraphData();
|
const data = filteredGraphData();
|
||||||
|
renderGraphLegend(data);
|
||||||
const anywayBtn = document.getElementById("graph-render-anyway");
|
const anywayBtn = document.getElementById("graph-render-anyway");
|
||||||
const filterBtn = document.getElementById("graph-filter-reset");
|
const filterBtn = document.getElementById("graph-filter-reset");
|
||||||
if (filterBtn) filterBtn.style.display = graphFilterIds ? "inline" : "none";
|
if (filterBtn) filterBtn.style.display = graphFilterIds ? "inline" : "none";
|
||||||
@ -1072,7 +1115,6 @@ Graph Console
|
|||||||
|
|
||||||
connect();
|
connect();
|
||||||
refreshSyncStatus();
|
refreshSyncStatus();
|
||||||
renderGraphLegend();
|
|
||||||
refetchGraph();
|
refetchGraph();
|
||||||
|
|
||||||
window.addEventListener("beforeunload", function () {
|
window.addEventListener("beforeunload", function () {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user