js-mindmap PlugIn

js-mindmap PlugIn

Hey Guys,

I stumbled upon this mindmap-PlugIn for jquery by Kenneth Kufluk:  http://code.google.com/p/js-mindmap/
Since it is published under the MIT/GPL-License, I figured it is okay for me to make additional modifications. I already tried to contact the author of the script, but without access. Maybe I can get in touch with him at some point to share my modifications with him.

So far I'm on my own which brings me to my specific problem. I'm trying to add additional nodes to the starting-point mindmap. So far, you can see the root-node and any sub-node that is connected to it. From these sub-nodes additional lines are derived that indicate more deeper-leveled sub-nodes (these can be made visible by clicking on one the sub-nodes on the second level).

I would like to add a parameter that specifies how deep / how many levels are show in the initial mindmap instead of just two levels. I don't expect any of you to rewrite the code for me, as I am the one who wants to modify the script. But at this point I'm having a hard time figuring out how to even add those third-level nodes to the end of the thin lines.

Maybe some of you could take a look at the script and get me started. In return, of course, I will publish my modification here and give it to the developer.

Thanks in advance
Tim

------------------------------
js-mindmap.0.3.js
  1. // js-mindmap
  2. // (c) Kenneth Kufluk 2008/09 http://kenneth.kufluk.com/
  3. // ported to jQuery plugin by Mike Trpcic http://www.mtrpcic.net/
  4. // GPLv3 http://www.gnu.org/licenses/gpl.html

  5. // TODO:
  6. // Area restriction
  7. // Exploration (showProgressive)
  8. // Hidden children should not be bounded
  9. // Layout children in circles
  10. // Children of a node should not be able to push a sister of that node - but they should be able to feel its effect
  11. // x - Multiple Parents
  12. // Add/Edit nodes
  13. // When activity stops, stop the loop (for performance)
  14. // Resize event


  15. (function($){

  16.     $.fn.mindmap = function(options) {
  17.         // Define default settings.
  18.         var options = $.extend({
  19.             attract: 7,
  20.             repulse: 1,
  21.             damping: 0.66,
  22.             timeperiod: 3,
  23.             wallrepulse: 0.1,
  24.             mapArea: {
  25.                 x:-1,
  26.                 y:-1
  27.             },
  28.             canvasError: 'alert',
  29.             minSpeed: 0.05,
  30.             maxForce: 0.1,
  31.             showSublines: true,
  32.             updateIterationCount: 20,
  33.             showProgressive: true
  34.         },options);
  35.     
  36.         // Define all Node related functions.
  37.         function Node(obj, index, el, parent, active){
  38.             this.obj = obj;
  39.          this.el = $(el);
  40.          this.el.mindMapObj = this;
  41.             if (active) {
  42.                 obj.activeNode = this;
  43.                 $(this.el).addClass('active');
  44.             }
  45.             this.parent = parent;
  46.             this.el.addClass('node');
  47.             this.index = index;
  48.             this.visible = true;
  49.             this.hasLayout = true;
  50.             this.x = Math.random()+(options.mapArea.x/2);
  51.             this.y = Math.random()+(options.mapArea.y/2);
  52.          this.el.css('left', this.x + "px");
  53.          this.el.css('top', this.y + "px");
  54.             this.dx = 0;
  55.             this.dy = 0;
  56.             this.count = 0;
  57.             
  58.             this.el.draggable();
  59.             this.el.css('position','absolute');        
  60.     
  61.             if (this.el.children()[0]) {
  62.                 if (this.el.children()[0].tagName == 'A') this.el.href = this.el.children().href;
  63.             }
  64.             var thisnode = this;
  65.             this.el.click(function(){
  66.  //               console.log(obj.activeNode);
  67.                  location.href=this.el.children()[0].tagName;

  68.                 if (obj.activeNode) obj.activeNode.el.removeClass('active');
  69.                 obj.activeNode = thisnode;
  70.                 obj.activeNode.el.addClass('active');
  71.                 return false;
  72.             });
  73.     
  74.             this.el.dblclick(function(){
  75.                 location.href=this.el.children()[0].tagName;
  76.                 return false;
  77.             });
  78.         }
  79.     
  80.         //TODO: Write this method!
  81.         Node.prototype.layOutChildren = function(){
  82.         //show my child nodes in an equally spaced group around myself, instead of placing them randomly
  83.         }
  84.     
  85.         Node.prototype.getForceVector = function(){
  86.             var fx = 0;
  87.             var fy = 0;
  88.             
  89.             var nodes = this.obj.nodes;
  90.             var lines = this.obj.lines;

  91.             for (var i = 0; i < nodes.length; i++) {
  92.                 if (i == this.index) continue;
  93.                 if ((options.showSublines && !nodes[i].hasLayout) || (!options.showSublines && !nodes[i].visible)) continue;
  94.                 // Repulsive force (coulomb's law)
  95.                 var x1 = (nodes[i].x - this.x);
  96.                 var y1 = (nodes[i].y - this.y);
  97.                 //adjust for variable node size
  98.     // var nodewidths = (($(nodes[i]).width() + $(this.el).width())/2);
  99.                 var xsign = x1 / Math.abs(x1);
  100.                 var ysign = y1 / Math.abs(y1);
  101.                 var dist = Math.sqrt((x1 * x1) + (y1 * y1));
  102.                 var theta = Math.atan(y1 / x1);
  103.                 if (x1 == 0) {
  104.                     theta = Math.PI / 2;
  105.                     xsign = 0;
  106.                 }
  107.                 // force is based on radial distance
  108.                 var myrepulse = options.repulse;
  109.                 if (this.parent==nodes[i]) myrepulse=myrepulse*10;  //parents stand further away
  110.                 var f = (myrepulse * 500) / (dist * dist);
  111.                 if (Math.abs(dist) < 500) {
  112.                     fx += -f * Math.cos(theta) * xsign;
  113.                     fy += -f * Math.sin(theta) * xsign;
  114.                 }
  115.             }
  116.             // add repulsive force of the "walls"
  117.             //left wall
  118.             var xdist = this.x + $(this.el).width();
  119.             var f = (options.wallrepulse * 500) / (xdist * xdist);
  120.             fx += Math.min(2, f);
  121.             //right wall
  122.             var rightdist = (options.mapArea.x - xdist);
  123.             var f = -(options.wallrepulse * 500) / (rightdist * rightdist);
  124.             fx += Math.max(-2, f);
  125.             //top wall
  126.             var f = (options.wallrepulse * 500) / (this.y * this.y);
  127.             fy += Math.min(2, f);
  128.             //botttom wall
  129.             var bottomdist = (options.mapArea.y - this.y);
  130.             var f = -(options.wallrepulse * 500) / (bottomdist * bottomdist);
  131.             fy += Math.max(-2, f);
  132.     
  133.             // for each line, of which I'm a part, add an attractive force.
  134.             for (var i = 0; i < lines.length; i++) {
  135.                 var otherend = null;
  136.                 if (lines[i].start.index == this.index) {
  137.                     otherend = lines[i].end;
  138.                 } else if (lines[i].end.index == this.index) {
  139.                     otherend = lines[i].start;
  140.                 } else continue;
  141.                 // Attractive force (hooke's law)
  142.                 var x1 = (otherend.x - this.x);
  143.                 var y1 = (otherend.y - this.y);
  144.                 var dist = Math.sqrt((x1 * x1) + (y1 * y1));
  145.                 var xsign = x1 / Math.abs(x1);
  146.                 var theta = Math.atan(y1 / x1);
  147.                 if (x1==0) {
  148.                     theta = Math.PI / 2;
  149.                     xsign = 0;
  150.                 }
  151.                 // force is based on radial distance
  152.                 var f = (options.attract * dist) / 10000;
  153.                 if (Math.abs(dist) > 0) {
  154.                     fx += f * Math.cos(theta) * xsign;
  155.                     fy += f * Math.sin(theta) * xsign;
  156.                 }
  157.             }
  158.     
  159.             // if I'm active, attract me to the centre of the area
  160.             if (this.obj.activeNode === this) {
  161.                 // Attractive force (hooke's law)
  162.                 var otherend = options.mapArea;
  163.                 var x1 = ((otherend.x / 2) - 100 - this.x);
  164.                 var y1 = ((otherend.y / 2) - this.y);
  165.                 var dist = Math.sqrt((x1 * x1) + (y1 * y1));
  166.                 var xsign = x1 / Math.abs(x1);
  167.                 var theta = Math.atan(y1 / x1);
  168.                 if (x1 == 0) {
  169.                     theta = Math.PI / 2;
  170.                     xsign = 0;
  171.                 }
  172.                 // force is based on radial distance
  173.                 var f = (0.1 * options.attract*dist) / 10000;
  174.                 if (Math.abs(dist) > 0) {
  175.                     fx += f * Math.cos(theta) * xsign;
  176.                     fy += f * Math.sin(theta) * xsign;
  177.                 }
  178.             }
  179.     
  180.             if (Math.abs(fx) > options.maxForce) fx = options.maxForce * (fx / Math.abs(fx));
  181.             if (Math.abs(fy) > options.maxForce) fy = options.maxForce * (fy / Math.abs(fy));
  182.             return {
  183.                 x: fx,
  184.                 y: fy
  185.             };
  186.         }
  187.     
  188.         Node.prototype.getSpeedVector = function(){
  189.             return {
  190.                 x:this.dx,
  191.                 y:this.dy
  192.             };
  193.         }
  194.     
  195.         Node.prototype.updatePosition = function(){
  196.             if ($(this.el).hasClass("ui-draggable-dragging")) {
  197.          this.x = parseInt(this.el.css('left')) + ($(this.el).width() / 2);
  198.          this.y = parseInt(this.el.css('top')) + ($(this.el).height() / 2);
  199.          this.dx = 0;
  200.          this.dy = 0;
  201.          return;
  202.          }
  203.             
  204.             
  205.             //apply accelerations
  206.             var forces = this.getForceVector();
  207.             // console.log(forces.x);
  208.             this.dx += forces.x * options.timeperiod;
  209.             this.dy += forces.y * options.timeperiod;
  210.     
  211.             //TODO: CAP THE FORCES
  212.     
  213.             // this.el.childNodes[0].innerHTML = parseInt(this.dx)+' '+parseInt(this.dy);
  214.             this.dx = this.dx * options.damping;
  215.             this.dy = this.dy * options.damping;
  216.     
  217.             //ADD MINIMUM SPEEDS
  218.             if (Math.abs(this.dx) < options.minSpeed) this.dx = 0;
  219.             if (Math.abs(this.dy) < options.minSpeed) this.dy = 0;
  220.             //apply velocity vector
  221.             this.x += this.dx * options.timeperiod;
  222.             this.y += this.dy * options.timeperiod;
  223.             this.x = Math.min(options.mapArea.x,Math.max(1,this.x));
  224.             this.y = Math.min(options.mapArea.y,Math.max(1,this.y));
  225.             //only update the display after the thousanth iteration, so it's not too wild at the start
  226.             this.count++;
  227.             // if (this.count<updateDisplayAfterNthIteration) return;
  228.             // display
  229.          var showx = this.x - ($(this.el).width() / 2);
  230.          var showy = this.y - ($(this.el).height() / 2);
  231.          this.el.css('left', showx + "px");
  232.          this.el.css('top', showy + "px");
  233.         }
  234.     
  235.         // Define all Line related functions.
  236.         function Line(obj, index, startNode, finNode){
  237.             this.obj = obj;

  238.             this.index = index;
  239.             this.start = startNode;
  240.             this.colour = "blue";
  241.             this.size = "thick";
  242.             this.end = finNode;
  243.             this.count = 0;
  244.         }
  245.     
  246.         Line.prototype.updatePosition = function(){
  247.             if (options.showSublines && (!this.start.hasLayout || !this.end.hasLayout)) return;
  248.             if (!options.showSublines && (!this.start.visible || !this.end.visible)) return;
  249.             if (this.start.visible && this.end.visible) this.size = "thick";
  250.             else this.size = "thin";
  251.             if (this.obj.activeNode.parent == this.start || this.obj.activeNode.parent == this.end) this.colour = "red";
  252.             else this.colour = "blue";
  253.             switch (this.colour) {
  254.                 case "red":
  255.                     this.obj.ctx.strokeStyle = "rgb(100, 0, 0)";
  256.                     break;
  257.                 case "blue":
  258.                     this.obj.ctx.strokeStyle = "rgba(0, 0, 100, 0.2)";
  259.                     break;
  260.             }
  261.             switch (this.size) {
  262.                 case "thick":
  263.                     this.obj.ctx.lineWidth = "3";
  264.                     break;
  265.                 case "thin":
  266.                     this.obj.ctx.lineWidth = "1";
  267.                     break;
  268.             }
  269.             this.obj.ctx.beginPath();
  270.             this.obj.ctx.moveTo(this.start.x, this.start.y);
  271.             this.obj.ctx.quadraticCurveTo(((this.start.x + this.end.x) / 1.8),((this.start.y + this.end.y) / 2.4), this.end.x, this.end.y);
  272.             this.obj.ctx.lineTo(this.end.x, this.end.y);
  273.             this.obj.ctx.stroke();
  274.             this.obj.ctx.closePath();
  275.         }
  276.     
  277.         // Main Running Loop
  278.         var Loop = function (obj){
  279.             var nodes = obj.nodes;
  280.             var lines = obj.lines;
  281.             var canvas = $('canvas', obj).get(0);
  282.             if (typeof G_vmlCanvasManager != 'undefined') canvas=G_vmlCanvasManager.initElement(canvas);
  283.             obj.ctx = canvas.getContext("2d");


  284.             obj.ctx.clearRect(0, 0, options.mapArea.x, options.mapArea.y);
  285.             //update node positions
  286.             for (var i = 0; i < nodes.length; i++) {
  287.                 //TODO: replace this temporary idea
  288.                 var childActive = false;
  289.                 var currentNode = obj.activeNode;
  290.                 while (currentNode.parent && (currentNode = currentNode.parent)) {
  291.                     if (currentNode == nodes[i]) childActive = true;
  292.                 }
  293.                 if (childActive || obj.activeNode == nodes[i] || obj.activeNode == nodes[i].parent) {
  294.                     nodes[i].visible = true;
  295.                     nodes[i].hasLayout = true;
  296.                 } else {
  297.                     nodes[i].visible = false;
  298.                     if (nodes[i].parent && nodes[i].parent.parent && nodes[i].parent.parent == obj.activeNode) {
  299.                         nodes[i].hasLayout = true;
  300.                     } else {
  301.                         nodes[i].hasLayout = false;
  302.                     }
  303.                     if (!options.showProgressive) {
  304.                         nodes[i].visible = true;
  305.                         nodes[i].hasLayout = true;
  306.                     }
  307.                 }
  308.                 if (nodes[i].visible) {
  309.                     nodes[i].el.show();
  310.                 } else {
  311.                     nodes[i].el.hide();
  312.                 }
  313.                 if ((options.showSublines && !nodes[i].hasLayout) || (!options.showSublines && !nodes[i].visible)) continue;
  314.                 nodes[i].updatePosition();
  315.             }
  316.             //display lines
  317.             for (var i = 0; i < lines.length; i++) {
  318.                 lines[i].updatePosition();
  319.             }
  320.             
  321.         }
  322.     
  323.         // This Helper adds the UL into the mindmap
  324.         function addList(obj, ul, parent){
  325.             var nodes = obj.nodes;
  326.             var lines = obj.lines;
  327.             
  328.             // For each LI in this list
  329.             $('>li', ul).each(function(index) {

  330.                 // Add as a new Node
  331.                 var nodeno = nodes.length;
  332.                 nodes[nodeno] = new Node(obj, nodeno, this, parent);
  333. //                console.log(this);
  334.                 this.mindmapNode = nodes[nodeno];
  335.                 
  336.                 // Add subtrees
  337.                 $('>ul', this).each(function(index) {
  338.                     addList(obj, this, nodes[nodeno]);
  339.                 });
  340.                 
  341.                 // Add Relationship between Nodes (draw a line)
  342.                 if (parent != null) {
  343.                     var lineno = lines.length;
  344.                     lines[lineno] = new Line(obj, lineno, nodes[nodeno], parent);
  345.                 }
  346.             });
  347.         }
  348.     
  349.         
  350.         return this.each(function() {

  351.             var nodes = this.nodes = new Array();
  352.             var lines = this.lines = new Array();
  353.             this.activeNode = null;
  354.         
  355.             if (typeof window.CanvasRenderingContext2D == 'undefined' && typeof G_vmlCanvasManager == 'undefined') {
  356.                 if (options.canvasError === "alert"){
  357.                     alert("ExCanvas was not properly loaded.");
  358.                 } else if (options.canvasError === "console"){
  359.                     console.log("ExCanvas was not properly loaded.");
  360.                 } else {
  361.                     options.canvasError();
  362.                 }
  363.             } else {
  364.                 //CANVAS
  365.                 if (options.mapArea.x==-1) {
  366.                     options.mapArea.x = $(window).width();
  367.                 }
  368.                 if (options.mapArea.y==-1) {
  369.                     options.mapArea.y = $(window).height();
  370.                 }
  371.                 //create element
  372.                 this.canvas = $('<canvas width="'+options.mapArea.x+'" height="'+options.mapArea.y+'" style="position:absolute;left:0;top:0;"></canvas>');
  373.                 //add to document
  374.                 $(this).append(this.canvas);
  375.                 
  376.                 //NODES
  377.                 // create root node
  378.                 var myroot = $("a", this).get(0);
  379.                 var nodeno = nodes.length;
  380.                 nodes[nodeno] = new Node(this, nodeno, myroot, null, true);
  381.     
  382.                 // build the tree
  383.                 var myul = $("ul", this).get(0);
  384.                 addList(this, myul, nodes[nodeno]);

  385.                 // Flatten LIs
  386.                 $('li', myul).each(function(index) {
  387.                     // Move each LI to the root of the UL
  388.                     // We do this because of the cascading positioning of LIs
  389.                     // If I put an LI at (10, 10), all child UL>LIs will also be offset
  390.                     // so we move everything into the root UL
  391.                     $(myul).append(this);

  392.                 });

  393.                 // Add additional lines described by rel="id id id"
  394.                 var obj = this;
  395.                 $('li>a[rel]',myul).each(function() {
  396.                     var rel = $(this).attr('rel');
  397.                     var currentNode = $(this).parent()[0].mindmapNode;
  398.                     $.each(rel.split(' '), function(index) {
  399. //                        console.log(this);
  400.                         var parentNode = $('#'+this).parent()[0].mindmapNode;
  401.                         var lineno = lines.length;
  402.                         lines[lineno] = new Line(obj, lineno, currentNode, parentNode);
  403.                         
  404.                     });
  405.                 });


  406.                 
  407.                 //LOOP
  408.                 // Run the update loop on this object
  409.                 var loopCaller = (function(obj) {
  410.                     return function(){
  411.                         Loop(obj);
  412.                     };
  413.                 })(this);
  414.                 //setTimeout(loopCaller, 1);
  415.                 setInterval(loopCaller, 1);
  416.     
  417.                 // Finally add a class to the object, so that styles can be applied
  418.                 $(this).addClass('js-mindmap-active');
  419.     
  420.             }
  421.         });
  422.     };
  423. })(jQuery);