D3 中的刻度和轴





0/5 (0投票)
学习如何使用比例尺和坐标轴创建 D3 图表。
引言
当图表周围有坐标轴时,图表更有意义。坐标轴指示了平面上表示的特定数据的数量。这是一篇关于如何在 D3 图表周围创建带比例尺的坐标轴的操作指南。
使用代码
D3 提供了 .json() 方法来从 URL 加载 JSON 数据。此方法接受两个参数——第一个是返回 JSON 数据的 URL,第二个是处理服务器(在我这里是当前目录)返回的数据的匿名方法。此匿名方法本质上是异步的,最多接受两个参数。以下行从我当前目录中的 JSON 文件加载数据。
<code>var svgHeight = 500; var svgWidth = 800; var paddingH = 50; var paddingV = 50; var dataset = undefined; var maxX, xScale, yScale, radiusScale, colorXScale, colorYScale; /*----- Working with the D3 rendering -----*/ var svg = d3.select('body') .append('svg') .attr('width', svgWidth) .attr('height', svgHeight); d3.json('points.json', function ( error, data ) { if(error == null || error == undefined) { dataset = data; renderTheGraph(); } else { window.alert('Something wrong happened while loading the data from JSON file. Try again.'); } }); </code>
这里的匿名方法接受两个参数:error 和 data。如果数据未从服务器加载,则 error 将包含一个值;如果加载成功,则将其分配给 data。数据加载后,将其分配给全局变量 dataset,该变量可用于在平面上表示数据。
在这里,我的数据非常简单,表示具有权重的点。只需假设这些数据是由另一个进程生成的。数据是——
<code>[ { "x" : 5, "y" : 20, "weight" : 0.48 }, { "x" : 34, "y" : 1565, "weight" : 0.9 }, { "x" : 153, "y" : 530, "weight" : 0.7 }, { "x" : 51, "y" : 247, "weight" : 0.5 }, { "x" : 447, "y" : 290, "weight" : 0.74 }, { "x" : 45, "y" : 120, "weight" : 0.63 }, { "x" : 351, "y" : 29, "weight" : 0.95 }, { "x" : 654, "y" : 160, "weight" : 0.31 }, { "x" : 315, "y" : 320, "weight" : 0.17 }, { "x" : 50, "y" : 632, "weight" : 0.57 }, { "x" : 50, "y" : 20, "weight" : 0.64 }, { "x" : 159, "y" : 200, "weight" : 0.83 }, { "x" : 512, "y" : 170, "weight" : 0.19 }, { "x" : 445, "y" : 512, "weight" : 0.77 } ] </code>
这里的目的是根据 x 和 y 值在 x-y 平面上绘制点,并根据权重分别赋予每个点大小和颜色。例如,第一个点的取值为——
<code> { "x" : 5, "y" : 20, "weight" : 0.48 } </code>
它将在 (5, 20) 处绘制一个点,强度为 0.48。我所说的强度是,权重值越小,半径越小,点的透明度越高。所有这些都由 d3 比例尺处理。
一旦 .json() 方法加载了数据并将其保存在 dataset 变量中,我们就调用 renderChart() 函数。该方法如下——
<code>function renderTheGraph() { setScales(); var circles = svg.selectAll('circle') .data(dataset) .enter() .append('circle'); circles.attr('cx', function ( d ) { console.log("The x - " + xScale(d.x)); return xScale(d.x); }) .attr('cy', function ( d ) { console.log("The y - " + d.y); return yScale(d.y); }) .attr('r', function ( d ) { console.log("The radius - " + d.weight); return radiusScale(d.weight); }) .attr('fill', 'orange') .attr('stroke', function (d) { return 'rgba(' + (colorXScale(d.x) + colorYScale(d.y)) / 2+ "," + 0 + "," + 0 + ', '+ d.weight+')'; }) .attr('stroke-width' , function ( d ) { console.log("The stroke-width - " + d.weight); return radiusScale(d.weight) /2 ; }); svg.selectAll('text') .data(dataset) .enter() .append('text') .text(function( d ) { return "(" + d.x + "," + d.y + ")"; }) .attr('x', function ( d ) { console.log("The x - " + xScale(d.x)); return xScale(d.x); }) .attr('y', function ( d ) { console.log("The y - " + d.y); return yScale(d.y + (d.weight * 50)); }) .attr('text-anchor', 'middle') .attr('font-size', 11); renderAxes(); } </code>
此方法进一步执行三个子任务——
1. 设置比例尺
2. 绘制点
3. 绘制坐标轴
<code>Setting the scales Plotting the dots Drawing the axes </code>
第二个任务可以分配给另一个函数,但我当时很懒。在解释第二个和第三个子任务之前,我们需要查看如何根据服务器返回的数据设置比例尺。setScales 函数执行此操作。这是该函数的代码——
<code>function setScales ( ) { xScale = d3.scaleLinear() .domain( [ 0, 50 + d3.max( dataset, function( d ) { return d.x }) ] ) .range( [ paddingH, svgWidth - paddingH ] ); yScale = d3.scaleLinear() .domain( [ 0, 50 + d3.max( dataset, function( d ) { return d.y }) ] ) .range( [ svgHeight - (paddingV * 2), paddingV ] ); radiusScale = d3.scaleLinear() .domain( [0, d3.max( dataset, function ( d ) { return d.weight })] ) .range( [ 5, 15] ); colorXScale = d3.scaleLinear() .domain( [ 0, d3.max( dataset, function( d ) { return d.x }) ] ) .range( [ 100, 255 ] ); colorYScale = d3.scaleLinear() .domain( [ 0, d3.max( dataset, function( d ) { return d.y }) ] ) .range( [ 100, 255 ] ); } </code>
注意:scaleLinear() 方法仅适用于 D3 的第四个版本,而不适用于第三个版本。对于第三个版本,请使用 d3.scale.linear()。
在我解释比例尺实际是什么之前,我们应该熟悉域(domain)和值域(range)的概念。域和值域是集合论的概念。如果你学过基础数学,你可能还记得(或者你已经是数学天才)域和值域都是值的集合。如果你不太懂数学,就把它们想象成值的数组,这些值可以是原始数据类型,也可以是对象数据类型。在 d3 中,域跨越从服务器接收到的数据,我们从中计算最大值和最小值来创建域。值域是我们即将传入数据映射到的另一组值。例如,如果我们的数据的值在 10 到 1000 之间,而我们想将其映射到 0 到 500,那么 10 将映射到 0,1000 将映射到 500;同样,数据(域)的中值 500 将映射到略高于 250(值域)的值。
但我们为什么要将数据缩放到某个值域呢?例如,在我们的例子中,如果 x 的值为 1200,y 的值为 750,那么我们就需要一个至少有那么宽和高的画布(1200 × 750),以便点至少可以在平面的最角落处绘制。这么大的画布太僵硬了,因为它会超出某些设备(如手机和平板电脑)的边界。所以,我们需要一个解决方案,以便可以将大数字表示在小的 SVG 画布内。解决方案是将我们大的域值缩小到较小的值域值。对于小值也可以这样做(考虑一个 x 和 y 坐标需要从 1 到 5 之间的值绘制的数据!)。
为了计算比例尺的域和值域,d3 提供了 .domain() 和 .range() 方法。这两种方法都接受一个长度为 2 的数组(对于线性比例尺)。数组的第一个值表示最小值,第二个值表示最大值。因此,对于域和值域,我们都给出最大值和最小值。对于域,我们使用 .max() 方法计算最大值,该方法不言自明。域的最小值取 0(我假设我的数据中没有负值)。数据的范围是从 SVG 画布的内边距到画布的最大(宽度和高度)值减去内边距。对于 y 值域,我反转了这些值,以便我的散点图将根据我们从小到大绘制的图表绘制(实际上,计算机屏幕从左上角开始绘制,这会使 y 图向下颠倒,所以为了使它们对我们来说自然,值在值域中被反转了)。你可能想知道为什么我在 domain() 方法中将数据值的最大值增加了 50。我这样做的目的是让我的最高值点不会绘制在平面的边缘。只需删除那个 50+,自己试试看。
一旦定义了 x 和 y 比例尺,我们就定义半径比例尺,该比例尺基于点的权重大小来决定。权重越大,半径越大。但我将我的权重从 5 像素缩放到 15 像素半径,因为如果某个点的权重为 0 或接近 0,它在平面上将不可见。接下来,我定义了颜色值的比例尺。你很聪明,我知道你能弄清楚那些颜色比例尺。
哇!如果你消化了所有这些,恭喜你!现在我们继续图表绘制的第二步——在平面上绘制点(使用比例尺)。
步骤 – 2
好的!现在我们知道了比例尺是什么,并且可以定义我们的比例尺并使用这些比例尺在 x-y 平面上绘制我们的点(在散点图中是圆),我们根据 dataset 中定义的 x-y 值在坐标上绘制我们的圆。使用以下代码,我们在 SVG 画布中添加一些空的圆——
<code>var circles = svg.selectAll('circle') .data(dataset) .enter() .append('circle'); </code>
selectAll() 方法选择 SVG 画布中定义的所有属性。等等!什么?我们没有在画布中定义任何圆,那么我们怎么能选择任何圆标签呢?是的,你说得对,画布中没有定义圆标签,这就是为什么此方法在我们的情况下返回空数组。通过 data(dataset),我们定义了要处理的数据源。enter() 方法用于告诉 d3 我们将第一次向 SVG 画布添加圆元素(不更新或删除,我们将在后面的博文中讨论它们)。append 方法将圆添加到 SVG 画布,尽管我们还没有任何圆。我们知道这很令人困惑,如果没有圆,我们怎么把圆添加到画布呢?实际上,d3 会考虑到我们在读取 dataset 中的数据时会将圆添加到画布,并且还会设置它们的属性。
如果我们将在控制台打印这些圆,我们会看到——
<code>Object { _groups: Array[1], _parents: Array[1] } </code>
所以它实际上是一个包含两个字段的对象,这两个字段是长度等于 1 的数组。
现在,我们定义圆的属性,以便它们开始出现在 SVG 画布上。在这里,我们使用 x 和 y 比例尺定义 x-y 坐标,根据半径比例尺定义圆的半径,填充橙色,创建每个圆的边界并设置每个边界的宽度。圆的属性使用 .attr() 方法定义。
设置圆的 x 和 y 属性——
<code>.attr('cx', function ( d ) { console.log("The x - " + xScale(d.x)); return xScale(d.x); }) .attr('cy', function ( d ) { console.log("The y - " + d.y); return yScale(d.y); }) </code>
这里,如果我们简单地写 .attr('cx', 30 ),那么所有的圆都将绘制在 x = 30 的垂直线上,也就是说,所有的圆的 x 值都将是 30。但是,而不是给一个固定值,我们可以根据 dataset 中可用数据定义 x 和 y 坐标。要做到这一点,.attr() 方法将一个匿名方法作为第二个参数。这个匿名方法一次从 dataset 中获取值,第一个参数是数据元素,第二个参数是该元素在 dataset 中的索引。由于我们不需要数据元素的索引,所以我们简单地不在匿名方法的参数列表中提及它。这里的 function ( d ) {.....} 的 d 参数代表一个对象,其中包含“x”、“y”和“weight”键及其相应的值。所以我们简单地获取 x 值并将其传递给 xScale(),类似地,y 值被传递给 yScale()。[注意:我们定义的比例尺是函数,它们根据 domain() 和 range() 中设置的限制返回值]。最终,我们设置了要在 SVG 画布上绘制的圆的“cx”和“cy”属性。如果你对变量和/或函数的中间状态有任何困惑,可以使用 console.log(....) 来打印有意义的值。
类似地,我们使用 .attr() 方法定义各种属性。请注意,这是一个 SVG 元素,我们设置的所有属性都是在这个 SVG 元素上定义的属性。你可以探索这个 SVG 元素的更多属性,并使用它们来增强画布上绘制的圆的表示。圆 SVG 元素在此处解释。设置圆的其他属性留给读者作为进一步的研究领域。
我们在画布上绘制的圆现在相当可见,但我们想让绘制在画布上的圆具有意义。所以每个圆绘制的坐标值被写(实际上是绘制)在它周围。圆 SVG 元素本身没有在上面绘制文本的方法[虽然我们有“title”属性,这与我们想实现的不同]。要在 SVG 画布中写入文本,有一个 svg 元素。所以我们用以下行在散点图中编写文本——
<code>svg.selectAll('text') .data(dataset) .enter() .append('text') .text(function( d ) { return "(" + d.x + "," + d.y + ")"; }) .attr('x', function ( d ) { console.log("The x - " + xScale(d.x)); return xScale(d.x); }) .attr('y', function ( d ) { console.log("The y - " + d.y); return yScale(d.y + (d.weight * 50)); }) .attr('text-anchor', 'middle') .attr('font-size', 11); </code>
它有“x”和“y”属性,用于在 x-y 平面的特定坐标上绘制文本。“text-anchor”将文本移动到该位置,以便文本的中间位于使用 attr() 方法设置的文本的 x 和 y 坐标上。如果你没理解最后一行,只需删除“text-anchor”属性,看看区别。元素上还可以设置更多属性。
所以第二步,在 x-y 平面上绘制点,完成了!耶!现在继续在我们的 SVG 画布上绘制 x 和 y 坐标轴。
步骤 – 3
使用比例尺绘制散点图的最后一步是完成第三个子任务。我们使用 renderAxes() 函数在 SVG 画布上绘制坐标轴。此函数为 x 和 y 坐标轴中的每一个追加一个组。组是 g SVG 元素,它包含绘制 SVG 画布上的线或曲线的内部元素。我们将它们组合在一起,以便将它们分开,并使用一个属性集体处理它们。我们的坐标轴是组,进一步包含 SVG 元素的组。完成坐标轴绘制后,只需在浏览器的开发者工具中检查它们。我使用的是 Firefox,并打开了 Inspector 来查看实际绘制坐标轴的 DOM 元素。看到元素的结构非常有趣。我们的渲染坐标轴的函数是——
<code>function renderAxes() { svg.append('g') .attr('transform', 'translate(' + 0 + ', '+ (svgHeight - paddingV * 2) + ')') .call(d3.axisBottom(xScale) .ticks(20)); svg.append("g") .attr("transform", "translate( "+ paddingV+', '+ 0 + ')') .call(d3.axisLeft(yScale) .ticks(20)) } </code>
首先,我们为每个坐标轴向 SVG 画布追加一个组。然后,我们转换该组,将其移动到特定位置,以便正确地定位它们(使用 SVG 的 translate() 方法)。一旦组被转换,我们就使用 .call(d3.axisBottom(xScale)) 和 .call(d3.axisLeft(yScale)) 方法绘制我们的坐标轴。这是在 SVG 画布上绘制坐标轴的最低要求。我们可以进一步设置坐标轴的属性,例如,我们在这里将刻度设置为 20。d3.axisBottom() 接受 xAxis(d3 比例尺)作为参数来绘制水平轴,d3.axisLeft(yScale) 接受 yScale 作为参数来绘制垂直轴。
SVG 图形中最关键的事情之一是定义图形的内边距。正如我们所见,内边距在比例尺和坐标轴中到处都在使用——绘制一切。微调内边距以精确绘制一切本身就有点麻烦。但是你玩 D3 越多,你学到的就越多。
最后,我们的图表就准备好了,看起来像这样——