詳細介紹如何計算兩條折線的交點並使用Echarts展現以及圖表美化

一、背景

前段時間公司有個需求,須要在一個圖表中展現兩條折線,而且繪製出兩條線的交點。爲了知足需求大哥的需求,我也是着實想了有一會。下面我就把具體的實現過程給你們展現一下。html

1.一、ECharts 簡介

我的很喜歡Echarts這個圖表庫,就先給你們介紹一下,方便你們更好的瞭解。前端

ECharts 是一個使用 JavaScript 實現的開源可視化庫,能夠流暢的運行在 PC 和移動設備上,兼容當前絕大部分瀏覽器(IE8/9/10/11,Chrome,Firefox,Safari等),底層依賴矢量圖形庫 ZRender,提供直觀,交互豐富,可高度個性化定製的數據可視化圖表。git

ECharts 功能很強大,提供了常見的二維圖表,好比折線圖、柱圖、散點圖、餅圖、K線圖,以及地圖、熱力圖、線圖(又稱路徑圖),還有用於表示數據關係的關係圖、旭日圖等,此外還能夠經過GL實現絢麗的三維可視化圖。github

Echarts 使用起來很方便,展現不一樣的圖表只須要設置簡單的配置項;官方的文檔寫的也很清楚,從簡單的實現,到複雜的配置,以及內含的API,應有盡有;除此以外,還有豐富的實例庫,以及Echarts的使用者的做品庫,很大程度上下降了Echarts的使用門檻,方便初學者使用。並且如今成爲了Apache孵化器項目,將來的前景很好。數組

二、初始繪製圖表

須要展現的數據是與日期相關的兩條線,每一個數據均和一個日期相對應。以下:瀏覽器

// 兩條折線圖的數據,橫座標是日期,縱座標爲當前日期的數值
    var chartData = {
      line1: ["114", "114", "118", "114", "130", "130", "126", "130", "126", "130", "130", "134", "135", "135", "135", "135", "134", "132", "130", "128", "126", "124", "121", "119", "117", "116", "114", "112", "111", "110", "109", "108", "107", "106", "106", "105", "105", "105", "105", "106", "106", "108", "109", "112", "113", "115", "116", "118", "120", "123", "125", "128", "129", "130", "131", "131", "131", "131", "131", "130", "128", "126", "123", "119", "117", "115", "113", "112", "111", "110", "109", "107", "106", "105", "103", "103", "102", "101", "100", "99", "99", "99", "99", "101", "102", "104", "107", "109", "112", "114", "117", "119", "122", "124", "126", "127", "128", "129", "129", "130", "129", "128", "127", "126", "124", "122", "120", "117", "115", "113", "111", "109", "107", "105", "103", "100", "99", "98", "97", "97", "97", "98", "99", "101", "104", "107", "109", "112", "115", "117", "120", "123", "125", "128", "129", "130", "131", "131", "131", "131", "131", "130", "128", "126", "123", "119", "104"],
      line2: ["124", "124", "128", "124", "140", "140", "136", "140", "136", "140", "140", "136", "132", "128", "128", "124", "124", "120", "116", "112", "112", "112", "112", "112", "108", "108", "108", "104", "108", "104", "104", "100", "104", "104", "108", "104", "108", "104", "108", "112", "116", "120", "124", "128", "124", "120", "124", "128", "132", "136", "140", "140", "136", "132", "128", "128", "124", "124", "120", "116", "112", "108", "104", "100", "104", "108", "112", "116", "112", "108", "104", "100", "96", "92", "88", "92", "96", "100", "100", "104", "104", "108", "112", "116", "116", "116", "120", "120", "124", "132", "132", "132", "132", "136", "132", "132", "128", "128", "124", "124", "120", "120", "116", "116", "112", "108", "108", "104", "104", "104", "100", "96", "96", "92", "92", "88", "92", "96", "100", "104", "108", "112", "116", "120", "124", "128", "124", "120", "124", "128", "132", "136", "140", "140", "136", "132", "128", "128", "124", "124", "120", "116", "112", "108", "104", "100", "103"],
      date: ['200701', '200702', '200703', '200704', '200705', '200706', '200707', '200708', '200709', '200710', '200711', '200712', '200801', '200802', '200803', '200804', '200805', '200806', '200807', '200808', '200809', '200810', '200811', '200812', '200901', '200902', '200903', '200904', '200905', '200906', '200907', '200908', '200909', '200910', '200911', '200912', '201001', '201002', '201003', '201004', '201005', '201006', '201007', '201008', '201009', '201010', '201011', '201012', '201101', '201102', '201103', '201104', '201105', '201106', '201107', '201108', '201109', '201110', '201111', '201112', '201201', '201202', '201203', '201204', '201205', '201206', '201207', '201208', '201209', '201210', '201211', '201212', '201301', '201302', '201303', '201304', '201305', '201306', '201307', '201308', '201309', '201310', '201311', '201312', '201401', '201402', '201403', '201404', '201405', '201406', '201407', '201408', '201409', '201410', '201411', '201412', '201501', '201502', '201503', '201504', '201505', '201506', '201507', '201508', '201509', '201510', '201511', '201512', '201601', '201602', '201603', '201604', '201605', '201606', '201607', '201608', '201609', '201610', '201611', '201612', '201701', '201702', '201703', '201704', '201705', '201706', '201707', '201708', '201709', '201710', '201711', '201712', '201801', '201802', '201803', '201804', '201805', '201806', '201807', '201808', '201809', '201810', '201811', '201812', '201901', '201902', '201903']
    }

