mistic100 / jQCloud

jQuery plugin for drawing neat word clouds that actually look like clouds
mistic100.github.io/jQCloud
MIT License
268 stars 106 forks source link

autoresize lead to overlapping words #33

Open rammanoj opened 6 years ago

rammanoj commented 6 years ago

On using JQcloud in a bootstrap modal. If I use autoresize event in JQCloud, everything works fine if bootstrap modal is opened and zoom operation is performed but when the bootstrap modal is closed and zoom operations are performed on the browser then if the modal is opened again then entire words are being overlapped.

I tried it by removing autoresize: true then it worked fine. But some words are being overlapped now because of performing zoom-in operations many times.

So, is there any way to perform autoresize without using "autoresize: true" or is there any way to prevent overlapping of words on using autoresize

HenroOrg commented 3 years ago

Maybe it's a resize timing problem. Internally, it is supposed to resize after 50 ms, but there are times when resize faster than the displayed timing. In this case, an override occurs because the dimension of the element is not set in the non-display state. First add below to jqcloud.js.

(function (factory) {
    if (typeof define == 'function' && define.amd) {
      // AMD
      define(['jquery'], factory);
    } else if (typeof exports === 'object') {
      // Node, CommonJS
      module.exports = factory(require('jquery'));
    } else {
        // Browser globals
      factory(jQuery);
    }
  }(function ($) {

    var inviewObjects = [], viewportSize, viewportOffset,
        d = document, w = window, documentElement = d.documentElement, timer;

    $.event.special.inview = {
      add: function(data) {
        inviewObjects.push({ data: data, $element: $(this), element: this });
        // Use setInterval in order to also make sure this captures elements within
        // "overflow:scroll" elements or elements that appeared in the dom tree due to
        // dom manipulation and reflow
        // old: $(window).scroll(checkInView);
        //
        // By the way, iOS (iPad, iPhone, ...) seems to not execute, or at least delays
        // intervals while the user scrolls. Therefore the inview event might fire a bit late there
        //
        // Don't waste cycles with an interval until we get at least one element that
        // has bound to the inview event.
        if (!timer && inviewObjects.length) {
           timer = setInterval(checkInView, 250);
        }
      },

      remove: function(data) {
        for (var i=0; i<inviewObjects.length; i++) {
          var inviewObject = inviewObjects[i];
          if (inviewObject.element === this && inviewObject.data.guid === data.guid) {
            inviewObjects.splice(i, 1);
            break;
          }
        }

        // Clear interval when we no longer have any elements listening
        if (!inviewObjects.length) {
           clearInterval(timer);
           timer = null;
        }
      }
    };

    function getViewportSize() {
      var mode, domObject, size = { height: w.innerHeight, width: w.innerWidth };

      // if this is correct then return it. iPad has compat Mode, so will
      // go into check clientHeight/clientWidth (which has the wrong value).
      if (!size.height) {
        mode = d.compatMode;
        if (mode || !$.support.boxModel) { // IE, Gecko
          domObject = mode === 'CSS1Compat' ?
            documentElement : // Standards
            d.body; // Quirks
          size = {
            height: domObject.clientHeight,
            width:  domObject.clientWidth
          };
        }
      }

      return size;
    }

    function getViewportOffset() {
      return {
        top:  w.pageYOffset || documentElement.scrollTop   || d.body.scrollTop,
        left: w.pageXOffset || documentElement.scrollLeft  || d.body.scrollLeft
      };
    }

    function checkInView() {
      if (!inviewObjects.length) {
        return;
      }

      var i = 0, $elements = $.map(inviewObjects, function(inviewObject) {
        var selector  = inviewObject.data.selector,
            $element  = inviewObject.$element;
        return selector ? $element.find(selector) : $element;
      });

      viewportSize   = viewportSize   || getViewportSize();
      viewportOffset = viewportOffset || getViewportOffset();

      for (; i<inviewObjects.length; i++) {
        // Ignore elements that are not in the DOM tree
        if (!$.contains(documentElement, $elements[i][0])) {
          continue;
        }

        var $element      = $($elements[i]),
            elementSize   = { height: $element[0].offsetHeight, width: $element[0].offsetWidth },
            elementOffset = $element.offset(),
            inView        = $element.data('inview');

        // Don't ask me why because I haven't figured out yet:
        // viewportOffset and viewportSize are sometimes suddenly null in Firefox 5.
        // Even though it sounds weird:
        // It seems that the execution of this function is interferred by the onresize/onscroll event
        // where viewportOffset and viewportSize are unset
        if (!viewportOffset || !viewportSize) {
          return;
        }

        if (elementOffset.top + elementSize.height > viewportOffset.top &&
            elementOffset.top < viewportOffset.top + viewportSize.height &&
            elementOffset.left + elementSize.width > viewportOffset.left &&
            elementOffset.left < viewportOffset.left + viewportSize.width) {
          if (!inView) {
            $element.data('inview', true).trigger('inview', [true]);
          }
        } else if (inView) {
          $element.data('inview', false).trigger('inview', [false]);
        }
      }
    }

    $(w).on("scroll resize scrollstop", function() {
      viewportSize = viewportOffset = null;
    });

    // IE < 9 scrolls to focused elements without firing the "scroll" event
    if (!documentElement.addEventListener && documentElement.attachEvent) {
      documentElement.attachEvent("onfocusin", function() {
        viewportOffset = null;
      });
    }
  }));

