Hello, 各位怯懦的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
自己有丰盛的脱发技巧, 能让你一跃成为资深大咖.
一看就会一写就废是自己的宗旨, 菜到抠脚是自己的特点, 低微中透着一丝丝坚强, 傻人有傻福是对我最大的刺激.
欢送来到
小五
的随笔系列
之“D3.js”手绘分段折线图
.
写在后面
双手奉上代码链接 传送门 – ajun568
双脚奉上最终效果图
观前揭示
👺 本文以实现上图为最终目标,所有过程均服务于后果,而非对 svg
、D3.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:$ 记得 fill
给none
, 否则门路局部会被填充哟
🧟♂️ 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 = 800
const height = 400
d3.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
)
翠花, 上代码
👉 xScale
d3.scaleLinear() // 创立线性比例尺
.domain([// domin 数据 [x.min, x.max]
d3.min(xData),
d3.max(xData)
])
.rangeRound([0, width]) // range 数据 [0, width]
xAxis 绘制
接下来,咱们用 axisBottom
创立一个向下的坐标轴,并通过 scale
调用设置好的比例尺
👉 xAxis
d3.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])
👉 svg
svg
.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 中”咱们曾经对 defs
、g
、path
、use
别离做了解说.
翠花, 上代码
const arrowPath = 'M4,4 L20,12 L4,20 L8,12 Z'
const axisColor = '#fff'
const arrowOffsetDistance = 12
svg
.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)
MlineTo(x, y)
LclosePath()
Z
故也可依据办法动静生成
let arrowPath = d3.path() // M4,4 L20,12 L4,20 L8,12 Z
arrowPath.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(): 平分线
为了了解这个概念, 咱们先来讲讲它的亲戚bisectLeft
、bisectRight
d3.bisectLeft(arr, x)
其中 $arr$ 为被插入的数组(需排序), $x$ 为要插入的值, 返回值为插入地位. 若 $x$ 已在数组中, 则插入至雷同条目标最右面.
const arr1 = [2, 3, 5, 6, 7]
d3.bisectLeft(arr1, 4) // return 2
const arr1 = [2, 3, 3, 6, 7]
d3.bisectLeft(arr1, 3) // return 1
咱们说回 bisector
, 这是个啥呢, 它和下面其实是一样的, 咱们理论开发中根本都是简单的数组构造, 咱们能够依据bisector
来指定与 $x$ 比对的值.
const bisect = d3.bisector(d => d.xValue).right
const 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…