用 D3.js 畫一個手機(jī)專利關(guān)系圖, 看看蘋果,三星,微軟間的專利糾葛

打個小廣告:
如果你想獲取更多前端干貨鸳址、鵝廠工程師的前端面試指南,
歡迎關(guān)注我的個人微信公眾號:
前端夜談

用 D3.js 畫一個手機(jī)專利關(guān)系圖, 看看蘋果,三星,微軟間的專利糾葛

前言

本文靈感來源于Mike Bostock 的一個 demo 頁面

原 demo 基于 D3.js v3 開發(fā), 筆者將其使用 D3.js v5 進(jìn)行重寫, 并改為使用 ES6 語法.

源碼: github

在線演示 : demo

效果

[圖片上傳失敗...(image-4b13f9-1531707279039)]

可以看到, 上圖左上角為圖例, 中間為各個手機(jī)公司之間的專利關(guān)系圖.

圖例中有三種線段:

  • 紅色實線: 正在進(jìn)行專利訴訟 (箭頭指向方為被訴訟方)
  • 藍(lán)色虛線: 訴訟已經(jīng)結(jié)束
  • 綠色實線: 專利已經(jīng)授權(quán)

實現(xiàn)

下面讓我們看看如何一步步實現(xiàn)上圖的效果.

分析數(shù)據(jù)

[
  { source: 'Microsoft', target: 'Amazon', type: 'licensing' },
  { source: 'Microsoft', target: 'HTC', type: 'licensing' },
  { source: 'Samsung', target: 'Apple', type: 'suit' },
  { source: 'Motorola', target: 'Apple', type: 'suit' },
  { source: 'Nokia', target: 'Apple', type: 'resolved' },
  { source: 'HTC', target: 'Apple', type: 'suit' },
  { source: 'Kodak', target: 'Apple', type: 'suit' },
  { source: 'Microsoft', target: 'Barnes & Noble', type: 'suit' },
  { source: 'Microsoft', target: 'Foxconn', type: 'suit' },
  ...
]

可以看到, 每一條數(shù)據(jù)都是由以下幾部分組成:

  • source : 訴訟方的公司名稱
  • target : 被訴訟方的公司名稱
  • type : 當(dāng)前訴訟狀態(tài)

需要注意的是: 有一些公司 (如 Apple, Microsoft ) 同時參與了多起訴訟案件, 但我們在數(shù)據(jù)可視化時只會為每一個公司分配一個節(jié)點, 然后用連線表示各個公司之間的關(guān)系.

數(shù)據(jù)可視化最重要的就是數(shù)據(jù)和圖像之間的映射關(guān)系, 本例中我們的可視化的邏輯為:

  • 將每一個公司作為圖中的一個圓形節(jié)點
  • 將每一條訴訟關(guān)系表示為兩個圓形節(jié)點之間的連線

公司 ==> 圓形節(jié)點

公司 ==> 圓形節(jié)點

訴訟關(guān)系 ==> 連線

公司 ==> 圓形節(jié)點

技術(shù)分析

要實現(xiàn)可以拖動, 自動布局的網(wǎng)絡(luò)圖, 本 demo 用到了 D3.js 中的 d3-forced3-drag , 當(dāng)然還有最基礎(chǔ)的 d3-selection.

(為了方便搭建用戶界面, 使用了 Vue 作為前端框架. 但 Vue 并不對數(shù)據(jù)可視化邏輯產(chǎn)生影響, 不使用也不會對我們的實現(xiàn)造成影響.)

代碼實現(xiàn)

現(xiàn)在讓我們進(jìn)入代碼部分, 首先我們畫出每個公司代表的圓形節(jié)點:

上面說到了, 原始數(shù)據(jù)中, 有部分公司多次出現(xiàn)在不同的訴訟關(guān)系中, 而我們要為每個公司畫出唯一的節(jié)點, 所以我們要對數(shù)據(jù)進(jìn)行一些處理:

  initData() {
    this.links = [
      { source: 'Microsoft', target: 'Amazon', type: 'licensing' },
      { source: 'Microsoft', target: 'HTC', type: 'licensing' },
      { source: 'Samsung', target: 'Apple', type: 'suit' },
      { source: 'Motorola', target: 'Apple', type: 'suit' },
      { source: 'Nokia', target: 'Apple', type: 'resolved' },
      ...
    ] // 這里省略了一些數(shù)據(jù)

    this.nodes = {}

    // Compute the distinct nodes from the links.
    this.links.forEach(link => {
      link.source =
        this.nodes[link.source] ||
        (this.nodes[link.source] = { name: link.source })
      link.target =
        this.nodes[link.target] ||
        (this.nodes[link.target] = { name: link.target })
    })
    console.log(this.links)
  }

