二、用于可视化的 JavaScript 和 HTML5
在前一章中,我提到 HTML5 的一些发展使得可视化变得更加容易。 本章将探讨其中几个,即以下内容:
- 帆布
- 可缩放矢量图形(SVG)
如果您熟悉这两个工具的功能,您可能想跳过这一章。
帆布
Canvas 是 HTML5 的功能之一,它在技术文章和演示中发挥得最多。 用画布可以完成的事情是非常令人印象深刻的,所以它出现得如此频繁也就不足为奇了。 Canvas 为绘图提供了低级的位图接口。 你可以把它想象成浏览器中的 Microsoft Paint。 在画布上生成的图像都是栅格图像,这意味着图像是由像素网格构建的,而不是像矢量图像那样由一组几何对象组成。 与绘制在画布上的元素的交互必须通过过滤器和全局转换完成; 精确的控制即使不是不可能,也是有问题的。
在页面上创建一个 canvas 元素是简单的。 你只需要添加如下的 HTML 元素:
<canvas width="200" height="200">Alternate text here</canvas>
提示
下载示例代码
您可以从您的账户http://www.packtpub.com下载您购买的所有 pack 书籍的示例代码文件。 如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,直接将文件通过电子邮件发送给您。
你也可以在https://github.com/stimms/SocialDataVisualizations找到这本书的所有代码示例。
这将创建一个正方形画布 200 x 200 像素。 如果所使用的浏览器不支持 canvas 元素,则会显示替代文本。 幸运的是,您很少会看到这个警告,因为 canvas 有广泛的支持。 到 2013 年年中,87%的互联网用户可以使用 canvas。 有关最新数据,请查看http://caniuse.com/#feat=canvas。 该网站还提供了其他 HTML5 功能的浏览器支持信息。 它甚至支持 iOS、Android、黑莓和 Windows Phone 的移动浏览器。 唯一不支持 canvas 的常用浏览器是 ie9 之前的版本。
如果您的目标用户大量使用旧版本的 Internet Explorer,那么一切都没有损失。 有一个方便的 JavaScriptpolyfill; 一段可下载的代码,为旧版本的 Internet Explorer 提供画布功能。 该图书馆可登录https://code.google.com/p/explorercanvas。 要使用它,可以使用条件包含,如下面的代码片段所示:
<head>
<!--[if lt IE 9]><script src="excanvas.js"></script><![endif]-->
…
</head>
如果运行的浏览器是旧版本的 Internet Explorer,这将只包括excanvas.js
。
Excanvas不完全支持 canvas,缺少阴影和 3D 等功能,但绝大多数功能可用。 使用 JavaScript 版本的 canvas 也会影响性能,因此,如果您使用动画,它们可能不会像在现代浏览器上那样流畅。 为了接触到剩下的少数用户,这是一个很小的代价。 随着浏览器的更新,这个问题将变得越来越不重要。
在画布上绘制简单的形状很容易,但也可以绘制一些非常复杂的对象,包括 3D 绘图。 我们将讨论一些在创建可视化时有用的更简单的形状和函数。
在我们开始绘图之前,我们先介绍一下 canvas 的坐标系统。 坐标系统的原点位于左上角,并向右下角增长。 在画布上作画,如下图所示,使用 JavaScript:
绘制的第一步是获取您想要绘制的画布实例的句柄。 当然,您可以在一个页面上拥有任意多的画布元素。 在这里,我们将创建一个 ID 为demo
的画布:
<canvas height="200" width="200" id="demo"></canvas>
现在我们可以使用标准的 JavaScript 方法来选择元素:
var demoCanvas = document.getElementById("demo");
或者,如果你正在使用一个 CSS 选择器库,比如 jQuery,你可以使用:
var demoCanvas = $("#demo")[0];
下一步是获取绘图上下文本身的引用。 context 包含一组用于绘图的方法,我们将在所有画布操作中使用它:
var context = demoCanvas.getContext("2d");
Canvas 支持许多基本形状,可以根据这些形状构建复杂对象。 最简单的形状是较低的矩形。 可以通过调用strokeRect()
函数来创建:
context.strokeRect(20, 30, 100, 50);
这将创建一个矩形,起始坐标为(20,30),宽度为 100 像素,高度为 50 像素。 事实上,这将绘制先前显示的矩形,但向右移动 20 个像素,向下移动 30 个像素。 这个方法的特点是给出矩形起始点的 x, y 坐标然后是宽度和高度。 除strokeRect()
功能外,还有fillRect
、clearRect
功能。 在画布上作画时,可以画一条线条,画一个填充结构,或清除一个区域的内容。
如果矩形不是你的风格,也许你会对画一个圆更感兴趣? Canvas 实际上认为圆是弧线的一种特殊情况。 因此,要绘制半径为 50px 的圆,不仅需要指定圆心和半径,还需要指定起始和结束角度。 这种圆的代码如下:
context.arc(75, 75, 50, 0, 2*Math.PI);context.fill();
这里,中心点为(75,75),半径为 50。 第四项是起始角,我们设为 0,而2*Math.PI
是结束角,即绕圆的整个角度。 一个可选的 final 参数决定是否逆时针绘制弧线。 默认为false
或clockwise
。
提示最终参数给出不同的弧,如下截图所示:
如你所见,画布中使用的所有角度都用弧度表示。 要将以度表示的角度转换为以弧度表示的角度,可以将其乘以Pi/180
。
对于更复杂的形状,canvas 支持直线或路径。 要画一条线,你设置一个起始坐标,然后给出一个结束点,,然后调用stroke()
函数。 Canvas 将推断出在哪里画线,如下面的代码片段所示:
context.beginPath();
context.moveTo(0,0);
context.lineTo(50,120);
context.closePath();
context.stroke();
在这里,我们开始一条新的道路; 笔被移动到(0,0),然后线被绘制到(50,120),然后路径结束。 开始和结束路径是很重要的,否则后续对stroke()
函数的调用将导致从最后一点开始的延续。 你可以把它想象成使用一支笔; beginPath
将笔放在纸上,moveTo
将笔暂时抬起并移动,lineTo
将笔移动到目标位置并在后面画一条线,最后closePath
将笔收回纸上。 不用拿起笔,下一次当你在纸上的某个地方画线时,笔就已经在纸上了,你就会得到一条额外的线。
如果您认为绘制直线的语法有点晦涩难懂,那么您并不孤单。 多个调用使您能够构建具有多个段的更复杂的行。 通过使用循环,我们可以构建相对复杂的形状,如下面的代码片段所示:
var width = 200;
var height = 200;
context.moveTo(0, 0);
for(i = 0; i> 100; I += 4)
{
context.lineTo(i, height-i);
context.lineTo(width - 1-i, height - 1 -i);
context.lineTo(width - 2-i, i+2);
context.lineTo(i+3, i+3);
}
context.stroke();
这段代码将生成一个螺旋。 在循环的每一次迭代中,我们向图像的中心移动 4 个像素,每个边缘一个像素。 结果如下截图所示:
画布在调用之间保持一种状态,因此,如果你使用context.fillStyle
设置填充的颜色,所有后续填充形状的调用将采用相同的填充样式。 可以想象,这是使用 canvas 构建可视化时常见的错误来源。 在调用函数对画布进行操作时,问题尤其严重。
幸运的是,有一个简单的解决方案:可以在输入和退出函数时保存和恢复上下文状态。 出于礼貌,不要让你的函数与全局状态纠缠在一起,这肯定会减少 bug 的数量:
function fillCircle(context, x, y, radius){
context.save();
context.fillStyle = "orange";
context.beginPath();
context.arc(x, y, radius, 0, 2* Math.PI);
context.fill();
context.restore();
}
在函数的第一行,当前绘图上下文被推到堆栈上,然后在函数的最后一行恢复它。 通过这样做,我们可以根据自己的喜好对方法中的上下文进行尽可能多的更改,并确保调用者的上下文不会被破坏。
画布支持一个完整的调色板颜色,也允许透明度甚至梯度。 到目前为止,这些示例都使用了颜色名称。 这些名称可以追溯到 20 世纪 90 年代中期,实际上来自 X11 窗口系统。 然而,还有两种方法可以为画布指定颜色。 第一种方法是使用一个十六进制字符串,将红色、绿色和蓝色的值指定为两位十六进制数字。 值越大,该颜色的强度越高,如下图所示:
最后,也是我的偏好,是使用十进制 RGB 值,如下图所示,我认为这更可读,也更容易以编程方式构建:
小数格式的一个轻微变体是使用rgba
函数代替rgb
。 这将添加一个额外的参数,它是一个介于 0 和 1 之间的小数,表示不透明度。 1 是完全不透明的,0 是完全透明的。
由于 canvas 是一个基于栅格的绘图系统,它可以包含大多数其他栅格文件。 栅格文件格式包括 JPEG、PNG、GIF 和 BMP。 能够导入现有的图像是一种非常方便的可视化工具。
然而,在画布上使用图像有一些注意事项。 当你包含一张图片时,你不能直接从 URL 加载它。 首先,需要创建一个 JavaScript Image 对象,并将该图像的来源设置为 URL。 这个 Image 对象可以在画布上使用:
Var image = new Image();
image.src = "logo.png";
从另一个域请求图像可能会很棘手。 为了维护请求数据的浏览器的安全性,其他域受到了限制。 对于镜像,您可以通过在镜像上设置crossOrigin
属性来请求托管域使用镜像的权限:
Var image = new Image();
image.crossOrigin = "anonymous";
image.src = "http://codinghorror.typepad.com/.a/6a0120a85dcdae970b0128776ff992970c-pi";
此处,已将crossOrigin
策略的值设置为anonymous
。 这意味着浏览器不会将任何身份验证信息传递给托管映像的服务器。 如果想传递凭据信息,还可以设置用户凭据的值。 对图像的crossOrigin
支持相对较新,所以您最好将图像托管在与画布相同的域上。
因为加载图像可能需要一些时间,所以不建议在图像上设置src
的值,然后立即尝试在画布上绘制它。 相反,你应该连接到图像上的onload
事件:
function drawImage()
{
var img = new Image();
img.src = 'worksonmymachine.png';
img.onload = function(){
var context = document.getElementById('example').getContext('2d');
context.drawImage(img, 0, 0);
}
}
使用onload
函数将防止空图像被渲染到画布上。 如果你正在加载多个图像,它们加载的顺序是不确定的。 在继续之前,您可能希望检查所有图像是否已加载。 使用 jQuery 的Deferred
功能可以管理复杂的依赖链。 drawImage
的第二个和第三个参数是绘制图像的坐标。 有更多的高级版本的drawImage
,允许缩放和裁剪图像之前绘制到画布上。
canvas 的最后一个特点是变换。 当构成更复杂的场景或可视化时,通常更容易以不同的比例、位置或方向构建对象。 转换提供了一种改变在画布上绘制的形状的机制。
一个函数,在这里是缩放函数,将每个坐标乘以 x 或 y 缩放因子。 Canvas 提供了独立缩放 x 和 y 值的功能。 这意味着很容易向一个方向拉伸一个形状:
var colours = ["rgba(255,0,0,.5)", "rgba(0,255,0,.5)", "rgba(0,0,255,.5)"];
for(var i = 0; i< 3; i++)
{
context.fillStyle = colours[i];
context.scale(i+1,i+1);
context.fillRect(10, 10, 50, 50);
}
前面的代码只是简单地绘制了一系列的三个矩形,它将生成如下截图所示的输出:
您会注意到,我们的缩放不仅创建了更大的图像,还将矩形的坐标相乘。 另一件需要注意的事情是,体型的增长似乎不是均匀的。 这是因为 canvas 是有状态的。 如果您在一个系列中应用多个缩放函数而不重置缩放,则每个函数将在最后一个缩放的基础上构建。 您可以跟踪您的转换并应用一个反转函数。 举个例子,如果你缩放了 2,你可以缩放 2 的乘法逆,也就是 1/2,回到原来的缩放。 使用我们前面提到的save
和restore
函数更容易保存和恢复上下文。
如果我们修改我们的函数,你可以看到结果图像是非常不同的,如下面的截图所示:
通常,当应用缩放转换时,您会希望应用转换来将起始坐标移动到您期望的位置。 你可以通过使用翻译转换来做到这一点:
var x = y =10;
var width = height = 100;
for(var i = 2; i><= 0; i--)
{
var scalingFactor = i+1;
context.save();
context.fillStyle = colours[i];
context.translate(x * -i, y *-i);
context.scale(scalingFactor, scalingFactor);
context.fillRect(x, y, width, height);
context.restore();
}
在高亮的线条上,我们移动画布,使它看起来就像矩形是在原点绘制的。 转换的应用顺序很重要,如下面的截图所示:
在左边,您可以看到我们代码的输出,而在右边,则是交换translate
和scale
操作的结果。
有许多其他伟大的功能的画布。 对于这么长的一本书来说,事无巨细都讲得太多了; 它很有可能是一本书。
可扩展矢量图形
可伸缩矢量图形,或者更常用的是 svg,是 HTML 的另一个相对较新的特性。 它们的作用与 canvas 相似,但不同之处在于它们是基于矢量而不是基于栅格的。 这意味着每个图像都是由一系列基本形状组成的。 这听起来有点像 canvas,毕竟我们使用基本形状创建了所有的 canvas 图像。 不同之处在于,在使用 SVG 时,基本形状在绘制之后仍然是不同的对象。 使用 canvas,用于创建图像的源命令在图像被渲染时就会丢失。 关于像素源的信息丢失在画布命令的混乱中。
方便的是,svg 被存储为 XML 文件。 虽然我通常不会考虑存储任何东西,但组合到我的保险箱,在这样一个不可访问的文件格式,它确实与 HTML 很好地集成。 栅格图像通常链接在一个独立于 HTML 的文件中。 svg 可以直接嵌入到 HTML 文档中。 这种技术可用于减少在用户代理上呈现页面所需的服务器访问次数。 然而,真正的优势是,它可以将 SVG 集成到HTML 文档对象模型(DOM),允许您操作可以使用 SVG 使用相同的技术来处理任何其他元素。
简单 SVG 的源代码如下所示:
<svg version="1.1">
<rect width="50" height="150" x="20" y="20" stroke="black"stroke-width="2" fill="#a3a3a3" />
</svg>
你可以将其粘贴到任何 HTML 文档中,你会得到一个简单的矩形,如下图所示:
代码很容易理解,语法应该对任何创建过网站的人都很熟悉。 我们首先打开新的 SVG 元素。 在没有任何显式大小调整信息的情况下,SVG 填充其容器。 在 SVG 中,我们创建一个宽度为 50px,高度为 150px 的新矩形。 矩形的轮廓是黑色的,宽度为 2px,而内部填充灰色。
既然您已经看到了 canvas 的实际使用情况,那么可以用来构建图像的原语也应该有些熟悉了。 rect
和path
从画布上保持不变。 然而,SVG 在处理圆形时有所不同,它为带有两个焦点的圆形提供了实际的<circle>
标签和<elipse>
标签。 <polygon>
和<polyline>
标签可绘制任意形状的直边形状,多边形为填充形状,折线为直线。 如果你想要更弯曲的形状,SVG 提供了一个path
元素,允许定义复杂的曲线和弧线。 手工建造一条弯曲的路径是非常棘手的。 通常,对于曲线路径,您需要使用编辑器或 SVG 库。 最后,SVG 支持使用名称恰当的<text>
元素编写文本。
在 SVG 中构建多个元素就像在 SVG 中添加另一个子元素一样简单,如下面的代码片段所示:
>svg version="1.1"<
>rect width="50" height="150" x="20" y="20" stroke="black" stroke-width="2" fill="#a3a3a3" /<
>rect width="50" height="120" x="75" y="50" stroke="black" stroke-width="2" fill="#a3a3a3"/<
>rect width="50" height="90" x="130" y="80" stroke="black" stroke-width="2" fill="#a3a3a3"/<
>rect width="50" height="60" x="185" y="110" stroke="black" stroke-width="2" fill="#a3a3a3"/<
>/svg<
结果如下图所示:
如您所见,代码非常重复。 在每个矩形上,我们指定stroke
和fill
信息。 有两种方法可以消除这种重复。 第一种方法是使用一组元素来定义样式信息。 SVG 提供了一个由<g>
标记表示的通用分组容器。 样式信息可以应用于该容器而不是单个元素:
>g stroke="black" stroke-width="2" fill="#a3a3a3"<
>rect width="50" height="150" x="20" y="20" /<
>rect width="50" height="120" x="75" y="50" /<
>rect width="50" height="90" x="130" y="80" /<
>rect width="50" height="60" x="185" y="110" /<
>/g<
另一种选择是使用 CSS 做样式为您:
>style<
rect {
fill: #f3f3f3;
stroke: black;
stroke-width: 2;
}
>/style<
>svg version="1.1" id="graph1"<
>rect width="50" height="150" x="20" y="20" /<
>rect width="50" height="120" x="75" y="50" /<
>rect width="50" height="90" x="130" y="80" /<
>rect width="50" height="60" x="185" y="110" /<
>/svg<
在前面的代码中,样式信息直接附加到类型为rect
的所有元素。 通常,您会希望避免使用诸如此类的宽选择器,因为它们将应用于页面上的所有 SVG 元素。 最好是缩小选择器的范围,要么使它们只应用于一个 SVG,要么最好将一个类分配给希望样式化的 SVG 元素。 它通常首选于使用 CSS 对元素(即使是属于 SVG 的元素)进行样式设置。 很可能您的 SVG 将包含多个矩形,您不希望它们具有相同的样式。
CSS 中使用的样式属性(fill,``stroke
,等等)与 SVG 标记中使用的样式属性没有区别。 更高级的 CSS 选择器也可用,如nth-child
,它只选择匹配特定模式的子元素。 考虑以下代码片段:
rect:nth-child(even)
{
fill:#878787;
}
在我们的示例中添加上述代码将非常简单地在我们的图上创建一个斑马条纹效果,如下面的截图所示:
我们甚至可以通过简单地在 CSS 中指定一个:hover
伪选择器并改变光标下的颜色来使用 CSS 为我们的图添加一些交互作用:
rect:hover
{
fill: rbg(87,152,176);
}
下面的截图显示了结果图:
当然,将 SVG 作为 DOM 的一部分为样式之外的其他因素打开了大门。 还可以使用 JavaScript 操作 SVG 的元素。 甚至可以将事件监听器分配给 SVG 元素。
通过使用奇妙的 jQuery 库,我们可以很容易地将事件监听器添加到 SVG 图表中的节点中:
$("rect").click(function(){
alert($(this).attr("height"));
});
如果你以前从未见过 jQuery,这里发生的事情是,我们正在选择页面上的所有rect
元素,当点击被触发时,它会打开一个警告框,显示我们点击的列的高度。
在本书中,我们将广泛使用 jQuery 库和这种基于 lambda 的编程风格。 如果你不熟悉 jQuery,建议你休息一下,阅读一些教程,如http://try.jquery.com/。
我们已经介绍了 svg 的所有基本功能,但还有一个高级特性我想提一下:过滤器。 filter是可以应用于 SVG 元素的转换。 这些过滤器超越了我们在 canvas 中看到的缩放和转换转换,尽管 SVG 支持scale
和translate
。 大约有 20 个这样的过滤器,每个都执行不同的转换。 我们不可能每一个都讲,但我们会看几个。
可视化中最常见的需求之一是给事物一种 3D 的感觉。 全 3D 可能非常困难,但我们可以通过使用阴影欺骗眼睛。 这些阴影可以使用三种不同的滤镜组合创建:偏移、高斯模糊和混合。
要使用筛选器,首先要定义它。 一个filter
元素可以定义为一系列按顺序应用的过滤器。 为了弄清楚阴影需要什么滤镜,我们可以从阴影的属性逆向工作。 首先要注意的是阴影与投射它们的物体是相抵消的。 为此,我们可以使用偏移过滤器,将元素向一个或另一个方向移动。 你想在哪里移动元素取决于你的光源在哪里。 出于我们的目的,我们假设它位于 SVG 的上方和左侧。 这将投射阴影向下和向右:
>defs<
>filter id="shadowFilter" width="175%" height="175%"<
>feOffset result="offsetImage" in="SourceAlpha" dx="5" dy="5"/<
>/filter<
>/defs<
在滤镜线上,我们需要为过滤后的元素指定一个高度,这个高度要大于原始元素的高度。 如果我们不这样做,当我们的阴影延伸到源物体的边界之外时,它的大部分将被切断。 在这里,我们给了过滤器一个 ID,以便稍后应用它。 您还会注意到,我们已经为feOffset
指定了in
和out
属性。 这允许我们将过滤器链接在一起。 在我们的例子中,我们从原始图像中提取SourceAlpha
,也就是 alpha,或者transparency
属性。
让我们将这个过滤器应用到图中的一个元素上,这样我们就可以看到发生了什么。 我已经删除了其他样式,以免混淆问题。 过滤器是通过使用filter
属性应用的,并给它之前创建的过滤器的 ID:
<rect width="50" height="60" x="185" y="110" filter="url(#shadowFilter)"/>
结果如下:
阴影也比原始图像更模糊。 这可以通过使用高斯模糊滤镜实现:
<feGaussianBlur result="blurredOffset" in="offsetImage" stdDeviation="8" />
高斯滤波器基于一个正态分布函数在你的图像中随机移动点。 你可能希望使用标准偏差来实现不同的模糊效果; 我发现在 8-12 的范围内对阴影效果很好:
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
最后,我们想把我们创建的模糊黑盒和原始的结合起来:
<feBlend in="SourceGraphic" in2="blurredOffset" mode="normal" />
将这个滤镜应用于鼠标悬停,当用户将鼠标悬停在图像上时,会产生非常令人信服的弹出效果,如下面的截图所示:
SVG 提供了易于操作图像的各个部分的样式化工具,这些工具在使用 CSS 时已经很熟悉了。 同时,能够将事件附加到图像上,可以创建令人印象深刻的用户交互。
用哪一个?
决定是使用 canvas 还是 SVG 可能是一个困难的问题。 这主要取决于哪一种感觉更舒服。 那些有计算机图形或动画背景的人可能更喜欢使用 canvas,因为 canvas 的redraw
循环将是熟悉的。 Canvas 更适合重绘整个场景,即使你计划使用 3D 元素。 如果你的可视化使用纹理或渲染图像,那么画布直接将它们绘制到画布上的能力几乎肯定是有利的。 对于依赖于保持快速帧率的可视化效果,canvas 通常是高性能的。
另一方面,SVG 是一种使用起来简单得多的技术。 SVG 中的每个元素都可以单独操作,这使得小动画更加容易。 与 DOM 的集成允许在与 SVG 的单个元素交互时触发事件。 要在画布中实现这一点,您必须手动跟踪在该位置绘制的内容。 SVG 还可以使用 CSS 进行样式设置,这使得组件在具有不同主题的站点上更容易重用。
出于本书的目的,我们将重点关注 svg。 SVG 的分辨率独立性,加上的易用性和出色的支持库,使它成为一个合乎逻辑的选择。 我不相信我们会创造出一种不能用画布创造的视觉效果,但我们会付出更大的努力。 这对于交互式可视化来说更是如此。
小结
现在,您应该能够在 SVG 和画布构建简单静态图像之间做出明智的决定。 我们将在下一章从视觉方面休息一下,讨论 OAuth 协议,它被许多社交媒体网站用来保护他们的数据。
版权属于:月萌API www.moonapi.com,转载请注明出处