Merge branch 'dev'
This commit is contained in:
commit
c675025f63
|
|
@ -18,7 +18,7 @@ interface GraphNode {
|
|||
interface GraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
type: "work_at" | "point_of_contact" | "collaborates";
|
||||
type: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ class FolkGraphViewer extends HTMLElement {
|
|||
connectedCallback() {
|
||||
this.space = this.getAttribute("space") || "demo";
|
||||
if (this.space === "demo") { this.loadDemoData(); return; }
|
||||
this.loadData();
|
||||
this.render();
|
||||
this.render(); // Show loading state
|
||||
this.loadData(); // Async — will re-render when data arrives
|
||||
}
|
||||
|
||||
private loadDemoData() {
|
||||
|
|
@ -114,16 +114,75 @@ class FolkGraphViewer extends HTMLElement {
|
|||
private async loadData() {
|
||||
const base = this.getApiBase();
|
||||
try {
|
||||
const [wsRes, infoRes] = await Promise.all([
|
||||
const [wsRes, infoRes, graphRes] = await Promise.all([
|
||||
fetch(`${base}/api/workspaces`),
|
||||
fetch(`${base}/api/info`),
|
||||
fetch(`${base}/api/graph`),
|
||||
]);
|
||||
if (wsRes.ok) this.workspaces = await wsRes.json();
|
||||
if (infoRes.ok) this.info = await infoRes.json();
|
||||
if (graphRes.ok) {
|
||||
const graph = await graphRes.json();
|
||||
this.importGraph(graph);
|
||||
}
|
||||
} catch { /* offline */ }
|
||||
this.render();
|
||||
}
|
||||
|
||||
/** Map server /api/graph response to client GraphNode/GraphEdge format */
|
||||
private importGraph(graph: { nodes?: any[]; edges?: any[] }) {
|
||||
if (!graph.nodes?.length) return;
|
||||
|
||||
// Build company name lookup for workspace assignment
|
||||
const companyNames = new Map<string, string>();
|
||||
for (const n of graph.nodes) {
|
||||
if (n.type === "company") companyNames.set(n.id, n.label || n.name || "Unknown");
|
||||
}
|
||||
|
||||
// Build person→company lookup from edges
|
||||
const personCompany = new Map<string, string>();
|
||||
for (const e of graph.edges || []) {
|
||||
if (e.type === "works_at") personCompany.set(e.source, e.target);
|
||||
}
|
||||
|
||||
// Edge type normalization: server → client
|
||||
const edgeTypeMap: Record<string, GraphEdge["type"]> = {
|
||||
works_at: "work_at",
|
||||
contact_of: "point_of_contact",
|
||||
involved_in: "point_of_contact",
|
||||
involves: "collaborates",
|
||||
};
|
||||
|
||||
this.nodes = graph.nodes.map((n: any) => {
|
||||
const name = n.label || n.name || "Unknown";
|
||||
const companyId = personCompany.get(n.id);
|
||||
const workspace = n.type === "company" ? name : (companyId ? companyNames.get(companyId) || "" : "");
|
||||
return {
|
||||
id: n.id,
|
||||
name,
|
||||
type: n.type,
|
||||
workspace,
|
||||
role: n.data?.role,
|
||||
location: n.data?.location,
|
||||
description: n.data?.email || n.data?.domain || n.data?.stage,
|
||||
} as GraphNode;
|
||||
});
|
||||
|
||||
this.edges = (graph.edges || []).map((e: any) => ({
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
type: edgeTypeMap[e.type] || e.type,
|
||||
label: e.label,
|
||||
} as GraphEdge));
|
||||
|
||||
// Update info stats from actual data
|
||||
this.info = {
|
||||
...this.info,
|
||||
member_count: this.nodes.filter(n => n.type === "person").length,
|
||||
company_count: this.nodes.filter(n => n.type === "company").length,
|
||||
};
|
||||
}
|
||||
|
||||
private getFilteredNodes(): GraphNode[] {
|
||||
let filtered = this.nodes;
|
||||
if (this.filter !== "all") {
|
||||
|
|
@ -145,18 +204,23 @@ class FolkGraphViewer extends HTMLElement {
|
|||
private computeForceLayout(nodes: GraphNode[], edges: GraphEdge[], W: number, H: number): Record<string, { x: number; y: number }> {
|
||||
const pos: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
// Initial positions: orgs in triangle, people around their org
|
||||
const orgCenters: Record<string, { x: number; y: number }> = {
|
||||
"org-1": { x: W / 2, y: 120 },
|
||||
"org-2": { x: 160, y: 380 },
|
||||
"org-3": { x: W - 160, y: 380 },
|
||||
};
|
||||
const orgNameToId: Record<string, string> = {
|
||||
"Commons DAO": "org-1", "Mycelial Lab": "org-2", "Regenerative Fund": "org-3",
|
||||
};
|
||||
// Collect company nodes and position them evenly around center
|
||||
const companies = nodes.filter(n => n.type === "company");
|
||||
const orgCenters: Record<string, { x: number; y: number }> = {};
|
||||
const cx = W / 2, cy = H / 2;
|
||||
const orbitR = Math.min(W, H) * 0.3;
|
||||
|
||||
companies.forEach((org, i) => {
|
||||
const angle = -Math.PI / 2 + (2 * Math.PI * i) / Math.max(companies.length, 1);
|
||||
orgCenters[org.id] = { x: cx + orbitR * Math.cos(angle), y: cy + orbitR * Math.sin(angle) };
|
||||
});
|
||||
|
||||
for (const [id, p] of Object.entries(orgCenters)) pos[id] = { ...p };
|
||||
|
||||
// Build workspace→orgId lookup from actual data
|
||||
const orgNameToId: Record<string, string> = {};
|
||||
for (const org of companies) orgNameToId[org.name] = org.id;
|
||||
|
||||
const peopleByOrg: Record<string, GraphNode[]> = {};
|
||||
for (const n of nodes) {
|
||||
if (n.type === "person") {
|
||||
|
|
@ -167,8 +231,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
for (const [oid, people] of Object.entries(peopleByOrg)) {
|
||||
const c = orgCenters[oid];
|
||||
if (!c) continue;
|
||||
const gcx = W / 2, gcy = 250;
|
||||
const base = Math.atan2(c.y - gcy, c.x - gcx);
|
||||
const base = Math.atan2(c.y - cy, c.x - cx);
|
||||
const spread = Math.PI * 0.8;
|
||||
people.forEach((p, i) => {
|
||||
const angle = base - spread / 2 + (spread * i) / Math.max(people.length - 1, 1);
|
||||
|
|
@ -176,6 +239,13 @@ class FolkGraphViewer extends HTMLElement {
|
|||
});
|
||||
}
|
||||
|
||||
// Position any remaining nodes (opportunities, unlinked people) randomly near center
|
||||
for (const n of nodes) {
|
||||
if (!pos[n.id]) {
|
||||
pos[n.id] = { x: cx + (Math.random() - 0.5) * W * 0.4, y: cy + (Math.random() - 0.5) * H * 0.4 };
|
||||
}
|
||||
}
|
||||
|
||||
// Run force iterations
|
||||
const allIds = nodes.map(n => n.id).filter(id => pos[id]);
|
||||
for (let iter = 0; iter < 80; iter++) {
|
||||
|
|
@ -289,17 +359,17 @@ class FolkGraphViewer extends HTMLElement {
|
|||
// Force-directed layout
|
||||
const positions = this.computeForceLayout(this.nodes, this.edges, W, H);
|
||||
|
||||
// Org colors
|
||||
const orgColors: Record<string, string> = {
|
||||
"org-1": "#6366f1", "org-2": "#22c55e", "org-3": "#f59e0b",
|
||||
};
|
||||
// Assign colors to companies dynamically
|
||||
const palette = ["#6366f1", "#22c55e", "#f59e0b", "#ec4899", "#14b8a6", "#f97316", "#8b5cf6", "#06b6d4"];
|
||||
const companies = this.nodes.filter(n => n.type === "company");
|
||||
const orgColors: Record<string, string> = {};
|
||||
companies.forEach((org, i) => { orgColors[org.id] = palette[i % palette.length]; });
|
||||
|
||||
// Cluster backgrounds based on computed positions
|
||||
const orgIds = ["org-1", "org-2", "org-3"];
|
||||
const clustersSvg = orgIds.map(orgId => {
|
||||
const pos = positions[orgId];
|
||||
const clustersSvg = companies.map(org => {
|
||||
const pos = positions[org.id];
|
||||
if (!pos) return "";
|
||||
const color = orgColors[orgId] || "#333";
|
||||
const color = orgColors[org.id] || "#333";
|
||||
return `<circle cx="${pos.x}" cy="${pos.y}" r="140" fill="${color}" opacity="0.04" stroke="${color}" stroke-width="1" stroke-opacity="0.15" stroke-dasharray="4 4"/>`;
|
||||
}).join("");
|
||||
|
||||
|
|
@ -320,6 +390,11 @@ class FolkGraphViewer extends HTMLElement {
|
|||
if (edge.label) {
|
||||
edgesSvg.push(`<text x="${mx}" y="${my - 6}" fill="#c084fc" font-size="8" text-anchor="middle" opacity="0.7">${this.esc(edge.label)}</text>`);
|
||||
}
|
||||
} else if (edge.type === "collaborates") {
|
||||
edgesSvg.push(`<line x1="${sp.x}" y1="${sp.y}" x2="${tp.x}" y2="${tp.y}" stroke="#f59e0b" stroke-width="1" stroke-dasharray="3 3" opacity="0.4"/>`);
|
||||
} else {
|
||||
// Fallback for any unknown edge type
|
||||
edgesSvg.push(`<line x1="${sp.x}" y1="${sp.y}" x2="${tp.x}" y2="${tp.y}" stroke="#555" stroke-width="1" opacity="0.25"/>`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue