loopy-sustainability-model.html
1 --- 2 layout: default 3 title: LOOPY Sustainability Model 4 permalink: /models/loopy-sustainability-model/ 5 --- 6 7 # LOOPY Sustainability Model for #B4mad Industries 8 9 This interactive visualization represents the #B4mad economic sustainability model, inspired by the LOOPY tool. Explore the reinforcing and balancing feedback loops that govern the platform's viability. 10 11 <style> 12 body { font-family: sans-serif; margin: 20px; } 13 .node { 14 fill: #ADD8E6; /* Light blue */ 15 stroke: #3182bd; 16 stroke-width: 2px; 17 cursor: pointer; 18 } 19 .node-label { 20 font-size: 12px; 21 text-anchor: middle; 22 pointer-events: none; /* So clicks pass through to the circle */ 23 } 24 .link { 25 stroke: #999; 26 stroke-opacity: 0.6; 27 stroke-width: 1.5px; 28 marker-end: url(#arrowhead); 29 } 30 .link.positive { 31 stroke: green; 32 } 33 .link.negative { 34 stroke: red; 35 } 36 .arrowhead { 37 fill: #999; 38 } 39 .arrowhead.positive { 40 fill: green; 41 } 42 .arrowhead.negative { 43 fill: red; 44 } 45 .control-panel { 46 margin-top: 20px; 47 padding: 15px; 48 border: 1px solid #ccc; 49 background-color: #f9f9f9; 50 } 51 .control-panel button { 52 margin-right: 10px; 53 padding: 8px 12px; 54 cursor: pointer; 55 } 56 </style> 57 58 <div id="loopy-model"></div> 59 60 <script src="https://d3js.org/d3.v7.min.js"></script> 61 <script> 62 const nodesData = [ 63 { id: "donations", name: "Donations", value: 10 }, 64 { id: "compute_budget", name: "Compute Budget", value: 50 }, 65 { id: "platform_quality", name: "Platform Quality", value: 70 }, 66 { id: "agent_capability", name: "Agent Capability", value: 60 }, 67 { id: "user_base", name: "User Base", value: 100 }, 68 { id: "community_size", name: "Community Size", value: 30 }, 69 { id: "oss_contributions", name: "Open Source Contributions", value: 20 }, 70 { id: "compute_cost", name: "Compute Cost", value: 40 }, 71 { id: "maintenance_burden", name: "Maintenance Burden", value: 25 } 72 ]; 73 74 const linksData = [ 75 { source: "donations", target: "compute_budget", polarity: "+" }, 76 { source: "compute_budget", target: "platform_quality", polarity: "+" }, 77 { source: "platform_quality", target: "agent_capability", polarity: "+" }, 78 { source: "agent_capability", target: "user_base", polarity: "+" }, 79 { source: "user_base", target: "donations", polarity: "+" }, 80 { source: "user_base", target: "community_size", polarity: "+" }, 81 { source: "community_size", target: "oss_contributions", polarity: "+" }, 82 { source: "oss_contributions", target: "agent_capability", polarity: "+" }, 83 { source: "oss_contributions", target: "maintenance_burden", polarity: "-" }, 84 { source: "user_base", target: "compute_cost", polarity: "+" }, 85 { source: "compute_cost", target: "compute_budget", polarity: "-" }, 86 { source: "maintenance_burden", target: "platform_quality", polarity: "-" }, 87 { source: "user_base", target: "maintenance_burden", polarity: "+" }, 88 { source: "platform_quality", target: "user_base", polarity: "+" } // Secondary reinforcement 89 ]; 90 91 const width = 960; 92 const height = 600; 93 94 const svg = d3.select("#loopy-model") 95 .append("svg") 96 .attr("width", width) 97 .attr("height", height); 98 99 // Define arrow markers 100 svg.append("defs").selectAll("marker") 101 .data(["positive", "negative"]) 102 .enter().append("marker") 103 .attr("id", d => "arrowhead-" + d) 104 .attr("viewBox", "0 -5 10 10") 105 .attr("refX", 20) // Shift arrow back to be on the edge of the circle 106 .attr("refY", 0) 107 .attr("orient", "auto") 108 .attr("markerWidth", 6) 109 .attr("markerHeight", 6) 110 .attr("xoverflow", "visible") 111 .append("path") 112 .attr("d", "M0,-5L10,0L0,5") 113 .attr("class", d => "arrowhead " + d) 114 .style("stroke", "none"); 115 116 const simulation = d3.forceSimulation(nodesData) 117 .force("link", d3.forceLink(linksData).id(d => d.id).distance(100)) 118 .force("charge", d3.forceManyBody().strength(-300)) 119 .force("center", d3.forceCenter(width / 2, height / 2)); 120 121 const link = svg.append("g") 122 .attr("class", "links") 123 .selectAll("line") 124 .data(linksData) 125 .enter().append("line") 126 .attr("class", d => "link " + (d.polarity === "+" ? "positive" : "negative")) 127 .attr("marker-end", d => "url(#arrowhead-" + (d.polarity === "+" ? "positive" : "negative") + ")"); 128 129 const node = svg.append("g") 130 .attr("class", "nodes") 131 .selectAll("circle") 132 .data(nodesData) 133 .enter().append("circle") 134 .attr("r", 15) 135 .attr("class", "node") 136 .call(d3.drag() 137 .on("start", dragstarted) 138 .on("drag", dragged) 139 .on("end", dragended)); 140 141 const labels = svg.append("g") 142 .attr("class", "labels") 143 .selectAll("text") 144 .data(nodesData) 145 .enter().append("text") 146 .attr("class", "node-label") 147 .attr("dy", "0.35em") 148 .text(d => d.name); 149 150 // Add interaction: click a node to increase/decrease its value (and trigger propagation) 151 node.on("click", function(event, d) { 152 d.value += 10; // Increase value by 10 153 updateNodeDisplay(d); 154 propagateEffect(d, 0.1); // Propagate a 10% effect 155 }); 156 157 function updateNodeDisplay(d) { 158 // For now, just update the label if we want to show value, or change color 159 // A simple way to represent value change visually is a color gradient or size change. 160 // For simplicity, let's just make it pulse or change fill slightly for now. 161 d3.select(this) // 'this' refers to the clicked circle 162 .transition().duration(200) 163 .attr("fill", "#FFD700") // Gold color on click 164 .transition().duration(500) 165 .attr("fill", "#ADD8E6"); // Back to light blue 166 } 167 168 function propagateEffect(sourceNode, initialEffectMagnitude) { 169 const queue = [{ node: sourceNode, effect: initialEffectMagnitude }]; 170 const visited = new Set(); 171 const nodeEffects = new Map(nodesData.map(n => [n.id, 0])); // Store cumulative effects for this step 172 173 let head = 0; 174 while (head < queue.length) { 175 const { node: currentNode, effect: currentEffect } = queue[head++]; 176 177 if (visited.has(currentNode.id)) continue; 178 visited.add(currentNode.id); 179 180 // Apply effect to current node (this is a simple visual update, not a full simulation) 181 // For a real simulation, currentNode.value would be updated based on effect 182 // We'll simulate a visual ripple for now. 183 d3.select(node.filter(d => d.id === currentNode.id).node()) 184 .transition().duration(100) 185 .attr("fill", "#FFD700") // Temporarily change color 186 .transition().duration(500).delay(100) 187 .attr("fill", "#ADD8E6"); 188 189 linksData.filter(l => l.source.id === currentNode.id).forEach(link => { 190 const targetNode = link.target; 191 const polarity = link.polarity === "+" ? 1 : -1; 192 const nextEffect = currentEffect * 0.5 * polarity; // Effect dampens over distance 193 194 // Visually indicate link activity 195 d3.select(link.lineElement) // Assume link elements are accessible 196 .transition().duration(100) 197 .style("stroke-width", "4px") 198 .transition().duration(500).delay(100) 199 .style("stroke-width", "1.5px"); 200 201 if (Math.abs(nextEffect) > 0.01) { // Only propagate significant effects 202 nodeEffects.set(targetNode.id, nodeEffects.get(targetNode.id) + nextEffect); 203 // Add to queue for next iteration, but don't re-add if already processing this path 204 if (!visited.has(targetNode.id)) { 205 queue.push({ node: targetNode, effect: nextEffect }); 206 } 207 } 208 }); 209 } 210 211 // After processing all direct and indirect effects, apply cumulative changes visually. 212 // This is a simplified propagation, not a true stock-and-flow model. 213 nodesData.forEach(n => { 214 if (nodeEffects.get(n.id) !== 0) { 215 // A more complex model would adjust n.value based on all incoming effects 216 // For this visualization, we just show the ripple effect 217 } 218 }); 219 } 220 221 222 simulation.on("tick", () => { 223 link 224 .attr("x1", d => d.source.x) 225 .attr("y1", d => d.source.y) 226 .attr("x2", d => d.target.x) 227 .attr("y2", d => d.target.y); 228 229 node 230 .attr("cx", d => d.x) 231 .attr("cy", d => d.y); 232 233 labels 234 .attr("x", d => d.x) 235 .attr("y", d => d.y + 30); // Position label below the node 236 }); 237 238 function dragstarted(event, d) { 239 if (!event.active) simulation.alphaTarget(0.3).restart(); 240 d.fx = d.x; 241 d.fy = d.y; 242 } 243 244 function dragged(event, d) { 245 d.fx = event.x; 246 d.fy = event.y; 247 } 248 249 function dragended(event, d) { 250 if (!event.active) simulation.alphaTarget(0); 251 d.fx = null; 252 d.fy = null; 253 } 254 255 // Store link elements for propagation (needed for visual feedback on links) 256 link.each(function(d) { 257 d.lineElement = this; 258 }); 259 260 </script>