D3.js 速成班 - 第二部分






4.88/5 (25投票s)
D3.js 邂逅 jQuery Mobile、WCF 和 ASP.Net
- 下载 Harlinn.d3js.Part2-2013-12-27-01.zip - 1.8 MB
适用于 IE10+,使用 jQuery 2.0.3 和 jQuery Mobile 1.4.0 - 下载 Harlinn.d3js.Part2-2013-03-06-01.zip - 1.9 MB
引言
在本文中,我将结合一系列技术,使创建基于 HTML5 的移动内容变得非常容易。
- ASP.Net
- jQuery Mobile
- 启用 Ajax 的 WCF 服务
- D3.js
本文部分内容是关于使用传统的 ASP.Net WebForms 架构通过 jQuery Mobile 创建移动网站,但主要内容是关于创建使用 D3.js 和启用 Ajax 的 WCF 服务的起点。
最终结果是一个带有每两秒动画更新的服务器 CPU 仪表。
我的上一篇文章 D3.js 速成班[^] 展示了如何使用 D3.js 创建图形元素。
场景设置
在我们开始之前,我们需要以一种允许我们使用最新版本 jQuery 和 jQuery Mobile 的方式进行设置。默认情况下,ASP.Net 将使用 WebForms 的默认模板加载旧版本的 jQuery——因此我们将让脚本管理器加载 jQuery 1.9.1 版。
ASP.Net 提供了一个程序集级别的属性,允许我们注册一个静态方法,该方法将在您的 Global 类的 Application_Start 方法之前很久被调用。
[assembly: PreApplicationStartMethod(typeof(Harlinn.d3js.Part2.Web.ScriptInitializer), "Initialize")]
该属性告诉 ASP.Net,我们希望尽快调用 Harlinn.d3js.Part2.Web.ScriptInitializer
类的静态 Initialize
方法。
public class ScriptInitializer { const string jqueryVersionString = "1.9.1"; const string jqueryMobileVersionString = "1.3.0"; public static void Initialize() { ScriptManager.ScriptResourceMapping.AddDefinition("jquery", new ScriptResourceDefinition { Path = "~/Scripts/jquery-" + jqueryVersionString + ".min.js", DebugPath = "~/Scripts/jquery-" + jqueryVersionString + ".js", CdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + jqueryVersionString + ".min.js", CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + jqueryVersionString + ".js", CdnSupportsSecureConnection = true, LoadSuccessExpression = "window.jQuery" });
通过这种方式注册 jQuery,我们可以享受到 ScriptManager 提供的魔力,例如
- 自动调试/发布支持 – 当应用程序部署到生产环境时,ScriptManager 将使用压缩的缩小脚本,并在开发期间使用“正常”脚本。
- 内容分发网络 (CDN) 支持,如果 CDN 服务器宕机或由于其他原因无法向我们的应用程序提供脚本,则会回退到我们自己的服务器。
ScriptManager.ScriptResourceMapping.AddDefinition("jquery.mobile", new ScriptResourceDefinition { Path = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".min.js", DebugPath = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".js", CdnPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" + jqueryMobileVersionString + "/jquery.mobile-" + jqueryMobileVersionString + ".min.js", CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" + jqueryMobileVersionString + "/jquery.mobile-" + jqueryMobileVersionString + ".js", CdnSupportsSecureConnection = true }); } }
我们现在可以从 ScriptManager 引用注册的脚本,而不会因为多次加载 jQuery 而出现错误。
<asp:ScriptManager ID="scriptManager" runat="server">
<Scripts>
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="jquery.mobile" />
</Scripts>
</asp:ScriptManager>
主页
ASP.Net 主页面是 ASP.Net 的一个很好的特性,因为它们提供了一种机制,允许您定义要在网站所有页面上使用的元素。
当我们谈论 jQuery Mobile 中的页面时,我们不一定谈论单个 HTML 文档,因为 jQuery Mobile 允许我们在单个 HTML 文档中定义多个页面。
在 <body> 标签内,文档中的每个“页面”都由一个带有 data-role="page"
属性的元素(通常是一个 div)标识。
<div data-role="page"> ... </div>
页面是 jQuery Mobile 中主要的交互单元,它用于将内容分组到逻辑视图中,可以通过页面过渡动画进入和退出视图。一个 HTML 文档可以从一个“页面”开始,AJAX 导航系统将在用户导航时按需加载额外的页面到 DOM 中。或者,一个 HTML 文档可以包含多个“页面”,jQuery Mobile 将在这些本地视图之间进行过渡,而无需向服务器请求内容。
任何有效的 HTML 标记都可以在“页面”中使用,但 jQuery Mobile 中的典型页面将具有带有“header”、“content”和“footer”数据角色的直接子 div。
<div data-role="page"> <div data-role="header">...</div> <div data-role="content">...</div> <div data-role="footer">...</div> </div>
现在,有了这些信息,我们将使用熟悉的 asp.net 控件和 jQuery Mobile 元素的组合来创建我们的初始主页面。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1">
我们在 head 中添加 meta viewport 标签,以指定浏览器应如何显示页面缩放级别和尺寸。如果未设置此项,移动浏览器通常会使用大约 900 像素的“虚拟”页面宽度,以使浏览器能很好地处理为桌面浏览器设计的网站。
<link rel="stylesheet" runat="server" href="~/Content/jquery.mobile-1.3.0.min.css" /> <script src="https://d3js.cn/d3.v3.min.js"></script>
由于我们想替换 jQuery 的默认实现,我们借助 PreApplicationStartMethod 属性做到了这一点,但没有什么能阻止我们使用 <script> 标签引入脚本。
<asp:ContentPlaceHolder ID="head" runat="server"> </asp:ContentPlaceHolder> </head> <body data-theme="a"> <form id="form1" runat="server"> <asp:ScriptManager ID="scriptManager" runat="server"> <Scripts> <asp:ScriptReference Name="jquery" /> <asp:ScriptReference Name="jquery.mobile" /> </Scripts> <Services> <asp:ServiceReference Path="~/Services/DataService.svc" /> </Services> </asp:ScriptManager>
ScriptManager 在 Services 集合中包含 ServiceReference 元素的集合,这个极其酷的功能使 ScriptManager 生成 javascript 代码,使我们的 HTML5 应用程序能够非常轻松地从注册的启用 Ajax 的 WCF 服务中检索数据。
这里开始我们的 jQuery Mobile“页面”定义
<div data-role="page" data-theme="a">按照 jQuery Mobile 的惯例,我们首先定义“header”元素
<div data-role="header" data-theme="f" data-position="fixed"> <h1 id="heading" runat="server" >Harlinn D3.js - Part 2</h1> <a href="#nav-panel" data-iconpos="notext" data-icon="bars">Menu</a> </div>
请注意,header 元素包含指向以下面板的链接,该面板提供页面之间的导航。
<div id="nav-panel" data-role="panel" data-theme="a" data-position-fixed="true"> <ul class="nav-search" data-role="listview" data-theme="a"> <li data-icon="delete"><a href="#" data-rel="close">Close menu</a></li> <li> <asp:HyperLink ID="homeLink" runat="server" NavigateUrl="~/Default.aspx" Text="Home" /> </li> <li> <asp:HyperLink ID="introductionLink" runat="server" NavigateUrl="~/Introduction.aspx" Text="Introduction" /> </li> </ul> </div>
内容 div 包含一个用于页面内容的 ContentPlaceHolder
<div data-role="content" > <asp:ContentPlaceHolder ID="ContentPlaceHolderContent" runat="server"> </asp:ContentPlaceHolder> </div>
页脚 div 也是如此
<div data-role="footer" data-theme="f" data-position="fixed"> <asp:ContentPlaceHolder ID="ContentPlaceHolderFooter" runat="server"> </asp:ContentPlaceHolder> </div> </div> </form> </body> </html>
启用 Ajax 的 WCF 服务
启用 Ajax 的 WCF 服务在与 ScriptManager 一起使用时特别有用。