下面開始初始繪製圖表:前端工程師

  1. 初始化 echarts 實例。ECharts 圖表的展現須要依存在一個設置好 width、height 的 dom 中,Echarts 提供了一個初始化 echarts 實例的方法 echarts.init,此方法返回一個 echarts 實例,後續對圖表的設置均是在此實例中實現的。這個方法的第一個參數爲 dom,第二個參數設置圖表的 theme,第三個參數則是額外的一些配置。通常狀況下設置第一個參數便可。
  2. 設置圖表的配置項。配置項是一個 js 對象,配置項的解釋在下面的代碼中,更多的配置能夠參考官網。
  3. 繪製圖表。echarts 圖表的繪製、修改均經過 echarts實例的 setOption 方法完成。此方法能夠接受四個參數,第一個爲圖表的配置項 option,其他三個均爲額外的設置。平時使用第一個便可。

代碼以下:echarts

<style>
    .charts {
      width: 1000px;
      height: 600px;
    }
</style>
<!-- 準備好一個 dom,而且設置好 width 和 height,後續在此 dom 中繪製圖表 -->
<div id="chart" class="charts"></div>
<script>
// 基於準備好的dom,初始化一個 echarts 實例。
var myChart = echarts.init(document.getElementById('chart'));

// 設置圖表的配置項
var option = {
  // 標題
  title: {
    text: "初始繪製折線圖"
  },
  // 提示框組件,鼠標放置上去後,會展現所在位置信息
  tooltip: {},
  // 圖例組件,與 series 中的 name 對應,用於表示不一樣系列的標記、顏色和名字。也能夠經過點擊圖例控制哪些系列不顯示。
  legend: {
    data: ['line1', 'line2']
  },
  // x 軸的配置,默認類型爲 type="category" 類目軸。
  xAxis: {
    data: chartData.date // 設置 x 軸的數據
  },
  // y 軸的配置,雖然沒有寫其餘配置,可是必須有,不然會報錯
  yAxis: {},
  // 系列列表,一個系列便可理解爲一個圖表,經過 type 決定所展現的圖表類型。
  series: [{
    name: 'line1', // 單個圖表系列的 name, 和 legend 中的 data 對應
    type: 'line',
    data: chartData.line1
  }, {
    name: 'line2', // 單個圖表系列的 name, 和 legend 中的 data 對應
    type: 'line',
    data: chartData.line2
  }]
}

// 使用數據和配置項展現圖表
myChart.setOption(option)
</script>

執行完上面的代碼,初始的圖表展現就歐克了:dom

image

三、計算折線交點

3.一、計算前的準備

計算交點前,須要把數據進行處理,且根據處理的數據,調整圖表的展現。優化

首先,搞一個 計算兩個線段交點 的方法,這個方法經過傳入 兩條線段四個端點橫縱座標 值,來計算二者交點的座標:

// 求兩條線段交點,a,b 爲第一條線段的始末點,c,d 爲第二條線段的始末點。x,y 爲點的橫縱座標
function segmentsIntr({ a, b, c, d } = {}) {
  var denominator = (b.y - a.y) * (d.x - c.x) - (a.x - b.x) * (c.y - d.y)
  var x = ((b.x - a.x) * (d.x - c.x) * (c.y - a.y) +
    (b.y - a.y) * (d.x - c.x) * a.x -
    (d.y - c.y) * (b.x - a.x) * c.x) / denominator
  var y = -((b.y - a.y) * (d.y - c.y) * (c.x - a.x) +
    (b.x - a.x) * (d.y - c.y) * a.y -
    (d.x - c.x) * (b.y - a.y) * c.y) / denominator
  return [x, y]
}

