Hello, 各位怯懦的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
 

自己有丰盛的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是自己的宗旨, 菜到抠脚是自己的特点, 低微中透着一丝丝坚强, 傻人有傻福是对我最大的刺激.

欢送来到小五随笔系列“D3.js”手绘分段折线图.

写在后面

双手奉上代码链接 传送门 - ajun568

双脚奉上最终效果图

观前揭示

本文以实现上图为最终目标,所有过程均服务于后果,而非对svgD3.js的零碎学习。

导读

敌人,你是否与我有雷同的性能诉求,应用支流的图表库不易满足咱们的需要;敌人,你是否又和我一样急于求成,想在短时间内实现相应的性能开发。如果屏幕前你也有雷同的想法, 那D3是一个不错的抉择,它易于上手且可针对需要定制化绘制。上面就让咱们一起进入这个充斥奇幻色调的图形世界吧❗️

筹备工作

yarn add d3

截止2021-03,以后最新版本d3^6.6.0,咱们以此版本来开展旅程。万变不离其宗,如若版本更替,倡议采纳最新版本。

解剖

拟定数据格式如下

dataset = [  {    xValue: x轴数据 | Number,    yValue: y轴数据 | Number,    filled: 是否为实心点 | Boolean,  },  ...[and so on]]

look picture

可将其拆解为以下几个局部:

  • 坐标轴 $(x, y)$
  • 坐标点 & 点到坐标轴的虚线
  • 门路
  • tooltip & hover时点到坐标轴的虚线

浅谈SVG

一般场景下 $d3.js$ 就是 $svg$ 的语法糖

既然通篇都要与 $svg$ 打交道,怎么能不认识一下这个可恶的小家伙呢,所谓知己知彼,百战不殆

viewport => width / height: 指定画布的宽度和高度

<svg width="800" height="400"></svg>

viewBox => (x, y, width, height): 从$(x, y)$点, 向正方向选取宽为$width$, 高为$height$的矩形, 并放大至画布大小

正方向如图:

来几段代码感受一下

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <rect width="20" height="15" fill="red"/></svg>

♂️ Image

♂️ Code

<svg width="400" height="300" viewBox="10, -150, 400, 300">  <rect width="20" height="15" fill="red"/></svg>

♂️ Image

此图为在(10, -150)地位, 向正方向选取400✖️300的画布并展现

♂️ Code

<svg width="400" height="300" viewBox="-10, -10, 40, 30">  <rect width="20" height="15" fill="red"/></svg>

♂️ Image

此图相当于将选取的元素放大了20倍

常见标签

rect 绘制矩形 @params => x, y, width, height,$x, y$ 为矩形偏移量。上文都是以矩形举例的,就不在赘述了。

circle 绘制圆形

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <circle cx="100" cy="60" r="50" fill="red"/></svg>

$tips:$ fill 填充色

♂️ Image

ellipse 绘制椭圆

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <ellipse cx="100" cy="60" rx="80" ry="50" fill="red"/></svg>

♂️ Image

text 绘制文本

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <text x="100" y="60" stroke="red">我是绘制的文字</text></svg>

$tips:$

stroke 描边色

style -> text-anchor 对齐形式, 默认middle, 可选start、middle、end

♂️ Image

line 绘制直线

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <line x1="100" y1="60" x2="300" y2="10" stroke="red" stroke-width="2"/></svg>

$tips:$ stroke-width 描边宽度

♂️ Image

polyline 绘制折线

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <polyline points="30,140 100,60 300,10 350,50" fill="none" stroke="red" stroke-width="2"/></svg>

$tips:$ 记得fillnone, 否则门路局部会被填充哟

♂️ Image

path 门路

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <path d="M 20,20 L 100,60 L 20,100 L 60,60 Z" fill="red"/></svg>

♂️ Image

g 分组 将标签进行分组, 便于归类或复用

use 复制

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <path d="M 20,20 L 100,60 L 20,100 L 60,60 Z" fill="red"/>  <use href="#arrow" x="200" y="0" /></svg>

♂️ Image

defs 自定义图形

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <defs>    <path id="arrow" d="M 20,20 L 100,60 L 20,100 L 60,60 Z"/>  </defs>  <use href="#arrow" x="200" y="0" fill="red" /></svg>