上面這段代碼的邏輯是, 遍歷所有的 links, 將其中的 source 和 target 作為 key 放置到 nodes 中, 這樣我們就得到了不含重復(fù)節(jié)點的數(shù)據(jù) nodes:

公司 ==> 圓形節(jié)點

細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)了, 上面的數(shù)據(jù)中有許多 x, y 的坐標(biāo)數(shù)據(jù), 這些數(shù)據(jù)是從哪里來的呢? 答案就是 d3-force, 因為我們要實現(xiàn)的是模擬物理作用力的分布圖, 所以我們使用了 d3-force 來模擬并幫助我們計算出每個節(jié)點的位置, 調(diào)用方法如下:

this.force = this.d3
  .forceSimulation(this.d3.values(this.nodes))
  .force('charge', this.d3.forceManyBody().strength(50))
  .force('collide', this.d3.forceCollide().radius(50))
  .force('link', forceLink)
  .force(
    'center',
    this.d3
      .forceCenter()
      .x(width / 2)
      .y(height / 2)
  )
  .on('tick', () => {
    if (this.path) {
      this.path.attr('d', this.linkArc)
      this.circle.attr('transform', transform)
      this.text.attr('transform', transform)
    }
  })

這里我們?yōu)?d3-force 添加了三種作用力:

  • .force('charge', this.d3.forceManyBody().strength(50)) 為每個節(jié)點添加互相之間的吸引力
  • .force('collide', this.d3.forceCollide().radius(50)) 為每個節(jié)點添加剛體碰撞效果
  • .force('link', forceLink) 添加節(jié)點之間的連接力

執(zhí)行上面的代碼后, d3-force 就會為每一個節(jié)點計算好坐標(biāo)并將其 作為 x, y 屬性賦予每個節(jié)點.

畫出代表公司的 圓形節(jié)點

處理好了數(shù)據(jù), 讓我們將其映射到頁面上的 svg ==> circle 元素:

this.circle = this.svgNode // svgNode 為頁面中的 svg節(jié)點 (d3.select('svg'))
  .append('g')
  .selectAll('circle')
  .data(this.d3.values(this.nodes)) // d3.values() 將對象數(shù)據(jù) Object{}轉(zhuǎn)換為數(shù)組數(shù)據(jù) Array[]
  .enter()
  .append('circle')
  .attr('r', 10)
  .style('cursor', 'pointer')
  .call(this.enableDragFunc())

注意到這里我們在最后調(diào)用了 .call(this.enableDragFunc()) , 這點代碼是為了實現(xiàn) circle 節(jié)點的拖拽功能, 我們在后面再進(jìn)一步講解.

上面這段代碼邏輯為: 將 nodes 數(shù)據(jù)映射為 circle 元素, 并設(shè)置 circle 元素的屬性:

  • 半徑 10
  • 鼠標(biāo)懸停圖標(biāo)為手指
  • 將每個 node 的 x, y 屬性賦予 circle 的 x, y (˙ 這一步我們在代碼中沒有聲明, 是因為 d3 默認(rèn)會將數(shù)據(jù)的 x, y 屬性作為 circle 的 x, y 屬性)

執(zhí)行以上代碼后的效果:

circles

畫出公司名稱

畫出代表公司的圓形節(jié)點后, 再畫出公司名稱就很簡單了. 只需要將 x, y 坐標(biāo)進(jìn)行一定偏移即可.

這里我們將公司名稱放在圓形節(jié)點的右方:

this.text = this.svgNode
  .append('g')
  .selectAll('text')
  .data(this.d3.values(this.nodes))
  .enter()
  .append('text')
  .attr('x', 12)
  .attr('y', '.31em')
  .text(d => d.name)

