【二】:使用 Express-Validator进行后端校验react
最近接到了一个小需求,须要画一个数据漏斗图,本来也没什么,由于原本项目里就有 chart 相关需求,无非就是用现成的漏斗图改巴改巴就完事了。github
产品的PRD大概长这个样子:canvas
我想像的而且 chart 官网给的示例是这样的:后端
UI设计出来的是这个样子的:api
文本内容涉及到内部项目,我就马赛克了,虽然放出来也没啥问题😄数组
通过一番激烈的讨论,观点以下:浏览器
我:人家有现成的漏斗图,我直接拿来用十分钟就搞定了,节约项目时间
UI:我这设计属于原创,呕心沥血设计出来的,最好按照这个实现
产品:我以为UI设计的挺好看,我们不能和其余家同样
我:能够😊(心中一万匹不知名动物奔腾,一个B端项目要什么原创)
复制代码
好了,结论出来了,按照UI来吧,那么就开始想方案:bash
第一种 - 纯Dom实现:看起来结构并非很复杂,若是纯用dom来实现我以为是可能的,层叠方案设计好一层一层放置元素应该是能够实现的。
第二种 - Canvas绘制:,虽然说看起来很简单,可是里面涉及到了不少多边形和箭头线段,若是使用 Dom + CSS 可能能实现,可是必定是大费周章,因此可能 Canvas 相比来讲更容易一些。
第三种 - SVG绘制:SVG和Canvas同样,都是进行绘制,只不过方式不一样而已。而这里为何选择SVG。一方面,由于SVG不依赖于像素,放大缩小不会失真,更适合处理图表类;另外一方面,Canvas 多多少少使用过,而 SVG 还没真正使用过(基于SVG的ICON就不算了)。既然 UI 选择了原创,那么我也以为这个小需求也值得我原创成长一下。所以选择了SVG。
SVG | Canvas | |
---|---|---|
优势 | 矢量图,不依赖于像素,放大缩小不会失真。以 Dom 的形式表示,事件绑定由浏览器直接分发到节点上。 | 定制型更强,能够绘制绘制任何本身想要的东西。非 Dom 结构形式,用 JavaScript 进行绘制,涉及到动画性能较高。 |
缺点 | Dom 形式,涉及到大量更新 Dom 以及动画的时候,性能较低。 | 事件分发由canvas处理,绘制的内容的事件须要本身作处理。依赖于像素,没法高效保真,画布较大时候性能较低。 |
本文记录了一天时间内,SVG学习实践系列,仅供你们参考,最后我实现出来的效果是这样的:
看起来基本一致哈~😄
这里我就介绍几个特别经常使用的吧,简单介绍一下使用方法,由于官方文档写的十分详细,各个API的用法。惟一的缺点就是是英文的,因此在这里我就简单的把几个经常使用的介绍一下,其余的你们业务场景使用到了再去翻API就好了。
yarn add svg.js
复制代码
import React, { Component } from 'react';
import SVG from 'svg.js';
class API extends Component {
componentDidMount() {
// 初始化,获取svg document
const draw = SVG('api_container').size('100%', '400px');
....
}
render() {
return (
<div id='api_container' />
)
}
}
export default API;
复制代码
获取到 SVG Document 以后,咱们就可使用它进行绘制,获取它须要的是元素 id,因此通常咱们在componentDidMount
这个生命周期进行初始化获取。
首先,咱们来使用最简单的 API 来画一条直线,两点肯定一条直线,依次输入两点的(x,y)坐标便可。
const line = draw.line(0, 100, 100, 0);
line.stroke({ color: '#f06', width: 10 });
复制代码
效果如上图所示,而且,全部的 API 均为链式调用(若是你熟悉jQuery),那么必定不会陌生,因此上面代码也能够直接写成。
draw.line(0, 100, 100, 0)
.stroke({ color: '#f06', width: 10 });
复制代码
上面直线,很容易就画出来了,也就是从(0, 100) -> (100, 0),不过呢,我画的时候不必定非要在起始位置画吧,有可能我就想在画布中间,那怎么办?没错,有设置起始位置的API —— move
。
// 从(100, 100)开始画
draw.line(0, 100, 100, 0)
.move(100, 100)
.stroke({ color: '#f06', width: 10 });
复制代码
如图,能够看到,起始位置也能够设置,而且代码的位置由于是链式,因此放在哪里都是能够的。
画矩形就更简单了,只须要传入你想要画的长和宽就好了,固然你也能够从任何地方开始画。
// 好比,我也想从(100, 100)画
draw.rect(140, 140).move(100, 100);
复制代码
画出来是黑色,也就是默认填充是黑色,我但愿变成蓝色,那么可使用fill('blue')
。
通常来讲,与颜色相关的 API 有两种,
fill
和stroke
,fill
通常是填充色,stroke
通常是描边色。
不知道注意到没有,上面咱们的矩形把直线覆盖住了,嗯没错,若是位置重叠,它的顺序是后画的会覆盖在先画的上面,这与 CSS 层叠样式规则很像。咱们将两者顺序调换一下再看一下。
componentDidMount() {
const draw = SVG('api_container').size('100%', '400px');
// 画矩形
draw.rect(140, 140).move(100, 100).fill('blue');
// 画直线
const line = draw.line(0, 100, 100, 0).move(100, 100);
line.stroke({ color: '#f06', width: 10 });
}
复制代码
这里想说明的就是,当你须要在绘制的图形内部画文字的时候,绘制的前后顺序必定不能乱,必定是先画图形,再画文本。
画圆就更简单了,只须要给出半径就行。
// 从(200, 200)开始画一个半径是100的红边橘色圆
draw.circle(100)
.move(200, 200)
.fill('orange')
.stroke({ color: 'red', width: 4 });
复制代码
绘制多边形也很简单,只不过画的过程须要开发者设计好,参数接受两种类型第一种是用逗号隔开的坐标,坐标x y用空格间隔,第二种是点二维数组。
// 字符串参数绘制三角形
draw.polygon('300 300, 360 240, 360 360');
// 点坐标绘制矩形
draw.polygon([[400, 400], [440, 400], [500, 300], [400, 300]])
.fill('green');
复制代码
绘制多边曲线与上面多边形类似,只不过就是用线段展现,能够理解为中空。
// 绘制多边曲线
draw.polyline('0,0 100,50 50,100').fill('none').stroke({ width: 1 })
复制代码
draw.image('https://cdn.img42.com/4b6f5e63ac50c95fe147052d8a4db676.jpeg')
.size(60, 60) // 设置绘制的长宽
.move(500, 100);
复制代码
绘制文本就是,直接绘制的文字放进来就行
draw.text('我是被绘制的文本')
.move(600, 200)
.stroke({ color: 'yellow' })
.font({ size: 20 });
复制代码
到如今为止,基本简单的都介绍完了,绘制一些基本需求没啥问题了,剩下的就是,咱们绘制上去的是 dom,既然是 dom 那么确定就有事件。svg.js
支持绑定各类 dom 事件,写法也是多种多样,这里咱们就介绍最通用的on
。
// 为图片绑定click事件
const draw_image = draw.image('https://cdn.img42.com/4b6f5e63ac50c95fe147052d8a4db676.jpeg')
.size(60, 60) // 设置绘制的长宽
.move(500, 100);
draw_image.on('click', function(){
alert(this.node.getAttribute('href'));
});
复制代码
其余更多 API 及其使用方法,建议你们去啃官方文档。
上面基本把我这里该用到的API都介绍完了,下面开始言归正传,正式进行产品需求自定义图表的开发工做。下面将从思路到实现逐步讲解。
大概就是上面的设计,分为左中右,上中下三等分。左侧部分,绘制文字;中间部分,绘制图表以及对应的文字;右侧部分,绘制背景板以及箭头文字部分。
function drawStepText(draw) {
const stepTextGroup = draw.group().move(0, 0);
const text_step1_label = draw.text('第一步').move(0, 43);
stepTextGroup.add(text_step1_label);
const text_step1_content = draw.text('AAAA').move(60, 43).font({ fill: '#5F6369' });
stepTextGroup.add(text_step1_content);
const text_step2_label = draw.text('第二步').move(0, 143);
stepTextGroup.add(text_step2_label);
const text_step2_content = draw.text('BBBB').move(60, 143).font({ fill: '#5F6369' });
stepTextGroup.add(text_step2_content);
const text_step3_label = draw.text('第三步').move(0, 243);
stepTextGroup.add(text_step3_label);
const text_step3_content = draw.text('CCCC').move(60, 243).font({ fill: '#5F6369' });
stepTextGroup.add(text_step3_content);
}
componentDidMount() {
const draw = SVG('statistics_draw').size('100%', '100%');
drawStepText(draw);
}
render() {
return (
<div id='statistics_draw' className='chart-container'></div>
)
}
复制代码
文字内容绘制完成了,这里使用到了draw.group()
,其实也没什么,就是对所画内容分一下组,既然是分为三部分,因此这边也就分为三组,方便划分。
绘制多变形漏斗区域,其实也挺简单的,只不过复杂之处在于要计算好位置,位置的计算比较费心。代码部分与下方 hover 提示一块儿讲解。
上面按照计算,漏斗图以及对应的文字已经绘制完成,接下来,最复杂的地方就在这里了。每个文字前面都有一个 hvoer 提示,鼠标悬浮上去会有个小弹窗。这里的思路很明显要加上on('mouseover', funciton())
事件。不过弹窗要怎么展现就是难点了。总不能每一个 hover 都事先画出来而后 hover 的时候再显示吧。虽然这样也行,可是一是我以为复杂,二是如何绘制隐藏而且还有复杂定位的弹窗。
因此,最后个人实现思路:
<div id='statistics_draw' className='chart-container'>
<div id='hover_container' className='tooltip-container' />
</div>
复制代码
const hoverDom = document.getElementById('hover_container');
const { left, top, width } = dom.getBoundingClientRect();
hoverDom.style.left = `${left + width / 2}px`;
hoverDom.style.top = `${top - 6}px`;
hoverDom.innerHTML = `${TYPE_TEXT[type]}`;
const arrowDom = document.createElement('div');
arrowDom.classList = `tooltip-arrow`;
hoverDom.appendChild(arrowDom);
hoverDom.style.display = 'block';
复制代码
这里用到了一个很重要的 DOM API —— getBoundingClientRect()
,用于获取 dom 的绝对位置坐标以及长宽各类参数。最后实现的效果:
image_referral_per.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'referral_per');
});
image_referral_per.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
复制代码
绘制矩形区域代码:
function drawPolygonArea(draw, data) {
/* 第一个多边形及文字 */
const polygonGroup = draw.group().move(140, 0);
const polygon_transform = draw.polygon('0 290, 120 290, 180 210,0 210').fill('#ACD3FA');
polygonGroup.add(polygon_transform);
const text_transform1 = draw.text(`付费线索数: ${data.paidNumberTotal}`)
.move(30, 260).fill(color_white).font({ size: 12 });
polygonGroup.add(text_transform1);
const image_transform1 = draw.image('/static/imgs/question-circle.png', 14, 14).move(10, 256);
image_transform1.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'transform1');
});
image_transform1.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
polygonGroup.add(image_transform1);
/* 第二个多边形及文字 */
...
/* 第三个多边形及文字 */
...
}
复制代码
从上面的 UI 图,咱们能够看出来,右侧除了箭头文字,还有一个渐变的背景色。所以,咱们在绘制文字以前,须要先绘制三块渐变背景色。
这里还要多介绍一个渐变色 API —— gradient
, 嗯,没错,就是处理相应的 CSS 渐变色操做的。
// 绘制右侧背景色
function drawShadowArea(draw) {
const { width } = document.getElementById('statistics_draw').getBoundingClientRect();
// 由于是自动填满右边界,须要手动计算右边界位置
const shadow_right = width - 260;
const shadowGroup = draw.group().move(260, 0);
const gradient = draw.gradient('linear', function(stop) {
stop.at(0, 'rgba(255,255,255,0)');
stop.at(1, 'rgba(247,247,247,1)');
});
const shadow_area1 = draw.polygon(`0 290, ${shadow_right} 290, ${shadow_right} 210, 60 210`).fill(gradient);
shadowGroup.add(shadow_area1);
const shadow_area2 = draw.polygon(`70 190, ${shadow_right} 190, ${shadow_right} 110, 130 110`).fill(gradient);
shadowGroup.add(shadow_area2);
const shadow_area3 = draw.polygon(`140 90, ${shadow_right} 90, ${shadow_right} 10, 200 10`).fill(gradient);
shadowGroup.add(shadow_area3);
}
复制代码
能够看到,背景色块出来了,这里使用的依然是多边形绘制,由于要与左侧多边互补才行。
最后只剩下右侧的箭头以及文字了,思路以下:
polyline
进行绘制polygon
绘制三角形剩下的就是复杂的定位计算了。
function drawArrowLine(draw, data) {
/* 第一条线 + 文字 */
const arrowTextGroup = draw.group().move(260, 0);
const polyline_paid_rate = draw.polyline([[40, 250], [340, 250], [340, 270], [30, 270]])
.fill('none').stroke({ color: '#e2e2e4', width: 1 });
const polygon_paid_rate = draw.polygon([[22, 270], [30, 265], [30, 275]]).fill('#e2e2e4');
const image_paid_transform = draw.image('/static/imgs/question-circle-black.png', 12, 12).move(350, 254);
image_paid_transform.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'paid_transform');
});
image_paid_transform.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
const text_paid_transform = draw.text(`付费转化率:${(data.paidConversionRateTotal * 100).toFixed(2)}%`)
.move(364, 256).fill('#5F6369').font({ size: 12 });
arrowTextGroup.add(polyline_paid_rate)
.add(polygon_paid_rate)
.add(image_paid_transform)
.add(text_paid_transform);
...
}
复制代码
最后效果,上面也看到了:
本文从实际需求触发,从零开始学习SVG基础并实践使用开发,期间所历时间较短,可能理解不是很深,各位看官不喜勿喷。
在多边形区域内绘制文字的时候,我在想一件事,正常来讲,若是多边形区域做为父元素,在内部绘制文本节点 text,这样的画岂不是会简单不少?那么出来的svg元素应该就是下面这段代码的样子:
<svg>
<rect> // 矩形
<text></text> // 矩形内的文本
</rect>
<text>矩形外的文本</text>
</svg>
复制代码
而我在使用svg.js
以及阅读文档,并无发现这种使用方式。都是经过绘制的前后顺序来肯定文本位置的,也就是下面这样。
<svg>
<rect></rect> // 矩形
<text></text> // 矩形内的文本,经过x y 肯定位置
<text></text> // 矩形外的文本,经过x y肯定位置
</svg>
复制代码
固然,由于只是简单看看就直接上手了,因此多是我不知道如何使用,若是你们有用过而且知道的,能够留言给个提示~万分感谢。