而後傳入同一個水平刻度內的兩條線段的四個點座標,不過目前水平刻度所表明的是日期字符串,不適用於計算交點座標。

因此能夠將 x 軸 的數據替換成日期在數組中的序列號 1...n,而後展現 x 軸刻度的時候經過 xAxis 的 formatter 屬性,實現自定義刻度,將其轉換成對應日期便可。

下面是 xAxis 修改後的配置:

xAxis: {
    // 設置 x 軸的數據, 使用 日期 在數據中的 序列號 來表示 橫座標數值。
    data: chartData.date.map((seg, idx) => {
      return idx
    }),
    // 設置 x 軸的 展現標籤, 使其根據 當前標籤的序列號轉換爲 日期
    axisLabel: {
      formatter: function (params) {
        return chartData.date[params]
      }
    }
},

調整後的圖表以下:

會發現,X 軸已經好了,可是 提示框 tooltip 還須要再調整一下,那麼咱們利用 formatter 設置一下 自定義 的 tooltip。經過 formatter 自帶參數 params 中的值,拼接好返回的格式便可。

// 提示框組件,鼠標放置上去後,會展現 鼠標 所在位置信息
  tooltip: {
    trigger: 'axis', // 設置提示框爲:座標軸觸發。此項主要用於柱圖、折線圖的配置。
    formatter: function (params) { // params 爲一個數組,數組的每一個元素 包含了 該折線圖的點 全部的參數信息,好比 value(數值)、seriesName(系列名)、dataIndex(數據項的序號)
      let dateIndex = 0; // 當前指示點的 日期序號
      let tipList = params.map((seg) => {
        let { value, seriesName, dataIndex } = seg;
        dateIndex = dataIndex;
        return `${seriesName}:${value}`
      })
      tipList.unshift(`${chartData.date[dateIndex]}`)
      return tipList.join('<br/>')
    }
  },

展現效果以下:

這下子 tooltip 的格式調整好了,不過樣式有點怪,原來是 tooltip 缺乏了 系列名 前面的 小圓點dot

搞個生成 小圓點dot 的方法:

//獲取tooltip的dot,radius 爲圓點半徑,color 爲圓點顏色
function getTipDot({ radius = 5, color = "red" } = {}) {
  return `<span style='width:${radius * 2}px;height:${radius * 2}px;display:inline-block;border-radius: ${radius}px;background:${color};margin:0px 3px;'></span>`
}

把 tooltip 的配置調整一下,添加對於 dot 的設置:

// 提示框組件,鼠標放置上去後,會展現 鼠標 所在位置信息
  tooltip: {
    trigger: 'axis', // 設置提示框爲:座標軸觸發。此項主要用於柱圖、折線圖的配置。
    // params 爲一個數組,數組的每一個元素 包含了 該折線圖的點 全部的參數信息,
    // 好比 value(數值)、seriesName(系列名)、dataIndex(數據項的序號)、color(系列顏色)
    formatter: function (params) {
      let dateIndex = 0; // 當前指示點的 日期序號
      let tipList = params.map((seg) => {
        let { value, seriesName, dataIndex, color } = seg;
        dateIndex = dataIndex;
        return `${getTipDot({ color })}${seriesName}:${value}` // 添加對於 dot 的配置
      })
      tipList.unshift(`${chartData.date[dateIndex]}`)
      return tipList.join('<br/>')
    }
  },

調整後的效果以下:

3.二、計算交點

遍歷數據,取出兩條線每兩個相鄰的點組成線段,利用現有的方法 segmentsIntr 開始計算交點。

不過在計算以前,須要判斷當前線段內是否有交點,避免沒必要要的計算。

// 判斷兩條線段是否有交點, a一、b1 爲兩條線在 x1 處的值;a二、b2 爲兩條線在 x2 處的值;
// 只要不是一條線段的兩個點都高於另外一個點就會有交點;
function ifHaveIntersectionPoint(a1, b1, a2, b2) {
  return (+a1 > +b1) != (+a2 > +b2)
}

除了判斷是否有交點,還須要判斷是不是遍歷的最後一組數據,由於最後一組數據idx,是不會有idx+1的數據的。

// 是否執行後續的計算 ? 不是最後一個點,且有交點時
function ifCalculatePoint(idx, lth, [a1, b1, a2, b2] = []) {
  return idx !== (lth - 1) && ifHaveIntersectionPoint(a1, b1, a2, b2)
}

執行計算

