重初始化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(); });
问题根源
原插件存在以下缺陷:
- 内部变量
obj_timestamp为全局变量,重复调用会导致数据污染 - 未清理目标容器的旧DOM和事件绑定,重复调用会引发冲突
- 部分内部函数依赖全局作用域,异步调用时作用域混乱
修改方案
修改插件核心逻辑,使其支持重复调用和异步更新,以下是完整修改后的插件代码:
/** * 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);
关键改动说明
- 将全局变量
obj_timestamp改为局部变量,避免数据污染 - 每次渲染前清空目标容器并解绑旧事件,防止重复绑定冲突
- 内部函数改为依赖参数传递数据,消除全局作用域依赖
- 增加默认
texts配置,避免未传入时出错 - 返回jQuery对象,支持链式调用
修改后,原异步更新代码无需改动,直接重复调用github_graph方法即可实现数据更新。
内容的提问来源于stack exchange,提问作者DPJ
相关产品推荐
相关产品推荐

