邊寫邊學系列(三) —— 使用 SVG.js 繪製產品自定義圖表

系列目錄

【一】:使用 apidoc,搞定自動化文檔node

【二】:使用 Express-Validator進行後端校驗react

【三】:使用 SVG.js 繪製產品自定義圖表git

前言

最近接到了一個小需求,須要畫一個數據漏斗圖,本來也沒什麼,由於原本項目裏就有 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學習實踐系列,僅供你們參考,最後我實現出來的效果是這樣的:

看起來基本一致哈~😄

SVG 基礎

這裏我就介紹幾個特別經常使用的吧,簡單介紹一下使用方法,由於官方文檔寫的十分詳細,各個API的用法。惟一的缺點就是是英文的,因此在這裏我就簡單的把幾個經常使用的介紹一下,其餘的你們業務場景使用到了再去翻API就好了。

svg.js 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這個生命週期進行初始化獲取。

畫直線 —— line(x1, y1, x2, y2)

首先,咱們來使用最簡單的 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 });
複製代碼

起始位置 —— move(x, y)

上面直線,很容易就畫出來了,也就是從(0, 100) -> (100, 0),不過呢,我畫的時候不必定非要在起始位置畫吧,有可能我就想在畫布中間,那怎麼辦?沒錯,有設置起始位置的API —— move

// 從(100, 100)開始畫
draw.line(0, 100, 100, 0)
    .move(100, 100)
    .stroke({ color: '#f06', width: 10 });
複製代碼

如圖,能夠看到,起始位置也能夠設置,而且代碼的位置由於是鏈式,因此放在哪裏都是能夠的。

畫矩形 —— rect(width, heigth)

畫矩形就更簡單了,只須要傳入你想要畫的長和寬就好了,固然你也能夠從任何地方開始畫。

// 好比,我也想從(100, 100)畫
draw.rect(140, 140).move(100, 100);
複製代碼

畫出來是黑色,也就是默認填充是黑色,我但願變成藍色,那麼可使用fill('blue')

通常來講,與顏色相關的 API 有兩種,fillstroke,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 });
}
複製代碼

這裏想說明的就是,當你須要在繪製的圖形內部畫文字的時候,繪製的前後順序必定不能亂,必定是先畫圖形,再畫文本。

畫圓形 —— circle(r)

畫圓就更簡單了,只須要給出半徑就行。

// 從(200, 200)開始畫一個半徑是100的紅邊橘色圓
draw.circle(100)
    .move(200, 200)
    .fill('orange')
    .stroke({ color: 'red', width: 4 });
複製代碼

畫多邊形 —— polygon(pointStr|pointArr)

繪製多邊形也很簡單,只不過畫的過程須要開發者設計好,參數接受兩種類型第一種是用逗號隔開的座標,座標x y用空格間隔,第二種是點二維數組。

// 字符串參數繪製三角形 
draw.polygon('300 300, 360 240, 360 360');
// 點座標繪製矩形
draw.polygon([[400, 400], [440, 400], [500, 300], [400, 300]])
  .fill('green');
複製代碼

畫多邊線段 —— polyline(pointStr|pointArr)

繪製多邊曲線與上面多邊形類似,只不過就是用線段展現,能夠理解爲中空。

// 繪製多邊曲線
draw.polyline('0,0 100,50 50,100').fill('none').stroke({ width: 1 })
複製代碼

畫圖片 —— image(url|path)

draw.image('https://cdn.img42.com/4b6f5e63ac50c95fe147052d8a4db676.jpeg')
    .size(60, 60) // 設置繪製的長寬
    .move(500, 100);
複製代碼

畫文本 —— text(str)

繪製文本就是,直接繪製的文字放進來就行

draw.text('我是被繪製的文本')
    .move(600, 200)
    .stroke({ color: 'yellow' })
    .font({ size: 20 });
複製代碼

添加事件 —— on

到如今爲止,基本簡單的都介紹完了,繪製一些基本需求沒啥問題了,剩下的就是,咱們繪製上去的是 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 提示一塊兒講解。

第四步:設計 hover 提示區域

上面按照計算,漏斗圖以及對應的文字已經繪製完成,接下來,最複雜的地方就在這裏了。每個文字前面都有一個 hvoer 提示,鼠標懸浮上去會有個小彈窗。這裏的思路很明顯要加上on('mouseover', funciton())事件。不過彈窗要怎麼展現就是難點了。總不能每一個 hover 都事先畫出來而後 hover 的時候再顯示吧。雖然這樣也行,可是一是我以爲複雜,二是如何繪製隱藏而且還有複雜定位的彈窗。

因此,最後個人實現思路:

  • fixed定位全局惟一一個 hover 彈窗,裏面內容爲空
<div id='statistics_draw' className='chart-container'>
    <div id='hover_container' className='tooltip-container' />
</div>
複製代碼
  • 鼠標移入對應元素的時候,手動計算 hover 彈窗應該出現的位置
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繪製三角形
  • 文字以及 hover 複用上面第二部份內容便可

剩下的就是複雜的定位計算了。

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?

在多邊形區域內繪製文字的時候,我在想一件事,正常來講,若是多邊形區域做爲父元素,在內部繪製文本節點 text,這樣的畫豈不是會簡單不少?那麼出來的svg元素應該就是下面這段代碼的樣子:

<svg>
    <rect> // 矩形
        <text></text> // 矩形內的文本
    </rect>
    <text>矩形外的文本</text>
</svg>
複製代碼

而我在使用svg.js以及閱讀文檔,並無發現這種使用方式。都是經過繪製的前後順序來肯定文本位置的,也就是下面這樣。

<svg>
    <rect></rect> // 矩形
    <text></text> // 矩形內的文本,經過x y 肯定位置
    <text></text> // 矩形外的文本,經過x y肯定位置
</svg>
複製代碼

固然,由於只是簡單看看就直接上手了,因此多是我不知道如何使用,若是你們有用過而且知道的,能夠留言給個提示~萬分感謝。

相關文章
相關標籤/搜索