You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重初始化Github-Contribution-Graph插件失败,求异步更新方案

解决GitHub风格活动图插件异步更新失效问题

问题描述

我使用一款JS插件生成GitHub风格活动图,首次初始化能正常加载,但通过AJAX异步获取新数据后,尝试重新调用插件方法渲染时失败,报错:
Uncaught TypeError: $(...).github_graph is not a function

原初始化代码

var processedData = [];

$(document).ready(function () {
});

$('#activityMap').github_graph({
    data: processedData,
    texts: ['activity', 'activities']
});

异步更新代码

function get_activity() {
    $.ajax({
        url: "?handler=LoadActivity",
        success: function (data) {
            var activities = JSON.parse(data).activities;
            processedData = processActivitiesData(activities);
            // 尝试重初始化失败
            $('#activityMap').github_graph({
                data: processedData,
                texts: ['activity', 'activities']
            });
        },
        error: function (error) {

        }
    });
}

调用逻辑

$(document).ready(function () {
   get_activity();
});

问题根源

原插件存在以下缺陷:

  1. 内部变量obj_timestamp为全局变量,重复调用会导致数据污染
  2. 未清理目标容器的旧DOM和事件绑定,重复调用会引发冲突
  3. 部分内部函数依赖全局作用域,异步调用时作用域混乱

修改方案

修改插件核心逻辑,使其支持重复调用和异步更新,以下是完整修改后的插件代码:

/**
 * Jquery plugin to render like contribution graph on Github.
 *
 * @author    bachvtuan@gmail.com
 * @license   MIT License
 * @since     0.1.0
 */

//Format string
if (!String.prototype.formatString) {
  String.prototype.formatString = function () {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function (match, number) {
      return typeof args[number] != "undefined" ? args[number] : match;
    });
  };
}