♂️ Image

层级关系

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <rect x="0" y="0" width="30" height="30" fill="purple"/>  <rect x="20" y="5" width="30" height="30" fill="blue"/>  <rect x="40" y="10" width="30" height="30" fill="green"/>  <rect x="60" y="15" width="30" height="30" fill="pink"/>  <rect x="80" y="20" width="30" height="30" fill="red"/></svg>

♂️ Image

✍️绘制画布

前置常识筹备的差不多了,伙计们,进入正题了❗️

$First$ $of$ $all$, 咱们先把$html$构造建进去

<div id="line"></div>

$Secondly$, 咱们用d3在这个div中创立出咱们的画布, 宽高为400✖️800

  • $d3$为链式构造
  • select 抉择元素 (选$id$)
  • selectAll 抉择全副元素 (选元素, 选类)
  • append 追加元素
  • attr 增加属性
const width = 800const height = 400d3.select('#line')  .append('svg')  .attr('width', width)  .attr('height', height)

✍️绘制坐标轴

比例尺

在开始比例尺之前,咱们先来看几个比拟好用的函数:

  • d3.max(arr) return $max$
  • d3.min(arr) return $min$
  • d3.extent(arr) return $[min, max]$

本文所用到的为线性比例尺scaleLinear

❓ $what$ $is$ 线性比例尺

将一个间断的区间,映射到另一区间 (domin 映射到 range)

翠花, 上代码

 xScaled3.scaleLinear() // 创立线性比例尺  .domain([ // domin数据 [x.min, x.max]    d3.min(xData),    d3.max(xData)  ])  .rangeRound([0, width]) // range数据 [0, width]

xAxis绘制

接下来,咱们用axisBottom创立一个向下的坐标轴,并通过scale调用设置好的比例尺

 xAxisd3.axisBottom().scale(xScale)

而后, 通过call办法填充至画布上

svg.call(xAxis)

至此, 一个毛糙的x轴就画好了, 接着奏乐接着舞

有没有发现什么不对劲:

  • 右侧的$100$惨遭截肢
  • 坐标轴占满了整个画布, 丝毫没有美感
  • 且$x$轴不应该在最上面吗, 左侧也要给它的好基友$y$轴留地位

$1, 2$的外围问题就是没有留白, 既然要留白, 加padding就完事了; 而$3$一个translate就能够搞定

const padding = { top: 30, right: 30, bottom: 30, left: 30 } xScale- .rangeRound([0, width])+ .rangeRound([0, width - padding.left - padding.right]) svgsvg  .append('g')  .attr('transform', `translate(${padding.left}, ${height - padding.bottom})`)  .call(xAxis)

♂️ Image

No.1

  • axis.tickValues([...arr]) 用于指定坐标轴显示的值
  • axis.tickFormat() 格式化坐标轴数据

$eg:$ 坐标轴数据按千分符模式格式化 axis.tickFormat(d3.format(",.0f")), 其中.0f为不格局小数局部

No.2

第二点则可了解为给左右两端各补一条数据, 而后对两点连线. 而要补数据, 就要拟定补多少, 咱们引入一个份数的概念, 将1份定义为总长度的 $ 1/dataset.length * 2 $, 最小为 $ 1/10 $.

// 份数计算+ const length = dataset.length * 2+ const partDistance = (d3.max(xData) - d3.min(xData)) / (length > 10 ? 10 : length).domain([-   d3.min(xData),-   d3.max(xData)]).domain([+  d3.min(xData, item => {+    return item - partDistance+  }),+  d3.max(xData, item => {+    return item + partDistance+  })])

No.3

上文 “浅谈SVG中” 咱们曾经对 defsgpathuse 别离做了解说.

翠花, 上代码

const arrowPath = 'M4,4 L20,12 L4,20 L8,12 Z'const axisColor = '#fff'const arrowOffsetDistance = 12svg  .append('defs')  .append('g')  .attr('id', 'arrowX')  .append('path')  .attr('d', arrowPath)  .attr('fill', axisColor)svg  .append('use')  .attr('href', '#arrowX')  .attr('x', width - padding.right - arrowOffsetDistance)  .attr('y', height - padding.bottom - arrowOffsetDistance)

M4,4 L20,12 L4,20 L8,12 Z

  • d3.path() 创立path门路
  • moveTo(x, y) M
  • lineTo(x, y) L
  • closePath() Z

故也可依据办法动静生成

let arrowPath = d3.path() // M4,4 L20,12 L4,20 L8,12 ZarrowPath.moveTo(4, 4)arrowPath.lineTo(20, 12)arrowPath.lineTo(4, 20)arrowPath.lineTo(8, 12)arrowPath.closePath()

No.+∞

为了使坐标轴更好看, 咱们来随便装点几笔, 最终出现成果如下:

$tip:$ 通过select / selectAll去选中元素更改对应属性 (或$style$或$svg$的图形属性)

补其它的好基友$y$轴斯密达

marker标记

用于对图形做元素追加, 咱们的坐标轴就十分合乎这个特色

  • markerWidth / markerHeight 宽 / 高
  • refX / refY $x / y$ 轴偏移量
  • markerUNits 是否容许marker随所连贯图形的缩放而追随缩放, 默认strokeWidth(缩放), 可选userSpaceOnUse(不缩放)
  • orient 旋转角度, 默认$auto$, 可指定具体旋转度数

❓ 如何对以绘制的图形做追加

对以绘制的图形增加以下属性: marker-end="url(#id)", 可选 [marker-start、marker-mid、marker-end]

特地留神: marker能够了解为作用于点, 所以marker-mid对两点间的连线是没有成果的, 要至多三个点能力产生成果

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <marker id="arrow" markerWidth="12" markerHeight="12" refX="6" refY="6" orient="30deg">    <path d="M2,2 L10,6 L2,10 L4,6 Z" fill="red" />  </marker>  <line x1="100" y1="60" x2="300" y2="60" stroke="red" stroke-width="2" marker-end="url(#arrow)" /></svg>

♂️ Image

两种写法的比照如下:

绘制坐标点

坐标点❓画个圈圈咒骂你 (画圆呀)

空心点❓人体描边大法 (stroke-width 你值得领有)

映射到坐标轴❓比例尺 炼金术 (按比例尺映射回去)

♂️ Code

♂️ Image

绘制折线

点、线、面, 有了点咱们开始连线. 用什么呢? 我倡议大家用path(“绘制坐标轴”处已阐明用法), 但我抉择用line, 没什么起因, 任性而已, 不服你打我呀!

知识点

d3.line().defined() 以后点是否与其相邻点进行连线

以点画线, 可点真的全了吗? 终点在哪里呀, 起点在哪里, 在那小朋友的眼睛里? 咱们按后面计算的份数补全下数据, 而后绘图.

// 折线数据处理let newDataset = []newDataset.push({  xValue: dataset[0].xValue - partDistance,  yValue: dataset[0].yValue,  filled: true})newDataset.push(...dataset)newDataset.push({  xValue: dataset[dataset.length - 1].xValue + partDistance,  yValue: dataset[dataset.length - 1].yValue,  filled: true})

线笼罩了空心点 ❓ 依据量子力学 - $svg$层级程序定律, 肯定是图层程序搞反了. 换下代码程序, 完满解决.

$x$坐标雷同的连线 ❓ 有没有留神到下面的defined, 不过, 我是要对其x坐标雷同的点不进行连线, 而不是放空这个点, 还它自在. 克隆一个, Perfect, 完满解决问题. 而至于克隆哪个点, 随情绪就好, 你说我两个都想要, 拖出去斩了.

// 折线数据处理- newDataset.push(...dataset)+ dataset.forEach(item => item.filled ? newDataset.push(item) : newDataset.push(...[item, item]))

♂️ Code

♂️ Image

绘制坐标点到坐标轴的虚线

stroke-dasharray : 用于绘制虚线, 每绘制$x$个像素点, 则空余$y$个像素点

♂️ Code

<svg width="400" height="300" viewBox="0, 0, 400, 300">  <line x1="50" y1="50" x2="350" y2="50" stroke-dasharray="3" fill="none" stroke="red" stroke-width="3"></line>  <line x1="50" y1="180" x2="350" y2="180" stroke-dasharray="6, 18, 4, 12" fill="none" stroke="red" stroke-width="3"></line></svg>

♂️ Image