上面的代碼只是將 text 元素放置在了 (12 , 0 ) 的位置, 我們在 d3-force 的每一個 tick 周期中, 對其 text 進(jìn)行位置的偏移, 這樣就達(dá)到了 text 元素在 circle 元素右側(cè) 12 個像素的效果:

this.force = this.d3
      ...
      .on('tick', () => {
        if (this.path) {
          this.path.attr('d', this.linkArc)
          this.circle.attr('transform', transform)
          this.text.attr('transform', transform)
        }
      })

效果如圖:

circles

畫出訴訟關(guān)系連線

接下來我們將有訴訟關(guān)系的節(jié)點連接起來. 因為連線不是規(guī)則的圖形, 所以我們使用 svg 的 path 元素來實現(xiàn).

this.path = this.svgNode
  .append('g')
  .selectAll('path')
  .data(this.links)
  .enter()
  .append('path')
  .attr('class', function(d) {
    return 'link ' + d.type
  })
  .attr('marker-end', function(d) {
    return 'url(#' + d.type + ')'
  })

我們使用 'link ' + d.type 為不同的訴訟關(guān)系連線賦予不同的 class, 然后通過 css 對不同 class 的連線添加不同的樣式(紅色實線, 藍(lán)色虛線, 綠色實線).

pathd 屬性我們同樣在 d3-force 的 tick 周期中設(shè)置:

this.force = this.d3
      ...
      .on('tick', () => {
        if (this.path) {
          this.path.attr('d', this.linkArc)
          this.circle.attr('transform', transform)
          this.text.attr('transform', transform)
        }
      })

  linkArc(d) {
    const dx = d.target.x - d.source.x
    const dy = d.target.y - d.source.y
    const dr = Math.sqrt(dx * dx + dy * dy)
    return (
      'M' +
      d.source.x +
      ',' +
      d.source.y +
      'A' +
      dr +
      ',' +
      dr +
      ' 0 0,1 ' +
      d.target.x +
      ',' +
      d.target.y
    )
  }

這里我們直接用字符串拼接了一小段 svg 的指令, 效果是畫出一條圓弧曲線, 完成上面的代碼后, 我們得到的效果是:

all svg ready

添加圖例

現(xiàn)在我們已經(jīng)基本完成了預(yù)期的效果, 但是圖中缺少圖例, 訪問者會不理解不同顏色的曲線分別代表著什么含義, 所以我們在畫面的左上角添加圖例.

圖例的實現(xiàn)方法大致上面步驟相同, 但是有兩個區(qū)別:

  • 圖例是固定在畫面左上角的, 坐標(biāo)可以在代碼中直接寫死
  • 圖例比真實數(shù)據(jù)多一個元素: 描述文字

我們構(gòu)造一下圖例的數(shù)據(jù):

const sampleData = [
  {
    source: { name: 'Nokia', x: xIndex, y: yIndex },
    target: { name: 'Qualcomm', x: xIndex + 100, y: yIndex },
    title: 'Still in suit:',
    type: 'suit'
  },
  {
    source: { name: 'Qualcomm', x: xIndex, y: yIndex + 100 },
    target: { name: 'Nokia', x: xIndex + 100, y: yIndex + 100 },
    title: 'Already resolved:',
    type: 'resolved'
  },
  {
    source: { name: 'Microsoft', x: xIndex, y: yIndex + 200 },
    target: { name: 'Amazon', x: xIndex + 100, y: yIndex + 200 },
    title: 'Locensing now:',
    type: 'licensing'
  }
]

const nodes = {}
sampleData.forEach((link, index) => {
  nodes[link.source.name + index] = link.source
  nodes[link.target.name + index] = link.target
})

按照同樣的步驟, 我們畫出圖例:

sampleContainer
  .selectAll('path')
  .data(sampleData)
  .enter()
  .append('path')
  .attr('class', d => 'link ' + d.type)
  .attr('marker-end', d => 'url(#' + d.type + ')')
  .attr('d', this.linkArc)

sampleContainer
  .selectAll('circle')
  .data(this.d3.values(nodes))
  .enter()
  .append('circle')
  .attr('r', 10)
  .style('cursor', 'pointer')
  .attr('transform', d => `translate(${d.x}, ${d.y})`)