// 獲取兩線全部交點
function getIntersectionPoint({ line1, line2, date } = {}) {
  // 交點數組
  var intersectionPointList = []
  date.map((seg, idx) => {
    // 分別是兩條線在相鄰兩處的數值,用於經過比較大小,來肯定此段內是否有交點
    var valueGroup = [line1[idx], line2[idx], line1[idx + 1], line2[idx + 1]]
    if (ifCalculatePoint(idx, date.length, valueGroup)) {
      var dotGroup = {
        a: { x: idx, y: line1[idx] },
        b: { x: idx + 1, y: line1[idx + 1] },
        c: { x: idx, y: line2[idx] },
        d: { x: idx + 1, y: line2[idx + 1] }
      }
      // 計算交點的位置
      var intersectionPoint = this.segmentsIntr(dotGroup)
      intersectionPointList.push(intersectionPoint)
    }
  })
  return intersectionPointList;
}

四、繪製交點

經過上面的交點計算,得出的結果以下:

intersectionPointList: [
    [11.4, 134.4], 
    [33.5, 106], 
    [34.666666666666664, 105.33333333333333], 
    [35.25, 105], 
    [36.75, 105], 
    [37.25, 105], 
    [53.4, 130.4], 
    [66.2, 112.8], 
    [68.33333333333333, 110.66666666666667], 
    [78, 100], 
    [96, 128], 
    [117.4, 97.6], 
    [135.4, 130.4]
]

能夠看到,計算出來的交點很不規律,大部分是小數,在當前的類目軸上並不能很好的展現。

不過好在,Echarts 能夠在同一個直角座標系中同時繪製多個座標軸。

咱們能夠在座標系中添加一條數值軸類型的x軸,而且用散點圖繪製出交點。初始繪製效果以下:

能夠看到如今有兩條 X 軸,調整一下配置項,隱藏掉上層的 X 軸展現。

// x 軸的配置,默認類型爲 type="category" 類目軸。
  xAxis: [{
    // 設置 x 軸的數據, 使用 日期 在數據中的 序列號 來表示 橫座標數值。
    data: chartData.date.map((seg, idx) => {
      return idx
    }),
    // 設置 x 軸的 展現標籤, 使其根據 當前標籤的序列號轉換爲 日期
    axisLabel: {
      formatter: function (params) {
        return chartData.date[params]
      }
    }
  }, 
  // 添加一個x軸用於展現散點圖(交點)
  {
    type: 'value',
    min: 0,
    max: chartData.date.length - 1,
    show: false
  }]

如上可見,簡單的交點已經算是計算並繪製出來了。可是實在是太醜了,下面我來優化一下。

五、圖表美化

5.一、總體基調的確認

由於如今流行深邃科技風(隨口一編~),那我們取色就順着這個方向去。

設置背景顏色爲 #010139,不過這裏背景我是使用了一個div當作背景層來搞的。代碼以下:

<style>
    .chartsArea {
      width: 1000px;
      height: 600px;
      position:relative;
    }

    .chartsAreaBack{
      background-color: #010139;
      width: 100%;
      height: 100%;
      position: absolute;
      opacity: 0.7;
      top: 0;
    }

    .charts {
      width: 100%;
      height: 100%;
    }
</style>
<div class="chartsArea">
    <div class="chartsAreaBack"></div>
    <!-- 準備好一個 dom,而且設置好 width 和 height,後續在此 dom 中繪製圖表 -->
    <div id="chart" class="charts"></div>
</div>

而後調整一下圖表裏面文字的顏色。

option:{
      ...
      // 設置標題的顏色
      title: {
        text: "設置圖表的主基調色",
        textStyle: {
          color: '#fff'
        }
      },
      ...
      // x 軸的配置,默認類型爲 type="category" 類目軸。
      xAxis: [{
        // 設置 x 軸的數據, 使用 日期 在數據中的 序列號 來表示 橫座標數值。
        data: chartData.date.map((seg, idx) => {
          return idx
        }),
        // 設置 x 軸的 展現標籤, 使其根據 當前標籤的序列號轉換爲 日期
        axisLabel: {
          formatter: function (params) {
            return chartData.date[params]
          },
          color: '#04a5bd',//設置標籤的樣式
          fontWeight: 'bold'
        }
      },
      // 添加一個x軸用於展現散點圖(交點)
      {
        type: 'value',
        min: 0,
        max: chartData.date.length - 1,
        show: false
      }],
      // y 軸配置標籤顏色
      yAxis: {
        axisLabel: {
          color: '#04a5bd',
          fontWeight: 'bold'
        }
      }
      ...
}

而後,就是這樣了,仍是挺醜的哈~

5.二、對座標軸進行優化