找到点, 连上线, 咱们的虚线就画好了

翠花, 上酸菜

♂️ Image

Tooltip

在构思tooltip之前, 先抛出几个问题

tooltip显示的是什么

对应点的坐标

❓ hover的区域是什么

整个坐标轴

❓ 显示的地位在哪里

对应点的旁边

❓ 还有须要留神的吗

不要超出边界范畴

咱们来总结下

$hover$的是整个区域 一个笼罩整个坐标轴的$rect$, 而后对此进行事件处理

$tooltip$ 一个显示在对应点旁边的小$div$

$hover$时找到x=c上对应的点的坐标, 对$x$轴和$y$轴连贯虚线, 并在点旁显示$tooltip$

躁动起来

先来append一个rect, 而后对齐做事件处理. 这里用到咱们相熟且性感的老朋友们就能够了, 上面有请他们闪亮退场:

  svg    .append('rect')    .attr('width', areaWidth)    .attr('height', areaHeight)    .style('fill', 'none')    .style('pointer-events', 'all')    .style('cursor', 'pointer')    .attr('transform', `translate(${padding.left}, ${padding.top})`)    .on('mouseover', mouseOver)    .on('mouseout', mouseOut)    .on('mousemove', mouseMove)

接下来咱们把$tooltip$和其到$x$轴与到$y$轴的虚线绘制进去.

这时会有这样一个纳闷, 我还没有计算地位, 怎么绘制, 看官莫急, 请看下文

mouseMove这个小家伙能够帮咱们拿到所在点的offsetX, 那找点的问题就转变成了已知$x$求$y$, 进而转变成求关联$x,y$的函数表达式.

已知首段和尾段$y$恒定($y=c$), 两头各段为:

$$(x - x1) * (y2 - y1) = (x2 - x1) * (y - y1)$$

扫盲

拿起机关枪, 对准盲点先来一波扫射

scaleLinear -> invert: 反向映射 (依据给定的位于 range 中的值返回对应的位于 domain 的值)

const xInvert = xScale.invert(d.offsetX - padding.left)

d3.bisector(): 平分线

为了了解这个概念, 咱们先来讲讲它的亲戚bisectLeftbisectRight

d3.bisectLeft(arr, x)

其中$arr$为被插入的数组(需排序), $x$为要插入的值, 返回值为插入地位. 若$x$已在数组中, 则插入至雷同条目标最右面.

const arr1 = [2, 3, 5, 6, 7]d3.bisectLeft(arr1, 4) // return 2const arr1 = [2, 3, 3, 6, 7]d3.bisectLeft(arr1, 3) // return 1

咱们说回bisector, 这是个啥呢, 它和下面其实是一样的, 咱们理论开发中根本都是简单的数组构造, 咱们能够依据bisector来指定与$x$比对的值.

const bisect = d3.bisector(d => d.xValue).rightconst xInvert = xScale.invert(d.offsetX - padding.left)const i = bisect(dealDataset, xInvert)

这样就求出了以后所在点的区间, 进而晓得了要用哪段函数求$y$

getBoundingClientRect & getBBox

selection.node().getBoundingClientRect() 获取HTML元素的相干属性

展现数据如下:

selection.node().getBBox() 获取$svg$元素的相干属性

书接上文, 已知函数和点$x$, 即可求点$y$, 进而画出该点到$x$轴和$y$轴的虚线. 而后咱们对 $tooltip$ 填充展现内容, 为避免边界点超出画布, 依据 $getBoundingClientRect$ 做边界值的地位调整.

♂️ Code

♂️ Image

功败垂成, 完结撒花

残缺代码连贯 传送门 Github - ajun568

参考链接

D3官网

[[MDN] SVG element reference](https://developer.mozilla.org...

[[Scott MurrayAn] SVG primer](https://alignedleft.com/tutor...

[[阮一峰] SVG 图像入门教程](https://www.ruanyifeng.com/bl...

[[张鑫旭] 了解SVG viewport,viewBox,preserveAspectRatio缩放](https://www.zhangxinxu.com/wo...

[[彦子] Marker个性——在元素中援用marker
](https://www.w3cplus.com/svg/s...

[[Terry] D3:什么是平分线?](https://www.javaer101.com/art...