sampleContainer
  .selectAll('.companyTitle')
  .data(this.d3.values(nodes))
  .enter()
  .append('text')
  .style('text-anchor', 'middle')
  .attr('x', d => d.x)
  .attr('y', d => d.y + 24)
  .text(d => d.name)

sampleContainer
  .selectAll('.title')
  .data(sampleData)
  .enter()
  .append('text')
  .attr('class', 'msg-title')
  .style('text-anchor', 'end')
  .attr('x', d => d.source.x - 30)
  .attr('y', d => d.source.y + 5)
  .text(d => d.title)

最終效果:

all svg ready

總結(jié)

使用 D3.js 進(jìn)行這樣的數(shù)據(jù)可視化非常簡單, 而且非常靈活. 只是在使用 d3-force 時需要多調(diào)整一下參數(shù)來達(dá)到理想的效果, 實際實現(xiàn)的代碼并不長, 邏輯代碼放在這個文件中: graphGenerator.js, 感興趣的讀者不妨直接看看源碼.

想繼續(xù)了解 D3.js

這里是我關(guān)于 D3.js 受葛、 數(shù)據(jù)可視化 博客 的github 地址, 歡迎 start & fork :tada:

D3-blog

如果覺得不錯的話, 不妨點擊下面的鏈接關(guān)注一下 : )

github: ssthouse

知乎專欄: Data Visualization / 數(shù)據(jù)可視化

掘金: ssthouse

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市艘蹋,隨后出現(xiàn)的幾起案子车酣,更是在濱河造成了極大的恐慌内边,老刑警劉巖榴都,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異漠其,居然都是意外死亡嘴高,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門和屎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阳惹,“玉大人,你說我怎么就攤上這事眶俩∮ㄌ溃” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵颠印,是天一觀的道長纲岭。 經(jīng)常有香客問我抹竹,道長,這世上最難降的妖魔是什么止潮? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任窃判,我火速辦了婚禮,結(jié)果婚禮上喇闸,老公的妹妹穿的比我還像新娘袄琳。我一直安慰自己,他們只是感情好燃乍,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布唆樊。 她就那樣靜靜地躺著,像睡著了一般刻蟹。 火紅的嫁衣襯著肌膚如雪逗旁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天舆瘪,我揣著相機(jī)與錄音片效,去河邊找鬼。 笑死英古,一個胖子當(dāng)著我的面吹牛淀衣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播召调,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼舌缤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了某残?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤陵吸,失蹤者是張志新(化名)和其女友劉穎玻墅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壮虫,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡澳厢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了囚似。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剩拢。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饶唤,靈堂內(nèi)的尸體忽然破棺而出徐伐,到底是詐尸還是另有隱情,我是刑警寧澤募狂,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布办素,位于F島的核電站角雷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏性穿。R本人自食惡果不足惜勺三,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望需曾。 院中可真熱鬧吗坚,春花似錦、人聲如沸呆万。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桑嘶。三九已至炊汹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逃顶,已是汗流浹背讨便。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留以政,地道東北人霸褒。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像盈蛮,于是被迫代替她去往敵國和親废菱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • d3 (核心部分)選擇集d3.select - 從當(dāng)前文檔中選擇一系列元素抖誉。d3.selectAll - 從當(dāng)前文...
    謝大見閱讀 3,441評論 1 4
  • 1.發(fā)現(xiàn)故事 本課講述可視化用到的:敘事結(jié)構(gòu)數(shù)據(jù)收集過程數(shù)據(jù)處理 2.新聞方法 給可視化添加語境圍繞數(shù)據(jù)進(jìn)行敘事 ...
    esskeetit閱讀 2,812評論 0 2
  • 我不記得這是第幾次熬夜寫稿子了殊轴,寫著自己不喜歡的形式文章。每一次也總是有老師幫助我袒炉,感謝旁理!
    有一只小熊閱讀 222評論 0 0
  • 環(huán)境 阿里云 CentOS 7.4 (Linux) 安裝方法 本次安裝使用rpm安裝包的方式參考文章 https:...
    小塵鳥閱讀 280評論 0 0
  • 今天學(xué)習(xí)第三課,蒙版很好玩我磁!
    迷鹿mirror閱讀 175評論 4 0