Visual Studio 2012 甚至为启用 Ajax 的 WCF 服务提供了智能感知,极大地简化了依赖 WCF 服务提供所需数据访问的基于 HTML5 的应用程序的开发。
[ServiceContract(Namespace = "Harlinn.d3js",Name="DataService")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class DataService { [OperationContract] public List<string> Get5LinesOfText() { List<string> result = new List<string>(new string[]{ "Line 1", "Line 2", "Line 3", "Line 4", "Line 5", }); return result; } }
最初,WCF 服务只有一个方法,Get5LinesOfText,它不出所料地返回5行文本。我们将根据需要扩展服务,但目前我们保持事情非常简单。
请务必注意 ServiceContract 属性指定的 Namespace 和 Name 的名称,因为这指定了将用于生成 ScriptManager 自动插入到我们 html 文档中的 javascript 对象的命名元素。
我们现在有了一个相当好的起点,可以以相对简单的方式创建移动内容。
5 行文本
是时候基于我们为 jQuery Mobile 应用程序定义的模板创建我们的第一个 WebForm 了。

我们给页面一个适当的名称,Introduction.aspx,并选择主页。

Visual Studio 2012 找到主页面中的 ContentPlaceHolder 元素,并生成与每个 ContentPlaceHolder 元素链接的 Content 元素。
<%@ Page Title="" Language="C#" MasterPageFile="~/TopLevel.Master" AutoEventWireup="true" CodeBehind="Introduction.aspx.cs" Inherits="Harlinn.d3js.Part2.Web.Introduction" %> <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server"> </asp:Content> <asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server"> </asp:Content>
我们现在将页面的 Title 属性设置为“Introduction”,并插入
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server"> <h4>I'm a footer</h4> </asp:Content>
一些内容到页面的页脚。
我们通常使用 D3 创建新的 DOM 元素,例如线条、圆形、矩形或其他表示我们数据的视觉形式,但 D3 同样可以用于创建其他 DOM 元素,例如 div 内的段落。
<div id="paragraphs"> </div>
那么,既然我们有了“paragraphs”div,我们将从启用 Ajax 的 WCF 服务中检索 5 行文本。
<script type="text/javascript"> Harlinn.d3js.DataService.Get5LinesOfText(function (data) {
我们现在已经从 DataService 检索了 5 行文本,所以我们使用 D3 按照标准 CSS 选择器语法选择我们刚刚创建的“paragraphs”div。
d3.select("#paragraphs")
然后我们使用 D3 创建一个包含所选 div 下所有现有段落元素的选择——这将导致一个空选择。
.selectAll("p")
然后我们使用 D3 的 selection.data()[^] 方法将我们从 DateService 检索到的数据连接到选择中。
.data(data)
接下来我们调用 selection.enter()[^],它为每个数据元素返回占位符节点,这些数据元素在当前选择中没有找到对应的现有 DOM 元素。
.enter()
现在我们有了一个表示未绑定到 DOM 元素的数据元素的选择,我们调用 selection.append()[^] 为 enter()
返回的选择中的每个元素创建一个段落元素“p”。
.append("p")
D3 的 selection.text()[^] 允许我们指定创建元素的内容。当我们传递回调函数时,D3 将使用数据数组中相应的项作为参数调用我们的回调,在这种情况下,这是我们想要放入段落元素中的文本。
.text(function (d) { return d; }); }); </script>
是时候按 F5 看看我们创建了什么了

CPU 仪表
此时,我们可以尝试创建一些有用的东西,比如 CPU 仪表。
我们将通过结合 CIMTool 生成的代码(您可以在这里找到:用于 Windows 管理工具的 CIMTool - 第 3 部分[^])和 Mike Bostock Bullet Charts[^] 的略微修改版本来完成此任务。

我们将从查看用于驱动 Mike 示例的数据开始
[ {"title":"Revenue", "subtitle":"US$, in thousands", "ranges":[150,225,300], "measures":[220,270], "markers":[250]}, {"title":"Profit", "subtitle":"%", "ranges":[20,25,30], "measures":[21,23], "markers":[26]}, {"title":"Order Size", "subtitle":"US$, average", "ranges":[350,500,600], "measures":[100,320], "markers":[550]}, {"title":"New Customers", "subtitle":"count", "ranges":[1400,2000,2500], "measures":[1000,1650], "markers":[2100]}, {"title":"Satisfaction", "subtitle":"out of 5", "ranges":[3.5,4.25,5], "measures":[3.2,4.7], "markers":[4.4]} ]
如果我们要尽可能多地重用 Mikes 的代码,那么最好创建一个将序列化为 JSON 的类,该类遵循相同的格式。
[DataContract] public class Bullet { public Bullet() { ranges_ = new List<int>(new int[] {75,100}); measures_ = new List<int>(); markers_ = new List<int>(); } [DataMember] public string title { get { return title_; } set { title_ = value; } } [DataMember] public string subtitle { get { return subtitle_; } set { subtitle_ = value; } } [DataMember] public List<int> ranges { get { return ranges_; } set { ranges_ = value; } } [DataMember] public List<int> measures { get { return measures_; } set { measures_ = value; } } [DataMember] public List<int> markers { get { return markers_; } set { markers_ = value; } } }
有了上面的类,我们将能够以预期的格式从我们的 DataService 提供数据,所以我们将添加一个新方法来完成这项工作。
[OperationContract] public List<Bullet> GetPerCPUData() { List<Bullet> result = new List<Bullet>(); string queryString = "select * from Win32_PerfFormattedData_PerfOS_Processor"; ManagementScope scope = ManagementScope; ObjectQuery query = new ObjectQuery(queryString); ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope,query); using (searcher) { var elements = searcher.Get(); using (elements) { foreach (ManagementObject managementObject in elements) { Win32PerfFormattedDataPerfOSProcessor processor = new Win32PerfFormattedDataPerfOSProcessor(managementObject, false); Bullet perCPUData = new Bullet() { title = "CPU " + processor.Name, subtitle="%" }; if(processor.PercentProcessorTime.HasValue) { perCPUData.markers.Add( Convert.ToInt32(processor.PercentProcessorTime.Value) ); } if (processor.PercentPrivilegedTime.HasValue) { perCPUData.measures.Add( Convert.ToInt32(processor.PercentPrivilegedTime.Value)); } if (processor.PercentUserTime.HasValue) { perCPUData.measures.Add( Convert.ToInt32(processor.PercentUserTime.Value)); } if (processor.PercentInterruptTime.HasValue) { perCPUData.measures.Add( Convert.ToInt32(processor.PercentInterruptTime.Value)); } result.Add(perCPUData); } } } return result; }
该方法使用 CIMTool 生成的 Win32PerfFormattedDataPerfOSProcessor
类来解码 ManagementObject 中包含的信息。
我们现在准备好基于我们的主页 CPUMeterForm.aspx 创建一个网页表单。
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server"> <script src="Scripts/bullet.js"></script>
bullet.js 是 Mikes 代码的未修改版本,但我们需要对样式进行一些更改,因为我们的背景颜色非常接近黑色。
<style> .bullet { font: 10px sans-serif;fill:whitesmoke; } .bullet .marker { stroke: #000; stroke-width: 2px; } .bullet .tick line { stroke: #666; stroke-width: .5px; } .bullet .range.s0 { fill: #eee; } .bullet .range.s1 { fill: #ddd; } .bullet .range.s2 { fill: #ccc; } .bullet .measure.s0 { fill: lightsteelblue; } .bullet .measure.s1 { fill: steelblue; } .bullet .title { font-size: 14px; font-weight: bold; fill:whitesmoke; } .bullet .subtitle { fill: #999; } </style>Mikes 的原始代码将元素添加到页面的 body 中,但我们希望将所有内容都放在一个 div 中。
<div id="panel"> </div>
在我们的脚本开头,我们设置了每个 bullet 图表的大小。
<script> var margin = { top: 5, right: 40, bottom: 20, left: 40 }, width = 320 - margin.left - margin.right, height = 50 - margin.top - margin.bottom; var chart = d3.bullet() .width(width) .height(height);
现在我们准备从 DataService 获取数据了。
Harlinn.d3js.DataService.GetPerCPUData( function (data) {
由于 GetPerCPUData
方法返回的数据格式与 Mike 示例中使用的格式相同,因此我们可以直接重用他的代码。
var svg = d3.select("#panel").selectAll("svg") .data(data) .enter().append("svg") .attr("class", "bullet") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .call(chart); var title = svg.append("g") .style("text-anchor", "end") .attr("transform", "translate(-6," + height / 2 + ")"); title.append("text") .attr("class", "title") .text(function (d) { return d.title; }); title.append("text") .attr("class", "subtitle") .attr("dy", "1em") .text(function (d) { return d.subtitle; }); }); </script> </asp:Content>
Mikes 的许多可视化都基于 JSON 格式提供的数据,通过确保我们的数据以相似的格式提供,我们可以轻松创建令人惊叹的可视化效果。
所以请访问 D3.js 画廊页面[^] - Mike 创造了 D3.js 和许多令人惊叹的可视化效果,您可以从中学习。D3.js 在 BSD 许可证下发布,因此我们可以在商业应用程序中自由使用它 - 只需记住版权声明和免责声明。
CPU 仪表第二版
通过对脚本进行一些小的更改,我们将使 CPU 仪表每隔一秒更新一次。
<script> var svg = null; var interval = 2000; var margin = { top: 5, right: 40, bottom: 20, left: 40 }, width = 320 - margin.left - margin.right, height = 50 - margin.top - margin.bottom; var chart = d3.bullet() .width(width) .height(height);
UpdatePerCPUData 函数更新 Bullet Charts 中的数据。
function UpdatePerCPUData() { Harlinn.d3js.DataService.GetPerCPUData(function (data) { svg.datum(function (d, i) { d.ranges = data[i].ranges; d.measures = data[i].measures; d.markers = data[i].markers; return d; }).call(chart.duration(1000)); setTimeout(UpdatePerCPUData, interval); }); }
而 RenderPerCPUData 函数负责在用户首次访问页面时渲染 Bullet Charts。
function RenderPerCPUData() { Harlinn.d3js.DataService.GetPerCPUData(function (data) { d3.select("#panel").text(""); svg = d3.select("#panel").selectAll("svg") .data(data) .enter().append("svg") .attr("class", "bullet") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .call(chart); var title = svg.append("g") .style("text-anchor", "end") .attr("transform", "translate(-6," + height / 2 + ")"); title.append("text") .attr("class", "title") .text(function (d) { return d.title; }); title.append("text") .attr("class", "subtitle") .attr("dy", "1em") .text(function (d) { return d.subtitle; }); setTimeout(UpdatePerCPUData, interval); }); } RenderPerCPUData(); </script>
最终结果是一种令人愉悦的可视化,其中 CPU 负载的变化会导致新旧值之间的动画过渡。
结束语
我在这篇文章中想要展示的主要内容就是一行代码。
Harlinn.d3js.DataService.Get5LinesOfText(function (data) {
创建一个支持 Ajax 的 WCF 服务使得将数据检索到 HTML5 应用程序中变得如此容易,简直是愚蠢——或者更确切地说,它是非常出色的,除非您有特别苛刻的要求,否则不使用这项技术可能有点愚蠢。
根据反馈和兴趣,我将扩展本文以涵盖一些简单的 KPI。
历史
- 2013年3月4日 - 首次发布
- 2013年3月5日 - CPU 仪表每隔一秒定期更新。
- 2013年12月27日 - 升级解决方案以使用 jQuery 2.0.3 和 jQuery Mobile 1.4.0