/ models / loopy-sustainability-model.html
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>