And change the initialize as below.

    initialize: function() {
        // Set/Get dimensions
        if (this.options.width) {
            this.$element.width(this.options.width);
        }
        else {
            this.options.width = this.$element.width();
        }
        if (this.options.height) {
            this.$element.height(this.options.height);
        }
        else {
            this.options.height = this.$element.height();
        }

        // Default options value
        this.options = $.extend(true, {}, jQCloud.DEFAULTS, this.options);

        // Ensure delay
        if (this.options.delay === null) {
            this.options.delay = this.word_array.length > 50 ? 10 : 0;
        }

        // Backward compatibility
        if (this.options.center.x > 1) {
            this.options.center.x = this.options.center.x / this.options.width;
            this.options.center.y = this.options.center.y / this.options.height;
        }

        // Create colorGenerator function from options
        // Direct function
        if (typeof this.options.colors == 'function') {
            this.colorGenerator = this.options.colors;
        }
        // Array of sizes
        else if ($.isArray(this.options.colors)) {
            var cl = this.options.colors.length;
            if (cl > 0) {
                // Fill the sizes array to X items
                if (cl < this.options.steps) {
                    for (var i = cl; i < this.options.steps; i++) {
                        this.options.colors[i] = this.options.colors[cl - 1];
                    }
                }

                this.colorGenerator = function(weight) {
                    return this.options.colors[this.options.steps - weight];
                };
            }
        }

        // Create sizeGenerator function from options
        // Direct function
        if (typeof this.options.fontSize == 'function') {
            this.sizeGenerator = this.options.fontSize;
        }
        // Object with 'from' and 'to'
        else if ($.isPlainObject(this.options.fontSize)) {
            this.sizeGenerator = function(width, height, weight) {
                var max = width * this.options.fontSize.from,
                    min = width * this.options.fontSize.to;
                return Math.round(min + (max - min) * 1.0 / (this.options.steps - 1) * (weight - 1)) + 'px';
            };
        }
        // Array of sizes
        else if ($.isArray(this.options.fontSize)) {
            var sl = this.options.fontSize.length;
            if (sl > 0) {
                // Fill the sizes array to X items
                if (sl < this.options.steps) {
                    for (var j = sl; j < this.options.steps; j++) {
                        this.options.fontSize[j] = this.options.fontSize[sl - 1];
                    }
                }

                this.sizeGenerator = function(width, height, weight) {
                    return this.options.fontSize[this.options.steps - weight];
                };
            }
        }

        this.data.angle = Math.random() * 6.28;
        this.data.step = (this.options.shape === 'rectangular') ? 18.0 : 2.0;
        this.data.aspect_ratio = this.options.width / this.options.height;
        this.clearTimeouts();

        // Namespace word ids to avoid collisions between multiple clouds
        this.data.namespace = (this.$element.attr('id') || Math.floor((Math.random() * 1000000)).toString(36)) + '_word_';

        this.$element.addClass('jqcloud');

        // Container's CSS position cannot be 'static'
        if (this.$element.css('position') === 'static') {
            this.$element.css('position', 'relative');
        }

        // Delay execution so that the browser can render the page before the computatively intensive word cloud drawing
        this.createTimeout($.proxy(this.drawWordCloud, this), 10);

        // Attach window resize event
        if (this.options.autoResize) {
            $(window).on('resize.' + this.data.namespace, throttle(this.resize, 50, this));
        }
        //console.log("root : "+this.options.width+", "+this.options.height+"("+this.$element.width()+", "+this.$element.height()+")");

        var parent = this;
        this.$element.on('inview',function(event,isInView){
            if(isInView){
                //console.log("show:"+$(this).width() + "," + $(this).height());
                parent.resize();
            }else{
                //console.log("hide:"+$(this).width() + "," + $(this).height());
            }
        });

    },

Finally, change resize as below.

    resize: function() {
        var new_size = {
            width: this.$element.width(),
            height: this.$element.height()
        };
        //console.log("resize:"+new_size.width + ", "+new_size.height);
        //console.log("visible:"+this.$element.is(':visible'));
        if (this.$element.is(':visible') && (new_size.width != this.options.width || new_size.height != this.options.height)) {
            this.options.width = new_size.width;
            this.options.height = new_size.height;
            this.data.aspect_ratio = this.options.width / this.options.height;

            this.update(this.word_array);
        }
    },