1// FIXME: These shouldn't be in the global space 2margin = {top: 10, right: 10, bottom: 0, left: 10}; 3left_width = 300; 4right_width = 400; 5width = left_width+right_width; 6height = 400; 7sim_center = [left_width + right_width/2, height/2]; 8 9class SimulationPlot { 10 mount(svg, step_button, restart_button, colors) { 11 this.svg = svg; 12 this.defs = svg.append('defs'); 13 this.step_button = step_button; 14 this.restart_button = restart_button; 15 svg.attr('width', width).attr('height', height); 16 this.box = svg.append("g"); 17 this.sim_g = svg.append('g') 18 .attr('x', left_width) 19 .attr('y', margin.top) 20 .attr('width', right_width) 21 .attr('height', height); 22 this.n = 200; 23 this.colors = colors; 24 if (colors == 2) { 25 this.wq = 50; 26 this.wu = 50; 27 this.w1 = 25; 28 this.t = .843; 29 this.D = 3; 30 this.initial_reps = 30; 31 } else { 32 this.w = 50; 33 this.t = .81071; 34 this.D = 2; 35 this.initial_reps = 15; 36 } 37 this.init_buttons(); 38 } 39 restart() { 40 if (this.colors == 2) { 41 const set1_colors = d3.scaleSequential(d3.interpolateBlues).domain([-3, 2]); 42 const set2_colors = d3.scaleSequential(d3.interpolateOranges).domain([-3, 4]); 43 const uni_colors = d3.scaleSequential(d3.interpolateGreys).domain([-1, 5]); 44 this.data = Array.from({ length: this.n }, (_, i) => { 45 const set1_item = Math.random() < this.wq/this.n; 46 const set2_item = set1_item 47 ? Math.random() < this.w1/this.wq 48 : Math.random() < (this.wu-this.w1)/(this.n-this.wq); 49 const lum = Math.random(); 50 const grey = uni_colors(lum); 51 return { 52 r: 2 * (4 + Math.random()**2) 53 ,color1: set1_item ? set1_colors(lum) : grey 54 ,color2: set2_item ? set2_colors(lum) : grey 55 ,is_set1_item: set1_item 56 ,is_set2_item: set2_item 57 ,id: Math.random() 58 }}); 59 } else { 60 const set_colors = d3.scaleSequential(d3.interpolateBlues).domain([-3, 2]); 61 const uni_colors = d3.scaleSequential(d3.interpolateGreys).domain([-1, 5]); 62 this.data = Array.from({ length: this.n }, (_, i) => { 63 const set_item = Math.random() < this.w/this.n; 64 const color = set_item ? set_colors(Math.random()) : uni_colors(Math.random()); 65 return { 66 r: 2 * (4 + Math.random()**2) 67 ,color1: color 68 ,color2: color 69 ,is_set1_item: set_item 70 ,is_set2_item: set_item 71 ,id: Math.random() 72 }}); 73 } 74 this.update_uni(); 75 76 this.clone_step = false; 77 this.step_button 78 .attr('style', 'visibility:show') 79 .html('<i class="material-icons">skip_next</i>' 80 + (this.clone_step ? 'Multiply' : 'Mutate')); 81 this.reps = []; 82 for (let i = 0; i < this.initial_reps; i++) { 83 this.reps.push({ 84 objs: [], 85 x: sim_center[0], 86 y: sim_center[1], 87 ox: sim_center[0], 88 oy: sim_center[1], 89 r: 15, 90 id: Math.random(), 91 val1: 0, 92 val2: 0, 93 alive: true 94 }); 95 } 96 this.update_sim(); 97 } 98 init_buttons() { 99 this.restart_button.on('click', () => { 100 // Remove alive class, so the reps don't shake 101 this.sim_g.selectAll('.alive').classed('alive', false); 102 this.restart(); 103 }); 104 let rep_sim = null; 105 this.step_button.on('click', () => { 106 for (const rep of this.reps) 107 if (rep.sim) 108 rep.sim.stop(); 109 const new_reps = []; 110 if (this.clone_step) { 111 for (const rep of this.reps) { 112 if (!rep.alive) 113 continue; 114 for (let i = 0; i < this.D; i++) { 115 const new_rep = { 116 objs: [], 117 x: rep.x, 118 y: rep.y, 119 ox: rep.x, 120 oy: rep.y, 121 id: Math.random(), 122 r: rep.r, 123 val1: rep.val1, 124 val2: rep.val2, 125 alive: true 126 }; 127 for (const old_obj of rep.objs) { 128 const obj = {...old_obj}; 129 obj.ox = obj.x; 130 obj.oy = obj.y; 131 new_rep.objs.push(obj); 132 } 133 new_reps.push(new_rep); 134 } 135 } 136 } 137 else { 138 for (const rep of this.reps) { 139 if (!rep.alive) 140 continue; 141 const par = this.data[Math.floor(Math.random()*this.n)]; 142 rep.objs.push({ 143 r: par.r, 144 x: par.x - rep.x, 145 y: par.y - rep.y, 146 ox: par.x - rep.x, 147 oy: par.y - rep.y, 148 id: Math.random(), 149 color1: par.color1, 150 color2: par.color2, 151 pid: par.id, // Used for the gradient 152 }); 153 rep.ox = rep.x; 154 rep.oy = rep.y; 155 rep.r = 10 + 10 * Math.sqrt(rep.objs.length); 156 rep.val1 = rep.val1 + (par.is_set1_item ? 1 : 0); 157 rep.val2 = rep.val2 + (par.is_set2_item ? 1 : 0); 158 rep.alive = Math.abs(rep.val1 - this.t*rep.objs.length) 159 <= Math.pow(this.t*(1-this.t)*rep.objs.length, 1/3) 160 && Math.abs(rep.val2 - this.t*rep.objs.length) 161 <= Math.pow(this.t*(1-this.t)*rep.objs.length, 1/3); 162 new_reps.push(rep); 163 } 164 } 165 this.reps = new_reps; 166 this.clone_step = !this.clone_step; 167 if (!this.reps.some(d => d.alive)) 168 this.step_button.attr('style', 'visibility:hidden'); 169 else 170 this.step_button.html( 171 '<i class="material-icons">skip_next</i>' 172 + (this.clone_step ? 'Multiply' : 'Mutate')); 173 this.update_sim(); 174 }); 175 } 176 update_sim() { 177 const rep_nodes = this.sim_g 178 .selectAll('g') 179 .data(this.reps, d => d.id) 180 .join( 181 enter => enter 182 .append('g') 183 .attr('class', 'rep') 184 .attr('transform', d => `translate(${d.x}, ${d.y})`) 185 .call(gs => gs 186 .append('circle') 187 .attr('class', 'rep_circle') 188 .attr('r', 0) 189 .attr('opacity', 0)) 190 , 191 update => update, 192 exit => { 193 // Mark nodes for dying, so they are no longer force animated 194 exit.classed('dying', true); 195 const alives = exit.filter('.alive'); 196 const not_alives = exit.filter(':not(.alive)'); 197 198 // Remove dead individuals 199 not_alives 200 .selectAll('circle') 201 .attr('opacity', 1) 202 .transition() 203 .duration(1000) 204 .attr('r', 0) 205 .attr('opacity', 0) 206 .attr('cx', 0) 207 .attr('cy', 0); 208 not_alives 209 .transition().delay(1000) 210 .remove(); 211 212 // Remove "alive" individuals that are branching 213 const shake = 15; 214 let shake_transition = alives; 215 for (let i = 0; i < 5; i++) 216 shake_transition = shake_transition 217 .transition().duration(100) 218 .attr('transform', d => `translate( 219 ${d.x + shake*(Math.random()-.5)}, 220 ${d.y + shake*(Math.random()-.5)})`); 221 shake_transition 222 .transition().delay(500) 223 .remove(); 224 225 exit 226 .filter('.alive') 227 .selectAll('circle') 228 .attr('opacity', 1) 229 .transition().delay(400).duration(600) 230 .attr('opacity', 0) 231 .attr('r', 0) 232 .attr('cx', 0) 233 .attr('cy', 0); 234 } 235 ) 236 .classed('alive', d => d.alive) 237 ; 238 239 // Update circle size 240 rep_nodes.selectAll('.rep_circle') 241 .transition() 242 .duration(1000) 243 .attr('r', d => d.r) 244 .attr('opacity', 1) 245 ; 246
247 const circles = rep_nodes 248 .selectAll('circle.obj') 249 .data(d => d.objs, d => d.id) 250 .join( 251 enter => { 252 const circles = enter 253 .append('circle') 254 .attr('class', 'obj') 255 //.attr('fill', d => d.color) 256 .attr('fill', d => d.color1 != d.color2 ? `url(#g${d.pid})` : d.color1) 257 //.attr('r', d => d.r) 258 // If the objects arrived by cloning, we are now no longer 259 // in the clone step. 260 if (!this.clone_step) 261 circles 262 .attr('r', 0) 263 .attr('opacity', 0) 264 .transition() 265 .duration(1000) 266 .attr('r', d => d.r) 267 .attr('opacity', 1) 268 else 269 circles 270 .attr('stroke', '#183752') 271 .attr('stroke-width', '1px') 272 .attr('r', d => 1.5*d.r) 273 .transition() 274 .duration(1000) 275 .attr('stroke', 'transparent') 276 .attr('r', d => d.r) 277 return circles; 278 } 279 ) 280 281 rep_nodes 282 .each(outer_d => { 283 outer_d.sim = d3.forceSimulation(outer_d.objs) 284 .on("tick", function() { 285 let a = this.alpha(); 286 a = Math.exp(-(Math.log(a)**4)); 287 circles 288 .attr('cx', d => d.x * (1-a) + d.ox * a) 289 .attr('cy', d => d.y * (1-a) + d.oy * a); 290 }) 291 .force("collide", d3.forceCollide().radius(d => 1 + d.r)) 292 .force("x", d3.forceX(0).strength(1e-1)) 293 .force("y", d3.forceY(0).strength(1e-1)) 294 } 295 ) 296 297 if (this.rep_sim) this.rep_sim.stop(); 298 let sim_g = this.sim_g; 299 this.rep_sim = d3.forceSimulation(this.reps) 300 .on("tick", function() { 301 let a = this.alpha(); 302 if (!this.clone_step) 303 a = Math.exp(-(Math.log(a)**4)/4); 304 // We filter out dying reps, since they are busy shaking 305 sim_g.selectAll('g:not(.dying)') 306 .attr('transform', function(d) { 307 let x = d.x * (1-a) + d.ox * a; 308 let y = d.y * (1-a) + d.oy * a; 309 return `translate(${x}, ${y})`; 310 }); 311 }) 312 .force("collide", d3.forceCollide().radius(d => 5 + d.r)) 313 .force("x", d3.forceX(sim_center[0]).strength(5e-2)) 314 .force("y", d3.forceY(sim_center[1]).strength(5e-2)); 315 316 if (this.reps.length == 0) 317 this.restart(); 318 } 319 update_uni() { 320 const grads = this.data.filter(d => d.color1 != d.color2); 321 this.defs.selectAll('linearGradient') 322 .data(grads, d => d.id) 323 .join('linearGradient', 324 enter => enter.append('linearGradient'), 325 update => update, 326 // Wait a moment before removing the gradients, so we still have 327 // them as the old rep are exiting 328 exit => exit.delay(2000).remove() 329 ) 330 .attr('id', d => `g${d.id}`) 331 .attr('x1', 0) .attr('y1', 0) 332 .attr('x2', 1) .attr('y2', 1) 333 .call(lg => lg.append('stop') 334 .attr('offset', '.5').attr('style', d=>`stop-color:${d.color1}`)) 335 .call(lg => lg.append('stop') 336 .attr('offset', '.5').attr('style', d=>`stop-color:${d.color2}`)) 337 ; 338 const nodes = this.box.selectAll('circle') 339 .data(this.data, d => d.id) 340 .join('circle') 341 .attr('r', d => d.r) 342 .attr('fill', d => d.color1 != d.color2 ? `url(#g${d.id})` : d.color1) 343 ; 344 345 function tick(nodes) { 346 nodes.attr("cx", d => d.x).attr("cy", d => d.y); 347 } 348 this.rescale = isNaN(this.data[0].x); 349 const box_center = [margin.left/2+left_width/2, (margin.top-margin.bottom+height)/2]; 350 const simulation = d3 351 .forceSimulation(this.data) 352 .on("tick", () => tick(nodes)) 353 .force("collide", d3.forceCollide().radius(d => 2 + d.r)) 354 .force("x", d3.forceX(box_center[0]).strength(0.007)) 355 .force("y", d3.forceY(box_center[1]).strength(0.001)) 356 .stop(); 357 358 // once the arrangement is initialized, scale and translate it 359 const scale = 1; 360 if (this.rescale) { 361 for (const node of this.data) { 362 node.x = node.x * scale + box_center[0]; 363 node.y = node.y * scale + box_center[1]; 364 } 365 } 366 367 // differ application of the forces 368 simulation.restart(); 369 } 370 update() { 371 this.restart(); 372 } 373}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.