(function ($) {
  $.fn.github_graph = function (options) {
    //If the number less than 10, add Zero before it
    var prettyNumber = function (number) {
      return number < 10
        ? "0" + number.toString()
        : number.toString();
    };

    /*
        Count the number on each day and store the object
        */
    var processListTimeStamp = function (list_timestamp) {
      var obj_timestamp = {};
      for (var i = 0; i < list_timestamp.length; i++) {
        var _type = typeof list_timestamp[i];
        var _d =
          _type == "number"
            ? new Date(list_timestamp[i])
            : new Date(list_timestamp[i].timestamp);

        var display_date = getDisplayDate(_d);
        var increase = _type == "number" ? 1 : list_timestamp[i].count;
        if (!obj_timestamp[display_date]) {
          obj_timestamp[display_date] = increase;
        } else {
          obj_timestamp[display_date] += increase;
        }
      }
      return obj_timestamp;
    };

    var getDisplayDate = function (date_obj) {
      var pretty_month = prettyNumber(date_obj.getMonth() + 1);
      var pretty_date = prettyNumber(date_obj.getDate());
      return "{0}-{1}-{2}".formatString(
        date_obj.getFullYear(),
        pretty_month,
        pretty_date,
      );
    };

    var getCount = function (display_date, obj_timestamp) {
      if (obj_timestamp[display_date]) {
        return obj_timestamp[display_date];
      }
      return 0;
    };

    var getColor = function (count, settings) {
      if (typeof settings.colors[0] == "string") {
        return count > settings.colors.length - 1
          ? settings.colors[settings.colors.length - 1]
          : settings.colors[count];
      }

      const isLargeNumber = (element) => element.count > count;
      var i = settings.colors.findIndex(isLargeNumber);
      return i == -1
        ? settings.colors[settings.colors.length - 1].color
        : settings.colors[i - 1].color;
    };

    var start = function (_this, settings) {
      var obj_timestamp = processListTimeStamp(settings.data);
      var wrap_chart = _this;

      // 清空容器并解绑旧事件,避免重复绑定冲突
      wrap_chart.empty().off('click mouseenter mouseleave');

      settings.colors_length = settings.colors.length;
      var radius = settings.border.radius;
      var hoverColor = settings.border.hover_color;
      var clickCallback = settings.click;

      var start_date;
      if (settings.start_date == null) {
        start_date = new Date();
        start_date.setMonth(start_date.getMonth() - 12);
        start_date.setDate(start_date.getDate() + 1);
      } else {
        start_date = new Date(settings.start_date);
      }

      var end_date = new Date(start_date);
      end_date.setMonth(end_date.getMonth() + 12);
      end_date.setDate(end_date.getDate() - 1);

      var loop_html = "";
      var step = 13;

      var month_position = [];
      month_position.push({ month_index: start_date.getMonth(), x: 0 });
      var using_month = start_date.getMonth();

      var week = 0;
      var g_x = week * step;
      var item_html = '<g transform="translate(' + g_x.toString() + ', 0)">';

      for (
        ;
        start_date.getTime() <= end_date.getTime();
        start_date.setDate(start_date.getDate() + 1)
      ) {
        if (start_date.getDay() == 0) {
          var item_html =
            '<g transform="translate(' + g_x.toString() + ', 0)">';
        }

        var month_in_day = start_date.getMonth();
        var data_date = getDisplayDate(start_date);

        if (start_date.getDay() == 0 && month_in_day != using_month) {
          using_month = month_in_day;
          month_position.push({ month_index: using_month, x: g_x });
        }
        var count = getCount(data_date, obj_timestamp);
        var color = getColor(count, settings);

        var y = start_date.getDay() * step;
        item_html +=
          '<rect class="day" width="11" height="11" y="' +
          y +
          '" fill="' +
          color +
          '" data-count="' +
          count +
          '" data-date="' +
          data_date +
          '" rx="' +
          radius +
          '" ry="' +
          radius +
          '"/>';

        if (start_date.getDay() == 6) {
          item_html += "</g>";
          loop_html += item_html;

          item_html = null;

          week++;
          g_x = week * step;
        }
      }

      if (item_html != null) {
        item_html += "</g>";
        loop_html += item_html;
      }

      if (month_position.length > 1 && month_position[1].x - month_position[0].x < 40) {
        month_position.shift();
      }

      for (var i = 0; i < month_position.length; i++) {
        var item = month_position[i];
        var month_name = settings.month_names[item.month_index];
        loop_html +=
          '<text x="' +
          item.x +
          '" y="-5" class="month">' +
          month_name +
          "</text>";
      }

      loop_html +=
        '<text text-anchor="middle" class="wday" dx="-10" dy="22">{0}</text>'.formatString(
          settings.h_days[0],
        ) +
        '<text text-anchor="middle" class="wday" dx="-10" dy="48">{0}</text>'.formatString(
          settings.h_days[1],
        ) +
        '<text text-anchor="middle" class="wday" dx="-10" dy="74">{0}</text>'.formatString(
          settings.h_days[2],
        );

      var wire_html =
        '<svg width="721" height="110" viewBox="0 0 721 110"  class="js-calendar-graph-svg">' +
        '<g transform="translate(20, 20)">' +
        loop_html +
        "</g>" +
        "</svg>";

      wrap_chart.html(wire_html);

      wrap_chart
        .find(".day")
        .on("click", function () {
          if (clickCallback) {
            clickCallback(
              $(this).attr("data-date"),
              parseInt($(this).attr("data-count")),
            );
          }
        });

      wrap_chart
        .find(".day")
        .hover(
          function () {
            $(this).attr("style", "stroke-width: 1; stroke: " + hoverColor);
          },
          function () {
            $(this).attr("style", "stroke-width:0");
          },
        );

      wrap_chart.find("rect").on("mouseenter", mouseEnter.bind(null, settings));
      wrap_chart.find("rect").on("mouseleave", mouseLeave);
      appendTooltip();
    };

    var mouseLeave = function (evt) {
      $(".svg-tip").hide();
    };

    var mouseEnter = function (settings, evt) {
      var target_offset = $(evt.target).offset();
      var count = $(evt.target).attr("data-count");
      var date = $(evt.target).attr("data-date");

      var count_text = count > 1 ? settings.texts[1] : settings.texts[0];
      var text = "{0} {1} on {2}".formatString(count, count_text, date);

      var svg_tip = $(".svg-tip").show();
      svg_tip.html(text);
      var svg_width = Math.round(svg_tip.width() / 2 + 5);
      var svg_height = svg_tip.height() * 2 + 10;

      svg_tip.css({ top: target_offset.top - svg_height - 5 });
      svg_tip.css({ left: target_offset.left - svg_width });
    };

    var appendTooltip = function () {
      if ($(".svg-tip").length == 0) {
        $("body").append(
          '<div class="svg-tip svg-tip-one-line" style="display:none" ></div>',
        );
      }
    };

    var settings = $.extend(
      {
        colors: ["#eeeeee", "#d6e685", "#8cc665", "#44a340", "#44a340"],
        border: {
          radius: 2,
          hover_color: "#999",
        },
        click: null,
        start_date: null,
        month_names: [
          "Jan",
          "Feb",
          "Mar",
          "Apr",
          "May",
          "Jun",
          "Jul",
          "Aug",
          "Sep",
          "Oct",
          "Nov",
          "Dec",
        ],
        h_days: ["M", "W", "F"],
        data: [],
        texts: ['contribution', 'contributions']
      },
      options,
    );

    var _this = $(this);

    start(_this, settings);
    return this;
  };
})(jQuery);

关键改动说明

  1. 将全局变量obj_timestamp改为局部变量,避免数据污染
  2. 每次渲染前清空目标容器并解绑旧事件,防止重复绑定冲突
  3. 内部函数改为依赖参数传递数据,消除全局作用域依赖
  4. 增加默认texts配置,避免未传入时出错
  5. 返回jQuery对象,支持链式调用

修改后,原异步更新代码无需改动,直接重复调用github_graph方法即可实现数据更新。

内容的提问来源于stack exchange,提问作者DPJ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 06:57:25