咱們能夠看到當下的x、y軸以及裏面的軸線都很難看,咱們再繼續調整一下。

x軸調整座標軸顏色以及刻度線的樣式。

// 座標軸線的顏色調整一下
axisLine: {
  lineStyle: {
    color: '#00386d',
    opacity: 0.6
  }
},
// 座標軸刻度取消展現
axisTick: {
  show: false
}

y 軸將分割線的顏色調整一下,而且取消y軸主軸線的展現。

splitLine: {
    show: true,
        lineStyle: {
            color: '#00386d',
            opacity: 0.4
        }
    },
    axisLine: {
        show: false
    }

圖表的取值靠在上半區,因此咱們能夠調整一下y軸的展現範圍。

scale: true

調整完上面的之後,大概就是這個樣子了(我偷偷加了個背景圖~)

這樣看起來就舒服一點了,不過還得繼續調整。

5.三、調整折線圖以及散點圖的樣式

咱們先調整一下折線圖的線條顏色、寬度以及形狀等地方。

{
    name: 'line1', // 單個圖表系列的 name, 和 legend 中的 data 對應
    type: 'line',
    symbol:'none', // 取消折線圖上圓點的展現
    smooth:true, // 將折線進行平滑展現
    itemStyle: { // 設置折線圖顏色
      color: '#6B72E7'
    },
    lineStyle: { // 設置線條的寬度
      width: 0.7,
    },
    data: chartData.line1
  }, {
    name: 'line2', // 單個圖表系列的 name, 和 legend 中的 data 對應
    type: 'line',
    symbol:'none',
    smooth:true,
    itemStyle: {
      color: '#E93AC8'
    },
    lineStyle: {
      width: 0.7,
    },
    data: chartData.line2
  }

至於散點圖,我想着上升交點以及降低交點的顏色給區分開來,因此須要改造一下前面計算交點的方法。

// 獲取兩線全部交點
function getIntersectionPoint({ line1, line2, date } = {}) {
  // 交點數組
  var intersectionPointList = []
  date.map((seg, idx) => {
    // 分別是兩條線在相鄰兩處的數值,用於經過比較大小,來肯定此段內是否有交點
    var valueGroup = [line1[idx], line2[idx], line1[idx + 1], line2[idx + 1]]
    if (ifCalculatePoint(idx, date.length, valueGroup)) {
      var dotGroup = {
        a: { x: idx, y: line1[idx] },
        b: { x: idx + 1, y: line1[idx + 1] },
        c: { x: idx, y: line2[idx] },
        d: { x: idx + 1, y: line2[idx + 1] }
      }
      // 計算交點的位置
      var intersectionPoint = this.segmentsIntr(dotGroup)
      // 給每一個數據拼接第三個值,表明是上升仍是降低【新增】
      intersectionPoint = [].concat(intersectionPoint,line1[idx + 1] > line2[idx + 1])
      intersectionPointList.push(intersectionPoint)
    }
  })
  return intersectionPointList;
}

而後,根據得出的值,設置散點圖的樣式。調整散點圖的點形狀、顏色。

{
    name: 'scatter',
    type: 'scatter',
    xAxisIndex: 1,
    data: getIntersectionPoint(chartData).map((seg) => {
      return {
        value: seg.slice(0, 2),
        symbol:'pin',
        symbolSize:30,
        itemStyle: {
          // 根據上升仍是降低,來設置漸變色
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
            offset: 0,
            color: seg[2]?'#E93AC8':'#6B72E7'
          }, {
            offset: 0.9,
            color: '#08178c'
          }, {
            offset: 1,
            color: '#08178c'
          }], false)
        }
      }
    })
  }

哦!對了,前面忘記改圖例的文字顏色,這裏補充一下。

legend: {
    data: ['line1', 'line2'],
    textStyle: {
      color: '#fff'
    },
  },

下面看一下效果,感受是否是像個樣了?

六、結語

其實針對這個圖表還有不少能夠作的地方,我這裏只是給出了一個計算圖表中兩折線交點並繪製的大概方法,以及對於圖表進行了一些簡單的優化,但願此文章可以幫助你熟悉 Echarts 的用法,可以幫助你解決一些問題那就更好了~

github地址:https://github.com/JHCan333/can-Share/blob/master/demos-tips/getTwoLineNode.html

我是 JHCan333,公衆號:愛生活的前端狗 的做者。公衆號專一前端工程師方向,包括但不限於技術提升、職業規劃、生活品質、我的理財等方面,會持續發佈優質文章,從各個方面提高前端開發的幸福感。關注公衆號,咱們一塊兒向前走!

相關文章
相關標籤/搜索