写在前面
双手奉上代码链接 传送门 - 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)
M -
lineTo(x, y)
L -
closePath()
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)常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!