d3.js数据可视化从入门到放不下(三)—— 构建平面直角坐标系

1,386 阅读4分钟

首先,读到这儿,已经基本知道了d3的基本语法和套路。

在此之前的示例都是使用纯粹的HTML,由本文开始,正式引入SVG的概念,以后的所有示例均使用SVG模式。

SVG(Scalable Vector Graphics),即可缩放矢量图形。是W3C颁布的一种成熟标准,用用于规范网络和移动平台上的交互式图形。关于SVG的东西就不展开说了,毕竟光这个东西都可以写本书了,这里主要说一下它与D3数据可视化相关的一些特性:

  1. 可伸缩性好:因为它是矢量的,SVG图像能够在被任意放大的情况下质量不被降低,可伸缩性高;
  2. 高质量:SVG图像在任何分辨率的设备下都能保证高质量输出;
  3. 可读性高:SVG文件是基于XML的,可读性高;
  4. 易用:SVG可以很好的与JavsScript/CSS结合操作;
  5. 尺寸小:SVGJPEG/PNG图像相比,尺寸更小,文件的可压缩性更高;
  6. 应用广泛:所有的现代浏览器以及移动平台都支持SVG标准;
  7. 开放标准:SVGW3C组织创建,而且他不是一个专属商业标准。

举个栗子,以第一点为例。下图是SVG和位图像素化对比,由于SVG图形是一组由相对关系式描述的几何图形,这使得它在任意尺寸下都不会丢失精度。

SVG和位图像素化对比 话说回来,干正事儿~

最开始,在d3.js中是没有坐标的概念的。但后来因为这个东西实在是太常用了,d3.js发布之后也在不断的完善中,所以很快就加入了坐标轴的概念,也就是本文要讲的Axis组件。

创建基础组件:

首先,声明基础变量:

let height = 500,
  width = 500,
  margin = 25,
  axisWidth = width - 2 * margin;

然后,创建svg实例,并设置一些基础属性:

let svg = d3
  .select("body")
  .append("svg")
  .attr("class", "axis")
  .attr("width", width)
  .attr("height", height);

创建Axis组件

创建一个线性尺度,并设置其定义域和值域,线性尺度这个概念简单的说就是定义了数据元素和图形元素之间的映射关系。啊~很难用文字表达,看代码体会吧。。。

let scale = d3.scale.linear().domain([0, 100]).range([0, axisWidth]);

下面的代码通过使用d3.svg.axis()方法创建了坐标实例

let axis = d3.svg.axis().scale(scale).orient('bottom');

d3.js中,Axis组件实际上就是一个能够包含数值尺度、时间尺度、有序尺度的容器。上面代码中的scale方法就给Axis组件提供了尺度。另一个orient方法用来表示坐标轴的方向,这个地方下节展开讲。

接着再改造:

let axis = d3.svg.axis().scale(scale).orient(orient);
 svg
   .append("g") // < - A
   .attr("transform", () => "translate(" + margin + "," + offset + ")") // < - B
   .call(axis); // < - C

上面的代码,已经定义好了Axis组件,在第AB行创建svg:g,渲染坐标轴所需要的全部svg结构,并设置其transform属性来调整坐标轴的位置。

这段代码中最终的其实是第C行,我们将axis作为参数传入d3.selection.call函数。d3.selection.call会在当前选集上调用该参数代表的方法。

此时的页面效果: 在这里插入图片描述 可以看到页面上已经通过svg生成了一个基本的刻度尺,默认情况下,d3.js会根据线性尺度中的定义域和值域来自分配刻度。Axis组件也提供了一些API来做个性化配置:

let axis = d3.svg
  .axis()
  .scale(scale)
  .ticks(5) // < - A
  .tickSubdivide(5) // < - B
  .tickPadding(10) // < - C
  .tickFormat((v) => v + "%"); // < - D

此时的页面效果: 在这里插入图片描述 代码解读:

  • A行:ticks方法用于规定刻度尺上的刻度个数,此处设置为5个;
  • B行:tickSubdivide方法用于规定每个刻度之间的小刻度的个数,此处设置为5个(注意:此方法意为为大刻度添加n个小刻度,所有此处呈现出来的小格子实际应该为6个);
  • C行:tickPadding方法用于规定刻度数值和坐标轴之间的距离(以像素为单位),此处设置为10个;
  • D行:tickFormat方法用来对刻度数值做格式化处理,其参数为当前刻度的数值,此处为刻度数值添加了百分号;

绘制X

其实上面的代码已经绘制出了X轴的基本样子,但是由于要做一个完整示例,所有我们对现有代码做一下处理,避免后面代码逻辑混乱。我们把刚才的代码封装成一个renderXAxis函数。

绘制Y

绘制Y轴需要改变坐标轴的方向,Axis提供了orient方法。我们需要在创建Axis实例的时候去调用它,它的参数四个可选值:topbottomleftright,分别对应:

  • 上:水平的坐标轴,标题位于坐标轴之上;
  • 下:水平的坐标轴,标题位于坐标轴之下;
  • 左:垂直的坐标轴,标题位于坐标轴的左边;
  • 右:垂直的坐标轴,标题位于坐标轴的右边。

至此,根据orient方法改造renderXAxis方法,并封装绘制Y轴的renderYAxis方法:

function renderYAxis() {
  let axisLength = height - 2 * margin;
  let scale = d3.scale.linear().domain([100, 0]).range([0, axisLength]);
  let yAxis = d3.svg.axis().scale(scale).orient("left");
  svg
    .append("g")
    .attr("class", "y-axis")
    .attr("transform", () => "translate(" + margin + "," + margin + ")")
    .call(yAxis);
}

renderXAxis方法相比,renderYAxis方法主要是通过调用orient方法修改了坐标轴的方向,然后根据实际情况调整了其位置。

到这儿,一个基础的平面直角坐标轴就出来了: 在这里插入图片描述 检查一下元素结构:

在这里插入图片描述 所有页面元素全部使用svg生成,嗯~ 有内味儿了~~

优化:绘制表格线

继续改造,检查一下元素,发现横纵坐标分别为一个svg:g元素。坐标刻度又分别是这个svg:g元素的子集,并且也是个g元素。有办法了,改造一下横坐标对应的renderXAxis函数:

function renderXAxis() {
	...
	// 其它代码略
    d3.selectAll("g.x-axis g.tick") // < - A
       .append("line") // < - B
       .attr("class", "grid-line") // < - C
       .attr("x1", 0) // < - D
       .attr("y1", 0) // < - E
       .attr("x2", 0) // < - F
       .attr("y2", -(height - 2 * margin)); // < - G
}

代码解读: 第AB行的就很简单了,找到g.x-axis下面的g.tick,然后给它追加一个line元素。并给这个line元素加个样式(第C行)。 linesvg中最简单的图形,它有4个主要属性:

  • x1y1属性表示这个线条的起点
  • x2y2属性表示这个线条的终点

这里要注意的第G行,大家不要被现在页面的效果的迷惑了。此时我们操作的仍然是横坐标的这个g,所以我们位置仍要相对于横坐标,而并非目前呈现的整个坐标系,所以要把y2的值设置成负值。

renderYAxis函数基本同理,只需要调整终点的位置即可。

最终效果: 在这里插入图片描述