D3數據可視化-折線-line

案例

折線圖適合展示隨着時間推進,數值的變化趨勢。下圖是幾家科技公司在2009年6月到2019年6月間的股票價格圖,數據來源於雅虎金融。

解析

SVG 有幾種元素都可以畫出線段。

  • line: 用於繪製直線,只需要起始和結束點的x軸和y軸座標就可以確定一段直線的位置。類似這樣 <line x1="0" y1="0" x1="100" y="100" stroke="black" />

  • polyline: 折線元素,需要起始和結束以及中間各點的座標來定位線段。例子像這樣:
    <polyline points="0,0 20,30 50,40" fill="stroke" stroke="black"/>

  • path: 可以渲染各種各樣的圖形,d3.line 可以幫助我們生成 d 屬性用於描述折線。

在上面的折線例子中,縱座標代表股票價格,可以使用 scaleLinear 來 scale 值到圖形高度。橫座標代表連續的時間點,每個刻度(tick)代表一天,scaleTime 正好適合這種場景。

需要注意的是,有的時間座標場景並不適合使用 scaleTime。如果橫座標代表不同的月份,由於每個月份的天數不一樣,那麼每個月份刻度之間的距離會不同,這可能並不是我們期望的結果。如果希望月份之間距離相同,那麼還是採用 scalePoint 比較合適。

實現

慣例先上Git地址

核心代碼就幾行

const xScale = d3.scaleTime()
  .domain([firstDate, lastDate])
  .range([0, maxWidth]);
const yScale = d3.scaleLinear()
  .domain([minY, maxY])
  .range([maxHeight, 0]);
const line = d3.line().x((d) => xScale(d)).y((d) => yScale(d));

d3.csv() 可以讀取 csv 文件,因爲這次需要讀取幾個比較大的文件,採用了並行讀取的方式。

下面是完整版:

<!DOCTYPE html>
<html>
  <body>
    <style>
      svg {
        border: 1px solid lightgrey;
      }
    </style>

    <div>
      Stock Price of Tech Companies in the recent 10 years
    </div>
    <script src="http://d3js.org/d3.v5.min.js"></script>
    <script type="text/javascript">
      const maxHeight = 400;
      const maxWidth = 600;
      const barWidth = 20;
      const svg = d3.select('body')
        .append('svg')
        .attr('width', maxWidth + 50)
        .attr('height', maxHeight + 80);
      const colorArray = ['#38CCCB', '#0074D9', '#2FCC40', '#FEDC00', '#FF4036'];
      function renderLines(data, legends) {
        const getX = (d) => d.date;
        const getY = (d) => d.close;
        const lineMin = (data) => d3.min(data, getY);
        const lineMax = (data) => d3.max(data, getY);
        const xScale = d3.scaleTime()
          .domain(d3.extent(data[0], getX))
          .range([0, maxWidth]);
        const minY = d3.min(data, lineMin);
        const maxY = d3.max(data, lineMax);
        const yScale = d3.scaleLinear()
          .domain([minY, maxY])
          .range([maxHeight, 0]);
        const line = d3.line().x((d) => xScale(getX(d))).y((d) => yScale(getY(d)))
        svg.selectAll('path')
          .data(data)
          .enter()
          .append('path')
          .attr('d', (d) => {
            const lineData = line(d);
            return lineData;
          })
          .attr('stroke', (d, i) => colorArray[i % colorArray.length])
          .attr('fill', 'none');
        const axisRight = d3.axisRight(yScale);
        svg.append('g')
          .attr('transform', `translate(${maxWidth}, 0)`)
          .call(axisRight);
        const axisBottom = d3.axisBottom(xScale);
        svg.append('g')
          .attr('transform', `translate(0, ${maxHeight})`)
          .call(axisBottom);
        svg.append('g')
          .attr('width', 500)
          .attr('height', 30)
          .selectAll('.legend')
          .data(legends)
          .enter()
          .append('text')
          .attr('class', 'legend')
          .text((d) => d)
          .attr('y', maxHeight + 50)
          .attr('x', (d, i) => 50 + i * 100)
          .attr('stroke', (d, i) => colorArray[i % colorArray.length])
      }
      async function getData(fileLocation) {
        const data = await d3.csv(fileLocation, (row) => {
          return {
            date: new Date(row.Date),
            close: parseFloat(row.Close),
          };
        });
        return data;
      }
      async function render() {
        const files = ['AAPL.csv', 'INTC.csv', 'FB.csv', 'AMZN.csv', 'GOOG.csv']; // stock price in the recent 10 years
        const legends = files.map((file) => {
          const organization = /[a-zA-Z0-9]+(?=\.csv)/;
          const searchRes = organization.exec(file);
          return searchRes ? searchRes[0] : '';
        });
        const fetchQueue = files.map(getData);
        Promise.all(fetchQueue).then((data) => renderLines(data, legends));
      }
      render();
    </script>
  </body>
</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章