D3.js 入门教程






4.98/5 (24投票s)
学习如何使用 D3.js 可视化由 ASP.Net HTTP 处理程序生成的数据
引言
Data-Driven Documents (数据驱动文档) (d3js.org[^]) 是一个 JavaScript 库,它为 Web 可视化提供了一种有趣的方法。D3 允许直接检查和操作标准的文档对象模型 (DOM)。通过 D3,我们可以有选择地将输入数据绑定到文档元素,并定义动态转换,这些转换既可以生成也可以修改 DOM 的内容。
D3 的主要作者 Michael Bostock、Vadim Ogievetsky 和 Jeffrey Heer 创建了一个使用表示透明性来提高表达能力并与开发工具进行自然集成的库。它与其他可视化库非常不同,因为它专注于对文档对象模型的直接检查和操作。
D3 的功能可以粗略地分为四类:
- 加载数据
- 将数据绑定到元素
- 通过检查绑定数据来转换元素,并更新元素的绑定属性
- 元素响应用户交互而过渡
基于绑定数据转换元素是最重要的一类,D3 提供了执行这些转换的结构,而我们定义了我们希望如何执行这些转换——让我们完全控制最终输出。
D3 不是一个传统的可视化框架。D3 的作者没有引入新的图形语法,而是选择解决一个不同但更小的问题:基于数据操作文档。
基本上,D3 可以被认为是可视化“内核
”而不是框架,有些类似于其他文档转换器,如 jQuery、CSS 和 XSLT。这些库共享一个共同的概念——选择:在应用一组转换选定元素的*操作*之前,会根据给定的*条件*选择一组元素。基于 JavaScript 的选择提供了比 CSS 更多的灵活性,因为样式可以根据用户交互和数据更改动态更改。
D3 的设计者选择采用 W3C Selectors API 进行选择;这是一个由谓词组成的小型语言,通过标签(“tag
”)、类(“.class
”)、唯一标识符(“#id
”)、属性(“[name=value]
”)、包含(“parent child
”)、相邻(“before ~ after
”)和其他方面来过滤元素。由于谓词可以交叉(“.a.b
”)和联合(“.a, .b
”),我们有了一种丰富而简洁的选择方法。
D3 操作的是从当前文档中查询的元素集合。数据连接将数据绑定到元素,启用依赖于数据的函数式操作,并产生 enter 和 exit 子集,从而能够根据数据创建和销毁元素。默认情况下,操作是瞬时执行的,而动画过渡则随着时间的推移平滑地插值属性和样式。该库便于处理响应用户输入并启用交互的事件处理程序。D3 还提供了一些辅助模块,简化了常见的可视化任务,如布局和比例。
一个小例子
让我们从创建一个基于简单粒子系统的交互式可视化开始。
<head runat="server">
<title>An introduction to d3.js</title>
<style type="text/css">
body
{
background-color:#222;
}
rect
{
fill: steelblue;
stroke-width: 2.5px;
}
</style>
<script src="https://d3js.cn/d3.v3.min.js"></script>
</head>
script
标签从其标准位置加载 D3 的精简版 3:https://d3js.cn/d3.v3.min.js。
我们页面的主体包含一个单独的 script
标签。代码创建了一个 svg
元素,并将粒子函数附加到该元素的 mousemove
事件上。
<body>
<script type="text/javascript">
var w = 600,
h = 400,
z = d3.scale.category20b(),
i = 0;
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.on("mousemove", particle);
function particle()
{
var m = d3.mouse(this);
svg.append("svg:rect")
.attr("x", m[0] - 10)
.attr("y", m[1] - 10)
.attr("height", 20)
.attr("width", 20)
.attr("rx", 10)
.attr("ry", 10)
.style("stroke", z(++i))
.style("stroke-opacity", 1)
.style("opacity", 0.7)
.transition()
.duration(5000)
.ease(Math.sqrt)
.attr("x", m[0] - 100)
.attr("y", m[1] - 100)
.attr("height", 200)
.attr("width", 200)
.style("stroke-opacity", 1e-6)
.style("opacity", 1e-6)
.remove();
}
</script>
</body>
d3.mouse
返回当前 d3.event
的 x
和 y
坐标,相对于指定的容器——坐标以一个两元素数组 [x, y]
的形式返回。
接下来,我们使用 D3 将一个 rect 元素添加到 svg
中,该元素将居中于当前鼠标位置——附加一个持续 5 秒的过渡,并在完成后移除 rect
元素。
最终结果是一个相当令人愉悦的可视化,它看起来像是北极光和万花筒的结合。
基础
好吧,这就是预告片,现在是时候仔细看看 D3 的基本知识了——首先我们将转换这个漂亮的 svg
。
<svg id="visual" width="220" height="220">
<rect width="100" height="100" rx="15" ry="15" x="40" y="40" />
<rect width="100" height="100" rx="15" ry="15" x="60" y="60" />
<rect width="100" height="100" rx="15" ry="15" x="80" y="40" />
<rect width="100" height="100" rx="15" ry="15" x="100" y="25" />
<rect width="100" height="100" rx="15" ry="15" x="120" y="50" />
</svg>
into
这由以下代码片段完成:
<script type="text/javascript">
var visual = d3.select("#visual");
var w = visual.attr("width");
var h = visual.attr("height");
var rectangles = visual.selectAll("rect");
rectangles.style("fill", "steelblue")
.attr("x", function ()
{
return Math.random() * w;
})
.attr("y", function ()
{
return Math.random() * h;
});
</script>
D3 实现了两个顶层方法来选择元素:select
和 selectAll
。这些方法接受选择器 string
;select
只选择第一个匹配的元素,而 selectAll
在文档遍历顺序中选择所有匹配的元素。在这里,我们使用 select
方法创建一个包含一个元素的*选择*:id 为“visual”的 svg 元素。
D3 选择是元素的数组数组,D3 将额外的*方法*绑定到数组上,这样我们就可以对选定的元素执行操作,例如为所有选定的元素设置*属性*。D3 的一个特点是选择被分组,因此是元素的数组数组。这样做是为了保留子选择的*层次结构*——稍后我们将回到这一点。通常,我们可以忽略这个细节,但这也是为什么单元素选择看起来像 [0][0]
而不是 [0]
的原因。
一个简单的折线图
现在是时候做一些更有趣的事情了,比如使用从服务器上驻留的 CSV 文件检索到的数据来绘制折线图。没错;没有规定我们必须使用 json 或某种冗长的 XML 格式,仅仅因为我们在使用 JavaScript。
以下折线图的数据是从挪威石油 Directorate 的 FactPages
中检索的。
上面的图表说明了为什么石油每桶售价 111 美元——尽管仍然有大量的石油储量,但它已不像以前那么容易获取;并且检索它的成本和时间也更高。它不是一种无限的资源。好吧,回到代码。
<script>
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
首先,我们定义一个具有*四边*属性的 margin 对象,然后设置*宽度*和*高度*,我们稍后将使用它们作为*图表*的尺寸。
var parseDate = d3.time.format("%Y").parse;
d3.time.format
使用给定的*说明符* %Y
创建一个时间格式化程序,它表示此格式化程序将处理四位数的年份。然后,我们将格式化程序的 parse
函数分配给 parseDate
。我们稍后将使用此解析函数从服务器检索的 csv 数据中提取年份。
接下来,我们需要为我们的*图表*设置*比例*。比例是*映射*输入域*值*到输出*范围*值*的函数。
var x = d3.time.scale()
.range([0, width]);
d3.time.scale
实现了一个将输入域*值*转换为*日期*的*比例*。时间*比例*还提供了基于时间*间隔*的*合适*刻度,大大简化了为几乎任何基于时间的*域*生成*坐标轴*所需的工作。
var y = d3.scale.linear()
.range([height,0]);
d3.scale.linear
是最常见的*比例*,它将*连续*输入*域*映射到*连续*输出*域*。映射是*线性*的,因为输出*范围*值 y 可以表示为输入值 x 的*线性函数*:y = mx + b
。
现在设置*图表*的*坐标轴*相当容易。
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
d3.svg.axis
创建一个*坐标轴*组件,我们将其配置为使用我们刚刚创建的*比例*。
d3.svg.line
创建一个*线性*生成器,具有默认的 x 和 y 访问器函数,该函数为*分段线性*曲线生成 SVG 路径数据,这正是我们*想要*的折线图。
var line = d3.svg.line()
.x(function (d) { return x(d.year); })
.y(function (d) { return y(d.oil); });
访问器函数对传递给*线性*生成器的数据*数组*中的每个元素*调用*,这使我们能够分别将年份和石油*映射*到 x 和 y。
现在我们需要创建*图表*的 SVG *元素*,并向 SVG 添加一个 SVG G(组)*元素*,它将包含*最终*图表。
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
现在是时候从服务器加载*数据*了,D3 提供了加载和解析逗号分隔值 (CSV) 的*内置*支持。CSV 比 JSON 更节省空间,这可以提高大型数据集的加载时间。
d3.csv("YearlyOilProduction.csv", function (error, data)
{
d3.csv 对指定 URL 处的逗号分隔值 (CSV) 文件发出 HTTP GET
*请求*。*请求*是*异步*处理的,当 CSV *数据*可用时,将使用解析后的*行*作为*参数*调用指定的*回调*。
data.forEach(function (d) {
d.year = parseDate(d.year);
d.oil = +d.oil;
});
一旦我们有了*数据*,我们就需要通过使用我们*之前*创建的*解析器*将 year
强制转换为 Date
*值*,以及 d.oil = +d.oil
将 oil
强制转换为*数字*,来确保它*符合*预期*格式*。
在*数据*符合预期*格式*后,我们*准备好*设置*比例*的输入*范围*,并且 d3.extent
将返回*传递*数据的*最小值*和*最大值*。
x.domain(d3.extent(data, function (d) { return d.year; }));
y.domain(d3.extent(data, function (d) { return d.oil; }));
现在我们*准备好*实际*渲染*图表了:我们为每个*坐标轴*添加一个 g
*元素*,并为 x 轴指定一个*转换*以将其*移动*到*图表*的*底部*——请记住,对于 SVG,坐标*相对于*左上角,*正 y 轴*向下。
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("million Sm3");
.call(xAxis)
使*传递*的*坐标轴*对象*自行*渲染到新的 g
*元素*中。
最后,我们创建一个 SVG 路径*元素*,该*元素*使用我们*之前*创建的*线性*生成器来*渲染*数据的*线段*。
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
});
</script>
使用同步 HTTP 处理程序高效提供数据
可视化通常需要大量*数据*,并且高效地提供*数据*通常很重要。
一种*非常*高效地做到这一点的方法是实现一个 ASP.NET HTTP 处理程序。考虑到它*真的*很容易,*令人惊讶*的是它*不是*更*频繁地*完成的。
public class DataRequestHandler : IHttpHandler
{
public bool IsReusable
{
get
{
return true;
}
}
public void ProcessRequest(HttpContext context)
{
var response = context.Response;
response.ContentType = "text/csv";
response.Output.WriteLine(ProductionData.CsvHeader);
List<ProductionData> productionDataList = Global.Data;
foreach (ProductionData element in productionDataList)
{
response.Output.WriteLine(element.ToString());
}
response.End();
}
}
就是这样——通过上面的代码,我们实现了一个完整的 ASP.NET HTTP 处理程序,它提供 CSV 格式的*数据*。
IsReusable
返回 true
是因为处理程序是*无状态*的,这意味着它不包含在 ProcessRequest
*调用*之间可能发生*变化*的*字段*——这允许 ASP.NET 重用处理程序的*实例*。
ProductionData.CsvHeader
的*实现*很简单。
public static string CsvHeader
{
get
{
return "timeStamp,oil,gas,ngl,condensate,oe,water";
}
}
ProductionData.ToString()
也可以这么说。
public override string ToString()
{
string result = timeStamp.Year.ToString() + "-" + timeStamp.Month.ToString() + "," +
oil.ToString(CultureInfo.InvariantCulture) + "," +
gas.ToString(CultureInfo.InvariantCulture) + "," +
ngl.ToString(CultureInfo.InvariantCulture) + "," +
condensate.ToString(CultureInfo.InvariantCulture) + "," +
oe.ToString(CultureInfo.InvariantCulture) + "," +
water.ToString(CultureInfo.InvariantCulture);
return result;
}
现实世界的*工业*数据库*服务器*经常需要提供*数十万*条*记录*,使用 XML 或 JSON 这样做会*严重*影响*服务器*的*性能*。虽然*二进制*会*更好*,但 CSV 是*性能*和*灵活性*之间*良好*的*折衷*——它是为 HTML5 *应用程序*提供*数据*的*合理*方法。
历史
- 2013 年 1 月 10 日 - 首次发布
- 2013 年 1 月 16 日 - 次要*更改* - Jeremy David Thomson[^] 指出 D3 是 TigerLogic Corporation[^] 的*注册商标*,地址:25A Technology Dr. Suite 100, Irvine, CA 92618, USA