在這個由兩部分組成的系列中,我們將結合多功能的畫布元素和強大的 jQuery 庫來創建條形圖插件。在第二部分中,我們將把它轉換為 jQuery 插件,然後添加一些養眼的東西和其他功能。
#結束畫布的樂趣兩部分系列,今天我們將創建一個條形圖插件;請注意,這不是一個普通的插件。我們將展示 jQuery 對 canvas 元素的熱愛,以創建一個非常強大的外掛。
在第一部分中,我們只專注於將外掛程式的邏輯實作為獨立腳本。在第一部分結束時,我們的長條圖看起來像這樣。
第 1 部分最後的結果
在最後一部分中,我們將致力於轉換我們的程式碼並使其成為一個合適的 jQuery 插件,添加一些視覺細節,最後包括一些附加功能。最終,我們的輸出將如下所示:
成品
都熱身了嗎?讓我們開始吧!
在我們開始將程式碼轉換為插件之前,我們首先需要了解插件創作的一些手續。
我們先為外掛程式選擇一個名稱。我選擇了 barGraph 並將 JavaScript 檔案重新命名為 jquery.barGraph.js。現在,我們將上一篇文章中的所有程式碼包含在以下程式碼片段中。
$.fn.barGraph = function(settings) { //code here }
設定包含傳遞給外掛程式的所有可選參數。
在 jQuery 外掛程式創作中,通常會考慮使用 jQuery 來取代程式碼中的 $ 別名,以盡量減少與其他 Javascript 函式庫的衝突。我們可以使用 jQuery 文件中提到的自訂別名,而不用經歷所有這些麻煩。我們將所有外掛程式碼包含在這個自執行匿名函數中,如下所示:
(function($) { $.fn.barGraph = function(settings) { //plugin implementation code here } })(jQuery);
本質上,我們將所有程式碼封裝在一個函數中並將 jQuery 傳遞給它。現在,我們可以在程式碼中隨意使用 $ 別名,而不必擔心它與其他 JavaScript 函式庫可能發生衝突。
在設計插件時,最好向使用者公開合理數量的設置,同時如果使用者使用插件而不向其傳遞任何選項,則使用合理的預設選項。考慮到這一點,我們將允許使用者更改我在本系列的上一篇文章中提到的每個圖形選項變數。這樣做很容易;我們只需將每個變數定義為物件的屬性,然後存取它們。
var defaults = { barSpacing = 20, barWidth = 20, cvHeight = 220, numYlabels = 8, xOffset = 20, maxVal, gWidth=550, gHeight=200; };
我們最終需要將預設選項與傳遞的選項合併,並優先考慮傳遞的選項。這一行負責處理這個問題。
var option = $.extend(defaults, settings);
請記住在必要時更改變數名稱。如 -
return (param*barWidth)+((param+1)*barSpacing)+xOffset;
...更改為:
return (param*option.barWidth)+((param+1)*option.barSpacing)+option.xOffset;
這是插件被敲定的地方。我們的舊實作只能在頁面中產生單一圖形,而在頁面中建立多個圖形的能力是我們為此功能建立外掛程式的主要原因。另外,我們需要確保使用者不需要為每個要建立的圖表建立一個畫布元素。考慮到這一點,我們將根據需要動態建立畫布元素。讓我們繼續吧。我們將查看程式碼相關部分的早期版本和更新版本。
在開始之前,我想指出我們的插件將如何被呼叫。
$("#years").barGraph ({ barSpacing = 30, barWidth = 25, numYlabels = 12, });
就這麼簡單。 years 是保存我們所有值的表的 ID。我們根據需要傳遞選項。
首先,我們首先需要引用圖表的資料來源。我們現在訪問來源元素並獲取其 ID。將以下行新增到我們先前聲明的一組圖形變數中。
var dataSource = $(this).attr("id");
我們定義一個新變數並將所傳遞元素的 ID 屬性的值指派給它。在我們的程式碼中,this 指的是目前選定的 DOM 元素。在我們的範例中,它引用 ID 為 years 的表。
在先前的實作中,資料來源的 ID 是硬編碼的。現在我們將其替換為先前提取的 ID 屬性。 grabValues 函數的早期版本如下:
function grabValues () { // Access the required table cell, extract and add its value to the values array. $("#data tr td:nth-child(2)").each(function(){ gValues.push($(this).text()); }); // Access the required table cell, extract and add its value to the xLabels array. $("#data tr td:nth-child(1)").each(function(){ xLabels.push($(this).text()); }); }
更新為:
function grabValues () { // Access the required table cell, extract and add its value to the values array. $("#"+dataSource+" tr td:nth-child(2)").each(function(){ gValues.push($(this).text()); }); // Access the required table cell, extract and add its value to the xLabels array. $("#"+dataSource+" tr td:nth-child(1)").each(function(){ xLabels.push($(this).text()); }); }
function initCanvas () { $("#"+dataSource).after("<canvas id=\"bargraph-"+dataSource+"\" class=\"barGraph\"> </canvas>"); // Try to access the canvas element cv = $("#bargraph-"+dataSource).get(0); if (!cv.getContext) { return; } // Try to get a 2D context for the canvas and throw an error if unable to ctx = cv.getContext('2d'); if (!ctx) { return; } }
我们创建一个canvas元素并将其注入到表格之后的DOM中,该表格充当数据源。 jQuery 的 after 函数在这里非常方便。还应用了 barGraph 的类属性和 barGraph-dataSourceID 格式的 ID 属性,以使用户能够根据需要将它们全部设置为组或单独设置样式。 p>
有两种方法可以实际调用此插件。您可以单独创建每个图表并仅传入一个数据源,也可以传入多个数据源。在后一种情况下,我们当前的构造将遇到错误并退出。为了纠正这个问题,我们使用 each 构造来迭代传递的元素集。
(function($){ $.fn.barGraph = function(settings) { // Option variables var defaults = { // options here }; // Merge the passed parameters with the defaults var option = $.extend(defaults, settings); // Cycle through each passed object this.each(function() { // Implementation code here }); // Returns the jQuery object to allow for chainability. return this; } })(jQuery);
我们在获取并合并 this.each 构造中的设置后封装了所有代码。我们还确保最后返回 jQuery 对象以实现可链接性。
至此,我们的重构就完成了。我们应该能够调用我们的插件并根据需要创建尽可能多的图表。
现在我们的转换已经完成,我们可以努力使其视觉效果更好。我们将在这里做很多事情。我们将分别研究它们。
旧版本使用温和的灰色来绘制图表。我们现在将为酒吧实施主题机制。这本身由一系列步骤组成。
海洋:默认主题
树叶
樱花
频谱
var defaults = { // Other defaults here theme: "Ocean", };
我们在默认设置中添加了一个主题选项,使用户能够将主题更改为四个可用预设中的任何一个。
function grabValues () { // Previous code switch(option.theme) { case 'Ocean': gTheme = thBlue; break; case 'Foliage': gTheme = thGreen; break; case 'Cherry Blossom': gTheme = thPink; break; case 'Spectrum': gTheme = thAssorted; break; } }
一个简单的switch构造查看option.theme设置并将gTheme变量指向必要的颜色数组。我们对主题使用描述性名称,而不是通用名称。
// Themes var thPink = ['#FFCCCC','#FFCCCC','#FFC0C0','#FFB5B5','#FFADAD','#FFA4A4','#FF9A9A','#FF8989','#FF6D6D']; var thBlue = ['#ACE0FF','#9CDAFF','#90D6FF','#86D2FF','#7FCFFF','#79CDFF','#72CAFF','#6CC8FF','#57C0FF']; var thGreen = ['#D1FFA6','#C6FF91','#C0FF86','#BCFF7D','#B6FF72','#B2FF6B','#AAFE5D','#A5FF51','#9FFF46']; var thAssorted = ['#FF93C2','#FF93F6','#E193FF','#B893FF','#93A0FF','#93D7FF','#93F6FF','#ABFF93','#FF9B93'];
然后我们定义许多数组,每个数组保存一系列特定颜色的色调。它们从较浅的色调开始并不断增加。稍后我们将循环遍历这些数组。添加主题就像添加您需要的特定颜色的数组一样简单,然后修改之前的开关以反映更改。
function getColour (param) { return Math.ceil(Math.abs(((gValues.length/2) -param))); }
这是一个很小的函数,可以让我们实现类似渐变的效果并将其应用于图表。本质上,我们计算要渲染的值数量的一半与传递的参数(即数组中当前所选项目的索引)之间的绝对差。这样,我们就能够创建平滑的渐变。由于我们只在每个颜色数组中定义了九种颜色,因此我们的图表仅限于十八个值。扩展这个数字应该是相当微不足道的。
function drawGraph () { for(index=0; index<gValues.length; index++) { ctx.save(); ctx.fillStyle = gTheme[getColour(index)]; ctx.fillRect( x(index), y(gValues[index]), width(), height(gValues[index])); ctx.restore(); } }
这是我们实际为图表设置主题的地方。我们没有为 fillStyle 属性设置静态值,而是使用 getColour 函数来检索当前所选主题数组中元素的必要索引。
接下来,我们将让用户能够控制所绘制条形的不透明度。设置过程分为两步。
不透明
值为 0.8
var defaults = { // Other defaults here barOpacity : 0.8, };
我们在默认值中添加了一个 barOpacity 选项,使用户能够将图形的不透明度更改为 0 到 1 之间的值,其中 0 是完全透明,1 是完全不透明。
function drawGraph () { for(index=0; index<gValues.length; index++) { ctx.save(); ctx.fillStyle = gTheme[getColour(index)]; ctx.globalAlpha = option.barOpacity; ctx.fillRect( x(index), y(gValues[index]), width(), height(gValues[index])); ctx.restore(); } }
globalAlpha 属性控制渲染元素的不透明度或透明度。我们将此属性的值设置为传递的值或默认值以增加一点透明度。作为合理的默认值,我们使用值 0.8 使其稍微透明。
网格对于处理图表中呈现的数据非常有用。虽然我最初想要一个合适的网格,但后来我选择了一系列与 Y 轴标签对齐的水平线,并完全抛弃了垂直线,因为它们只是妨碍了数据。解决了这个问题,让我们来实现一种渲染它的方法。
禁用网格
启用网格
使用路径和lineTo方法创建线条似乎是绘制图形最明显的解决方案,但我碰巧遇到了一个渲染错误,这使得这种方法不适合。因此,我也坚持使用 fillRect 方法来创建这些线。这是完整的函数。
function drawGrid () { for(index=0; index<option.numYlabels; index++) { ctx.fillStyle = "#AAA"; ctx.fillRect( option.xOffset, y(yLabels[index])+3, gWidth, 1); } }
这与绘制 Y 轴标签非常相似,只不过我们不是渲染标签,而是绘制一条横跨图形宽度、宽度为 1 px 的水平线。 y 函数帮助我们定位。
var defaults = { // Other defaults here disableGrid : false, };
我们在默认值中添加了一个 disableGrid 选项,使用户能够控制是否渲染网格。默认情况下,它是渲染的。
// Function calls if(!option.disableGrid) { drawGrid(); }
我们只是检查用户是否希望渲染网格并进行相应操作。
现在条形图都已着色,在较浅的背景下缺乏强调。为了纠正这个问题,我们需要 1px 的描边。有两种方法可以做到这一点。第一种也是最简单的方法是在 drawGraph 方法中添加一个 strokeRect 方法;或者,我们可以使用 lineTo 方法来快速绘制矩形。我选择了前一条路线,因为像之前一样,lineTo 方法向我抛出了一些奇怪的渲染错误。
没有抚摸
抚摸
首先,我们将其添加到defaults对象中,以便用户控制是否应用它。
var defaults = { // Other defaults here showOutline : true, };
function drawGraph () { // Previous code if (option.showOutline) { ctx.fillStyle = "#000"; ctx.strokeRect( x(index), y(gValues[index]), width(), height(gValues[index])); } // Rest of the code } }
我们检查用户是否想要渲染轮廓,如果是,我们继续。这与渲染实际条形几乎相同,只是我们使用 tripleRect 方法而不是使用 fillRect 方法。
在原始实现中,画布元素本身和条形的实际渲染空间之间没有区别。我们现在就纠正这个问题。
无底纹
有底纹
function shadeGraphArea () { ctx.fillStyle = "#F2F2F2"; ctx.fillRect(option.xOffset, 0, gWidth-option.xOffset, gHeight); }
这是一个很小的函数,可以遮蔽所需区域。我们覆盖画布元素减去两个轴标签覆盖的区域。前两个参数指向起点的x和y坐标,后两个参数指向所需的宽度和高度。从 option.offset 开始,我们消除了 Y 轴标签覆盖的区域,并通过将高度限制为 gHeight,我们消除了 X 轴标签。
现在我们的图表看起来足够漂亮了,我们可以集中精力向我们的插件添加一些新功能。我们将分别讨论每一个。
考虑这张著名的 8K 峰值图。
当最高值足够高,并且大多数值落在最大值的 10% 以内时,图表就不再有用。我们有两种方法来纠正这个问题。
我们将首先从更简单的解决方案开始。通过将各个图表的值呈现在顶部,实际上解决了问题,因为可以轻松地区分各个值。下面是它的实现方式。
var defaults = { // Other defaults here showValue: true, };
首先,我们向 defaults 对象添加一个条目,以使用户能够随意打开和关闭它。
// Function calls if(option.showValue) { drawValue(); }
我们检查用户是否希望显示该值并进行相应处理。
function drawValue () { for(index=0; index<gValues.length; index++) { ctx.save(); ctx.fillStyle= "#000"; ctx.font = "10px 'arial'"; var valAsString = gValues[index].toString(); var valX = (option.barWidth/2)-(valAsString.length*3); ctx.fillText(gValues[index], x(index)+valX, y(gValues[index])-4); ctx.restore(); } }
我们迭代gValues数组并单独渲染每个值。涉及 valAsString 和 valX 的计算只不过是帮助我们正确缩进的微小计算,因此它看起来并不不合适。
这是两个解决方案中更难的一个。在此方法中,我们不是从 0 开始 Y 轴标签,而是从更接近最小值的位置开始。我们边走边解释。请注意,在上面的示例中,后续值相对于最大值之间的差异非常微不足道,并且没有显示出其有效性。其他数据集应该更容易解析结果。
var defaults = { // Other defaults here scale: false };
由于scale函数是渲染过程中不可或缺的一部分,因此我们需要更新它以允许缩放功能。我们像这样更新它:
function scale (param) { return ((option.scale) ? Math.round(((param-minVal)/(maxVal-minVal))*gHeight) : Math.round((param/maxVal)*gHeight)); }
我知道这看起来有点复杂,但它看起来只是因为使用了三元条件运算符。本质上,我们检查 option.scale 的值,如果它为 false,则执行旧代码。如果为真,我们现在不会将值标准化为数组中最大值的函数,而是将其标准化为最大值和最小值之差的函数。这让我们想到:
我们现在需要找出最大值和最小值,而不是之前只能找出最大值。函数更新为:
function minmaxValues (arr) { maxVal=0; for(i=0; i<arr.length; i++) { if (maxVal<parseInt(arr[i])) { maxVal=parseInt(arr[i]); } } minVal=maxVal; for(i=0; i<arr.length; i++) { if (minVal>parseInt(arr[i])) { minVal=parseInt(arr[i]); } } maxVal*= 1.1; minVal = minVal - Math.round((maxVal/10)); }
我确信您可以在一个循环中完成相同的任务,而无需使用像我一样多的代码行,但当时我感觉特别没有创造力,所以请耐心等待。完成计算手续后,我们将 maxVal 变量增加 5%,将 minVal 变量增加 5%,减去 minVal 的 5% >maxVal 的 值。这是为了确保条形不会每次都接触顶部,并且每个 Y 轴标签之间的差异是均匀的。
完成所有基础工作后,我们现在继续更新 Y 轴标签渲染例程以反映缩放。
function drawYlabels() { ctx.save(); for(index=0; index<option.numYlabels; index++) { if (!option.scale) { yLabels.push(Math.round(maxVal/option.numYlabels*(index+1))); } else { var val= minVal+Math.ceil(((maxVal-minVal)/option.numYlabels)*(index+1)); yLabels.push(Math.ceil(val)); } ctx.fillStyle = option.labelColour; var valAsString = yLabels[index].toString(); var lblX = option.xOffset - (valAsString.length*7); ctx.fillText(yLabels[index], lblX, y(yLabels[index])+10); } if (!option.scale) { ctx.fillText("0", option.xOffset -7, gHeight+7); } else { var valAsString = minVal.toString(); var lblX = option.xOffset - (valAsString.length*7); ctx.fillText(minVal, lblX, gHeight+7); } ctx.restore(); }
如果你问我的话,更新内容相当丰富!功能的核心保持不变。我们只是检查用户是否启用了扩展并根据需要分支代码。如果启用,我们会更改 Y 标签的分配方式,以确保它们遵循新算法。现在,我们不再将最大值划分为 n 个均匀间隔的数字,而是计算最大值和最小值之间的差值,将其划分为均匀间隔的数字,并将其添加到最小值以构建 Y 轴标签数组。之后,我们照常进行,单独渲染每个标签。由于我们手动渲染了最底部的 0,因此我们必须检查是否启用了缩放,然后在其位置渲染最小值。不要介意每个传递参数的小数字添加;只是为了确保图表的每个元素都按预期排列。
在我们之前的实现中,我们对图表的维度进行了硬编码,当值的数量发生变化时,这会带来很大的困难。我们现在要纠正这个问题。
var defaults = { // Other defaults here cvHeight: 250, //In px };
我们让用户单独设置canvas元素的高度。所有其他值都是动态计算的并根据需要应用。
initCanvas 函数处理所有画布初始化,因此需要更新以实现新功能。
function initCanvas () { $("#"+dataSource).after("<canvas id=\"bargraph-"+dataSource+"\" class=\"barGraph\"> </canvas>"); // Try to access the canvas element cv = $("#bargraph-"+dataSource).get(0); cv.width=gValues.length*(option.barSpacing+option.barWidth)+option.xOffset+option.barSpacing; cv.height=option.cvHeight; gWidth=cv.width; gHeight=option.cvHeight-20; if (!cv.getContext) { return; } // Try to get a 2D context for the canvas and throw an error if unable to ctx = cv.getContext('2d'); if (!ctx) { return; } }
注入canvas元素后,我们获得了对所创建元素的引用。画布元素的宽度计算为数组中元素数量的函数 - gValues ,每个条之间的空间 - option.barSpacing ,每个条本身的宽度- option.barWidth 和最后option.xOffset。图表的宽度根据每个参数动态变化。高度是用户可修改的,默认为 220 像素,栏本身的渲染区域为 220 像素。 20px 分配给 X 轴标签。
创建图表后,用户可能希望隐藏源表,这是有道理的。考虑到这一点,我们让用户决定是否删除该表。
var defaults = { // Other defaults here hideDataSource: true, };
if (option.hideDataSource) { $("#"+dataSource).remove();}
我们检查用户是否想要隐藏表格,如果是,我们使用 jQuery 的 remove 方法将其从 DOM 中完全删除。
现在所有的艰苦工作都已经完成,我们可以回顾一下如何优化我们的代码。由于该代码完全是为了教学目的而编写的,因此大部分工作都被封装为单独的函数,而且它们比需要的要冗长得多。
如果您确实想要尽可能精简的代码,我们的整个插件(不包括初始化和计算)可以在两个循环内重写。一个循环遍历 gValues 数组来绘制条形本身和 X 轴标签;第二个循环从 0 迭代到 numYlabels 以渲染网格和 Y 轴标签。代码看起来会更加混乱,但是,它应该会导致代码库明显更小。
就是这样,伙计们!我们完全从头开始创建了一个高级插件。我们研究了本系列中的许多主题,包括:
我希望您在读这篇文章时和我在写它时一样享受乐趣。这是一个 270 多行的作品,我确信我遗漏了一些东西。欢迎点击评论并询问我。或者批评我。或者夸奖我。你知道,这是你的决定!快乐编码!
以上是繼續 Canvas 的樂趣:建立條形圖插件,第 2 部分的詳細內容。更多資訊請關注PHP中文網其他相關文章!