Merge branch 'dev'
This commit is contained in:
commit
c1b572ef68
|
|
@ -15,14 +15,20 @@ interface WeightAccounting {
|
||||||
interface GraphNode {
|
interface GraphNode {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: "person" | "company" | "opportunity" | "rspace_user" | "space";
|
type: "person" | "company" | "opportunity" | "rspace_user" | "space" | "module" | "feed";
|
||||||
workspace: string;
|
workspace: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
trustScore?: number;
|
trustScore?: number;
|
||||||
delegatedWeight?: number; // 0-1 normalized, computed from delegation edges
|
delegatedWeight?: number; // 0-1 normalized, computed from delegation edges
|
||||||
|
baseWeights?: Record<string, number>; // per-authority base weights (0-1 scale from server)
|
||||||
weightAccounting?: WeightAccounting;
|
weightAccounting?: WeightAccounting;
|
||||||
|
// Layers mode fields
|
||||||
|
layerId?: string;
|
||||||
|
feedId?: string;
|
||||||
|
feedKind?: string; // FlowKind
|
||||||
|
moduleColor?: number;
|
||||||
// 3d-force-graph internal properties
|
// 3d-force-graph internal properties
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
|
|
@ -39,6 +45,30 @@ interface GraphEdge {
|
||||||
label?: string;
|
label?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
authority?: string;
|
authority?: string;
|
||||||
|
flowKind?: string; // FlowKind, for cross_layer_flow edges
|
||||||
|
strength?: number; // 0-1, for cross_layer_flow edges
|
||||||
|
}
|
||||||
|
|
||||||
|
type AxisPlane = "xy" | "xz" | "yz";
|
||||||
|
|
||||||
|
interface LayerInstance {
|
||||||
|
moduleId: string;
|
||||||
|
moduleName: string;
|
||||||
|
moduleIcon: string;
|
||||||
|
moduleColor: number;
|
||||||
|
axis: AxisPlane;
|
||||||
|
feeds: Array<{ id: string; name: string; kind: string }>;
|
||||||
|
acceptsFeeds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CrossLayerFlow {
|
||||||
|
id: string;
|
||||||
|
sourceLayerIdx: number;
|
||||||
|
sourceFeedId: string;
|
||||||
|
targetLayerIdx: number;
|
||||||
|
targetFeedId: string;
|
||||||
|
kind: string; // FlowKind
|
||||||
|
strength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DELEGATION_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const;
|
const DELEGATION_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const;
|
||||||
|
|
@ -70,6 +100,17 @@ const NODE_COLORS: Record<string, number> = {
|
||||||
|
|
||||||
const COMPANY_PALETTE = [0x6366f1, 0x22c55e, 0xf59e0b, 0xec4899, 0x14b8a6, 0xf97316, 0x8b5cf6, 0x06b6d4];
|
const COMPANY_PALETTE = [0x6366f1, 0x22c55e, 0xf59e0b, 0xec4899, 0x14b8a6, 0xf97316, 0x8b5cf6, 0x06b6d4];
|
||||||
|
|
||||||
|
// Layer/Flow constants (mirrored from lib/layer-types.ts for browser use)
|
||||||
|
const LAYER_FLOW_COLORS: Record<string, string> = {
|
||||||
|
economic: "#4ade80", trust: "#c4b5fd", data: "#60a5fa",
|
||||||
|
attention: "#fcd34d", governance: "#a78bfa", resource: "#6ee7b7", custom: "#94a3b8",
|
||||||
|
};
|
||||||
|
const LAYER_FLOW_LABELS: Record<string, string> = {
|
||||||
|
economic: "Economic", trust: "Trust", data: "Data",
|
||||||
|
attention: "Attention", governance: "Delegation", resource: "Resource", custom: "Custom",
|
||||||
|
};
|
||||||
|
const MODULE_PALETTE = [0x6366f1, 0x10b981, 0xf59e0b, 0xec4899, 0x14b8a6, 0xf97316, 0x8b5cf6, 0x06b6d4];
|
||||||
|
|
||||||
// Edge colors/widths by type
|
// Edge colors/widths by type
|
||||||
const EDGE_STYLES: Record<string, { color: string; width: number; opacity: number; dashed?: boolean }> = {
|
const EDGE_STYLES: Record<string, { color: string; width: number; opacity: number; dashed?: boolean }> = {
|
||||||
work_at: { color: "#888888", width: 0.5, opacity: 0.35 },
|
work_at: { color: "#888888", width: 0.5, opacity: 0.35 },
|
||||||
|
|
@ -99,6 +140,16 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
private demoDelegations: GraphEdge[] = [];
|
private demoDelegations: GraphEdge[] = [];
|
||||||
private showMemberList = false;
|
private showMemberList = false;
|
||||||
|
|
||||||
|
// Layers mode state
|
||||||
|
private layersMode = false;
|
||||||
|
private layerInstances: LayerInstance[] = [];
|
||||||
|
private crossLayerFlows: CrossLayerFlow[] = [];
|
||||||
|
private flowWiringSource: { layerIdx: number; feedId: string; feedKind: string } | null = null;
|
||||||
|
private layersPanelOpen = false;
|
||||||
|
private layerPlaneObjects: any[] = [];
|
||||||
|
private savedGraphState: { nodes: GraphNode[]; edges: GraphEdge[] } | null = null;
|
||||||
|
private savedCameraControls: { minPolar: number; maxPolar: number; minDist: number; maxDist: number } | null = null;
|
||||||
|
|
||||||
// Multi-select delegation state
|
// Multi-select delegation state
|
||||||
private selectedDelegates: Map<string, { node: GraphNode; weights: Record<string, number> }> = new Map();
|
private selectedDelegates: Map<string, { node: GraphNode; weights: Record<string, number> }> = new Map();
|
||||||
private delegateSearchQuery = "";
|
private delegateSearchQuery = "";
|
||||||
|
|
@ -198,6 +249,7 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
location: n.data?.location,
|
location: n.data?.location,
|
||||||
description: n.data?.email || n.data?.domain || n.data?.stage,
|
description: n.data?.email || n.data?.domain || n.data?.stage,
|
||||||
trustScore: n.data?.trustScore,
|
trustScore: n.data?.trustScore,
|
||||||
|
baseWeights: n.data?.trustScores || {},
|
||||||
} as GraphNode;
|
} as GraphNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -230,12 +282,20 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
if (sAcct) sAcct.delegatedAway[e.authority] = (sAcct.delegatedAway[e.authority] || 0) + w;
|
if (sAcct) sAcct.delegatedAway[e.authority] = (sAcct.delegatedAway[e.authority] || 0) + w;
|
||||||
if (tAcct) tAcct.receivedWeight[e.authority] = (tAcct.receivedWeight[e.authority] || 0) + w;
|
if (tAcct) tAcct.receivedWeight[e.authority] = (tAcct.receivedWeight[e.authority] || 0) + w;
|
||||||
}
|
}
|
||||||
acctMap.forEach((acct) => {
|
// Effective weight = retained base + received delegations (absolute tokens, ×100)
|
||||||
|
const nodeById = new Map(this.nodes.map(n => [n.id, n]));
|
||||||
|
acctMap.forEach((acct, nodeId) => {
|
||||||
|
const node = nodeById.get(nodeId);
|
||||||
for (const a of authorities) {
|
for (const a of authorities) {
|
||||||
acct.effectiveWeight[a] = acct.receivedWeight[a] + Math.max(0, 1 - acct.delegatedAway[a]);
|
const base = (node?.baseWeights?.[a] || 0) * 100;
|
||||||
|
const away = acct.delegatedAway[a] * 100;
|
||||||
|
const recv = acct.receivedWeight[a] * 100;
|
||||||
|
acct.delegatedAway[a] = away;
|
||||||
|
acct.receivedWeight[a] = recv;
|
||||||
|
acct.effectiveWeight[a] = Math.max(0, base - away) + recv;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Compute normalized delegatedWeight (for backwards compat) and attach accounting
|
// Attach accounting to nodes
|
||||||
let maxEW = 0;
|
let maxEW = 0;
|
||||||
acctMap.forEach((acct) => {
|
acctMap.forEach((acct) => {
|
||||||
for (const a of authorities) maxEW = Math.max(maxEW, acct.effectiveWeight[a]);
|
for (const a of authorities) maxEW = Math.max(maxEW, acct.effectiveWeight[a]);
|
||||||
|
|
@ -245,8 +305,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
const acct = acctMap.get(node.id);
|
const acct = acctMap.get(node.id);
|
||||||
if (acct) {
|
if (acct) {
|
||||||
node.weightAccounting = acct;
|
node.weightAccounting = acct;
|
||||||
const avgEW = authorities.reduce((s, a) => s + acct.effectiveWeight[a], 0) / authorities.length;
|
const totalEW = authorities.reduce((s, a) => s + acct.effectiveWeight[a], 0);
|
||||||
node.delegatedWeight = avgEW / maxEW;
|
node.delegatedWeight = totalEW / (maxEW * 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,7 +460,7 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
.member-item:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.04)); }
|
.member-item:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.04)); }
|
||||||
.member-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
.member-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.member-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.member-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.member-weight { font-size: 10px; color: var(--rs-text-muted); font-weight: 600; min-width: 28px; text-align: right; }
|
.member-weight { font-size: 10px; font-weight: 600; min-width: 70px; text-align: right; white-space: nowrap; }
|
||||||
|
|
||||||
.zoom-controls {
|
.zoom-controls {
|
||||||
position: absolute; bottom: 12px; right: 12px;
|
position: absolute; bottom: 12px; right: 12px;
|
||||||
|
|
@ -545,6 +605,88 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
.deleg-remaining { font-size: 11px; color: var(--rs-text-muted); margin-top: 6px; }
|
.deleg-remaining { font-size: 11px; color: var(--rs-text-muted); margin-top: 6px; }
|
||||||
.deleg-auth-label { font-size: 9px; font-weight: 600; min-width: 30px; text-align: center; }
|
.deleg-auth-label { font-size: 9px; font-weight: 600; min-width: 30px; text-align: center; }
|
||||||
|
|
||||||
|
/* ── Layers panel ── */
|
||||||
|
.layers-panel {
|
||||||
|
display: none; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
|
||||||
|
border-radius: 12px; padding: 12px 16px; margin-top: 8px;
|
||||||
|
}
|
||||||
|
.layers-panel.visible { display: block; }
|
||||||
|
.layers-panel-header {
|
||||||
|
display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.layers-panel-title { font-size: 13px; font-weight: 600; flex: 1; }
|
||||||
|
.layers-panel-close {
|
||||||
|
background: none; border: none; color: var(--rs-text-muted); cursor: pointer; font-size: 14px;
|
||||||
|
}
|
||||||
|
.module-picker {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.module-pick-btn {
|
||||||
|
padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-input-border);
|
||||||
|
background: var(--rs-input-bg); color: var(--rs-text-muted); cursor: pointer;
|
||||||
|
font-size: 11px; display: flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.module-pick-btn:hover { border-color: var(--rs-border-strong); }
|
||||||
|
.module-pick-btn.selected { border-color: var(--rs-primary-hover); color: var(--rs-primary-hover); }
|
||||||
|
.module-pick-btn.disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
.layer-row {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 6px 0;
|
||||||
|
border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.06));
|
||||||
|
}
|
||||||
|
.layer-row:last-child { border-bottom: none; }
|
||||||
|
.layer-row-icon { font-size: 16px; }
|
||||||
|
.layer-row-name { font-size: 12px; font-weight: 500; flex: 1; }
|
||||||
|
.axis-select {
|
||||||
|
padding: 3px 6px; border-radius: 4px; border: 1px solid var(--rs-input-border);
|
||||||
|
background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 11px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.layer-row-remove {
|
||||||
|
background: none; border: none; color: var(--rs-text-muted); cursor: pointer;
|
||||||
|
font-size: 12px; padding: 2px 4px; border-radius: 4px;
|
||||||
|
}
|
||||||
|
.layer-row-remove:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||||
|
.layer-feeds-hint { font-size: 10px; color: var(--rs-text-muted); margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ── Flow dialog overlay ── */
|
||||||
|
.flow-dialog-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 100;
|
||||||
|
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.flow-dialog {
|
||||||
|
background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
|
||||||
|
border-radius: 12px; padding: 20px; min-width: 300px; max-width: 400px;
|
||||||
|
}
|
||||||
|
.flow-dialog-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
||||||
|
.flow-dialog-pair { font-size: 12px; color: var(--rs-text-muted); margin-bottom: 10px; }
|
||||||
|
.flow-kind-option {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 6px 10px;
|
||||||
|
border-radius: 6px; cursor: pointer; margin-bottom: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.flow-kind-option:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.04)); }
|
||||||
|
.flow-kind-option.selected { border-color: var(--rs-primary-hover); }
|
||||||
|
.flow-kind-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||||
|
.flow-kind-label { font-size: 12px; flex: 1; }
|
||||||
|
.flow-strength-row { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
|
||||||
|
.flow-strength-label { font-size: 11px; color: var(--rs-text-muted); min-width: 60px; }
|
||||||
|
.flow-strength-slider { flex: 1; accent-color: #a78bfa; }
|
||||||
|
.flow-strength-val { font-size: 11px; font-weight: 600; min-width: 30px; text-align: right; }
|
||||||
|
.flow-dialog-actions { display: flex; gap: 8px; margin-top: 14px; }
|
||||||
|
.flow-dialog-btn {
|
||||||
|
padding: 6px 16px; border: none; border-radius: 8px; cursor: pointer;
|
||||||
|
font-size: 12px; font-weight: 600; flex: 1;
|
||||||
|
}
|
||||||
|
.flow-dialog-create { background: #a78bfa; color: #fff; }
|
||||||
|
.flow-dialog-create:hover { background: #8b5cf6; }
|
||||||
|
.flow-dialog-cancel { background: var(--rs-bg-surface-raised, rgba(255,255,255,0.08)); color: var(--rs-text-primary); }
|
||||||
|
.flow-dialog-cancel:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.12)); }
|
||||||
|
|
||||||
|
/* Wiring pulse animation for compatible feed targets */
|
||||||
|
@keyframes feed-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 4px 1px currentColor; }
|
||||||
|
50% { box-shadow: 0 0 12px 4px currentColor; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.graph-row { flex-direction: column; }
|
.graph-row { flex-direction: column; }
|
||||||
.graph-canvas { min-height: 300px; }
|
.graph-canvas { min-height: 300px; }
|
||||||
|
|
@ -566,6 +708,7 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
<button class="filter-btn" id="trust-toggle" title="Toggle trust-weighted view">Trust</button>
|
<button class="filter-btn" id="trust-toggle" title="Toggle trust-weighted view">Trust</button>
|
||||||
<button class="filter-btn" id="rings-toggle" title="Toggle concentric ring layout">Rings</button>
|
<button class="filter-btn" id="rings-toggle" title="Toggle concentric ring layout">Rings</button>
|
||||||
<button class="filter-btn" id="list-toggle" title="Toggle member list sidebar">List</button>
|
<button class="filter-btn" id="list-toggle" title="Toggle member list sidebar">List</button>
|
||||||
|
<button class="filter-btn" id="layers-toggle" title="Toggle multi-layer rApp visualization">Layers</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="authority-bar" id="authority-bar">
|
<div class="authority-bar" id="authority-bar">
|
||||||
|
|
@ -589,6 +732,11 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
<div class="detail-panel" id="detail-panel"></div>
|
<div class="detail-panel" id="detail-panel"></div>
|
||||||
<div class="deleg-panel" id="deleg-panel"></div>
|
<div class="deleg-panel" id="deleg-panel"></div>
|
||||||
|
|
||||||
|
<div class="layers-panel" id="layers-panel"></div>
|
||||||
|
<div class="flow-dialog-overlay" id="flow-dialog-overlay" style="display:none">
|
||||||
|
<div class="flow-dialog" id="flow-dialog"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="legend" id="legend">
|
<div class="legend" id="legend">
|
||||||
<div class="legend-item"><span class="legend-dot dot-person"></span> People</div>
|
<div class="legend-item"><span class="legend-dot dot-person"></span> People</div>
|
||||||
<div class="legend-item"><span class="legend-dot dot-company"></span> Organizations</div>
|
<div class="legend-item"><span class="legend-dot dot-company"></span> Organizations</div>
|
||||||
|
|
@ -705,6 +853,17 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Layers toggle
|
||||||
|
this.shadow.getElementById("layers-toggle")?.addEventListener("click", () => {
|
||||||
|
if (this.layersMode) {
|
||||||
|
this.exitLayersMode();
|
||||||
|
} else {
|
||||||
|
this.enterLayersMode();
|
||||||
|
}
|
||||||
|
const btn = this.shadow.getElementById("layers-toggle");
|
||||||
|
if (btn) btn.classList.toggle("active", this.layersMode);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private animateCameraDistance(targetDist: number) {
|
private animateCameraDistance(targetDist: number) {
|
||||||
|
|
@ -965,7 +1124,7 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
let badgeColor = "#7c3aed";
|
let badgeColor = "#7c3aed";
|
||||||
if (this.trustMode && node.weightAccounting && this.authority !== "all") {
|
if (this.trustMode && node.weightAccounting && this.authority !== "all") {
|
||||||
const ew = node.weightAccounting.effectiveWeight[this.authority] || 0;
|
const ew = node.weightAccounting.effectiveWeight[this.authority] || 0;
|
||||||
badgeText = ew.toFixed(1);
|
badgeText = String(Math.round(ew));
|
||||||
badgeColor = AUTHORITY_COLORS[this.authority] || "#7c3aed";
|
badgeColor = AUTHORITY_COLORS[this.authority] || "#7c3aed";
|
||||||
} else {
|
} else {
|
||||||
const trust = this.getTrustScore(node.id);
|
const trust = this.getTrustScore(node.id);
|
||||||
|
|
@ -1251,10 +1410,23 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
let weightHtml = "";
|
let weightHtml = "";
|
||||||
if (n.weightAccounting && (n.type === "rspace_user" || n.type === "person")) {
|
if (n.weightAccounting && (n.type === "rspace_user" || n.type === "person")) {
|
||||||
const acct = n.weightAccounting;
|
const acct = n.weightAccounting;
|
||||||
|
// Find max effective weight across all nodes for bar scaling
|
||||||
|
let detailMaxEW = 1;
|
||||||
|
for (const nd of this.nodes) {
|
||||||
|
if (!nd.weightAccounting) continue;
|
||||||
|
for (const a of DELEGATION_AUTHORITIES) {
|
||||||
|
detailMaxEW = Math.max(detailMaxEW, nd.weightAccounting.effectiveWeight[a] || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
weightHtml = DELEGATION_AUTHORITIES.map(a => {
|
weightHtml = DELEGATION_AUTHORITIES.map(a => {
|
||||||
const ew = Math.round((acct.effectiveWeight[a] || 0) * 100) / 100;
|
const ew = Math.round(acct.effectiveWeight[a] || 0);
|
||||||
|
const base = Math.round((n.baseWeights?.[a] || 0) * 100);
|
||||||
|
const recv = Math.round(acct.receivedWeight[a] || 0);
|
||||||
|
const away = Math.round(acct.delegatedAway[a] || 0);
|
||||||
const disp = AUTHORITY_DISPLAY[a];
|
const disp = AUTHORITY_DISPLAY[a];
|
||||||
return `<div class="detail-trust"><span class="trust-label" style="color:${disp?.color || '#a78bfa'}">${disp?.label || a}</span><span class="trust-bar"><span class="trust-fill" style="width:${Math.min(ew * 50, 100)}%;background:${disp?.color || '#a78bfa'}"></span></span><span class="trust-val" style="color:${disp?.color || '#a78bfa'}">${ew.toFixed(2)}</span></div>`;
|
const barPct = Math.min((ew / detailMaxEW) * 100, 100);
|
||||||
|
return `<div class="detail-trust"><span class="trust-label" style="color:${disp?.color || '#a78bfa'}">${disp?.label || a}</span><span class="trust-bar"><span class="trust-fill" style="width:${barPct}%;background:${disp?.color || '#a78bfa'}"></span></span><span class="trust-val" style="color:${disp?.color || '#a78bfa'}">${ew}</span></div>
|
||||||
|
<div style="font-size:10px;color:var(--rs-text-muted);margin:-4px 0 6px 76px">base ${base} − ${away} delegated + ${recv} received</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1529,9 +1701,17 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
if (sAcct) sAcct.delegatedAway[e.authority] = (sAcct.delegatedAway[e.authority] || 0) + w;
|
if (sAcct) sAcct.delegatedAway[e.authority] = (sAcct.delegatedAway[e.authority] || 0) + w;
|
||||||
if (tAcct) tAcct.receivedWeight[e.authority] = (tAcct.receivedWeight[e.authority] || 0) + w;
|
if (tAcct) tAcct.receivedWeight[e.authority] = (tAcct.receivedWeight[e.authority] || 0) + w;
|
||||||
}
|
}
|
||||||
acctMap.forEach((acct) => {
|
// Absolute token weights: base×100 - delegated + received
|
||||||
|
const nodeById2 = new Map(this.nodes.map(n => [n.id, n]));
|
||||||
|
acctMap.forEach((acct, nodeId) => {
|
||||||
|
const node = nodeById2.get(nodeId);
|
||||||
for (const a of authorities) {
|
for (const a of authorities) {
|
||||||
acct.effectiveWeight[a] = acct.receivedWeight[a] + Math.max(0, 1 - acct.delegatedAway[a]);
|
const base = (node?.baseWeights?.[a] || 0) * 100;
|
||||||
|
const away = acct.delegatedAway[a] * 100;
|
||||||
|
const recv = acct.receivedWeight[a] * 100;
|
||||||
|
acct.delegatedAway[a] = away;
|
||||||
|
acct.receivedWeight[a] = recv;
|
||||||
|
acct.effectiveWeight[a] = Math.max(0, base - away) + recv;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let maxEW = 0;
|
let maxEW = 0;
|
||||||
|
|
@ -1543,8 +1723,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
const acct = acctMap.get(node.id);
|
const acct = acctMap.get(node.id);
|
||||||
if (acct) {
|
if (acct) {
|
||||||
node.weightAccounting = acct;
|
node.weightAccounting = acct;
|
||||||
const avgEW = authorities.reduce((s, a) => s + acct.effectiveWeight[a], 0) / authorities.length;
|
const totalEW = authorities.reduce((s, a) => s + acct.effectiveWeight[a], 0);
|
||||||
node.delegatedWeight = avgEW / maxEW;
|
node.delegatedWeight = totalEW / (maxEW * 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1580,18 +1760,27 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authHeaders = DELEGATION_AUTHORITIES.map(a => {
|
||||||
|
const d = AUTHORITY_DISPLAY[a];
|
||||||
|
return `<span style="color:${d?.color};font-size:9px;font-weight:700">${d?.label?.[0]}</span>`;
|
||||||
|
}).join(`<span style="opacity:0.3">/</span>`);
|
||||||
|
|
||||||
panel.innerHTML = groups.filter(g => g.nodes.length > 0).map(g => `
|
panel.innerHTML = groups.filter(g => g.nodes.length > 0).map(g => `
|
||||||
<div class="member-group">
|
<div class="member-group">
|
||||||
<div class="member-group-header" style="color:${g.color}">
|
<div class="member-group-header" style="color:${g.color}">
|
||||||
${g.label}
|
${g.label}
|
||||||
<span class="member-group-count">${g.nodes.length}</span>
|
<span style="display:flex;align-items:center;gap:2px"><span class="member-group-count">${g.nodes.length}</span><span class="member-weight" style="font-size:9px">${authHeaders}</span></span>
|
||||||
</div>
|
</div>
|
||||||
${g.nodes.map(n => {
|
${g.nodes.map(n => {
|
||||||
const ew = n.weightAccounting ? (Object.values(n.weightAccounting.effectiveWeight).reduce((s, v) => s + v, 0) / 3).toFixed(1) : "";
|
const wa = n.weightAccounting;
|
||||||
|
const weights = wa ? DELEGATION_AUTHORITIES.map(a => {
|
||||||
|
const disp = AUTHORITY_DISPLAY[a];
|
||||||
|
return `<span style="color:${disp?.color}">${Math.round(wa.effectiveWeight[a] || 0)}</span>`;
|
||||||
|
}).join(`<span style="color:var(--rs-text-muted);opacity:0.4">/</span>`) : "";
|
||||||
return `<div class="member-item" data-member-id="${n.id}">
|
return `<div class="member-item" data-member-id="${n.id}">
|
||||||
<span class="member-dot" style="background:${g.color}"></span>
|
<span class="member-dot" style="background:${g.color}"></span>
|
||||||
<span class="member-name" title="${this.esc(n.name)}">${this.esc(n.name)}</span>
|
<span class="member-name" title="${this.esc(n.name)}">${this.esc(n.name)}</span>
|
||||||
${ew ? `<span class="member-weight">${ew}</span>` : ""}
|
${weights ? `<span class="member-weight">${weights}</span>` : ""}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue