React數據大屏的應用實踐

本文首發於 hzzly的博客css

原文連接:React數據大屏的應用實踐html

數據大屏與數據可視化

現現在大數據已無所不在,而且正被愈來愈普遍的被應用到歷史、政治、科學、經濟、商業甚至滲透到咱們生活的方方面面中,獲取的渠道也愈來愈便利。vue

今天咱們就來聊一聊「大屏應用」,說到大屏就必定要聊到數據可視化,現現在,數據可視化因爲數據分析的火熱也變得火熱起來,不過數據可視化並非一個新技術,可視化數據就是用可視化的方式展示的數據。而數據大屏做爲大數據展現媒介的一種,普遍運用於各類展現廳、會展、發佈會及各類狂歡節中,其中不乏一些通用的處理方案:阿里的DataV、百度的Suger、騰訊RayData等等。react

隨着物聯網、5G等各類跟鏈接有關的技術的出現與發展,每一個人手中掌握的數據量都呈指數級增加,光看這些數是看不過來也看不懂的,「數據可視化」就是一種簡化,讓艱難的數據理解過程,變成——看顏色,辨長短,分高低。從而大大縮短理解數據所需的時間。webpack

因公司的自研產品涉及到BI模塊,所以數據大屏展現的需求孕育而生(數據大屏需求已經完成)。git

下面是本人針對這個數據大屏需求前期作的一些探索實踐,數據也是mock的。github

gif圖

技術選型

  • React 全家桶(React-Router、React-Redux、React Hooks)
  • Webpack 編譯打包
  • Echarts 圖表組件
  • Socket.IO 即時通信、通知與消息推送
  • Grid 網格佈局

系統搭建

圖表選擇

六種基本圖表涵蓋了大部分圖表使用場景,也是作數據可視化最經常使用的圖表類型:web

  • 柱狀圖 用來反映分類項目之間的比較;
  • 餅圖 用來反映構成,即部分佔整體的比例;
  • 折線圖 用來反映隨時間變化的趨勢;
  • 條形圖 用來反映分類項目之間的比較;
  • 散點圖 用來反映相關性或分佈關係;
  • 地圖 用來反映區域之間的分類比較。

基本圖表類型都有通用的樣式,不過多的展開講解。咱們更多的考慮如何選擇經常使用圖表來呈現數據,達到數據可視化的目標。基本方法:明確目標 —> 選擇圖形 —> 梳理維度 —> 突出關鍵信息ajax

數據請求推送

當信息一旦準備就緒,咱們就須要從服務器獲取它們。這裏咱們須要一種基於推送的方法,例如 WebSocket 協議、輪詢、服務器推送事件(SSE)以及最近的 HTTP2 服務器推送。這裏咱們簡單比較一下 WebSocket 與輪詢。服務器

輪詢須要客戶端定時向服務器發送ajax請求,服務器接到請求後返回響應信息。這就須要大量的佔據服務器資源。同時在HTTP1.x協議中也存在一些好比線頭阻塞、頭部冗餘等問題。因此這種方案直接pass了。

再來講說 WebSocket,創建在 TCP 協議之上,數據格式比較輕量,性能開銷小,通訊高效,能夠發送文本,也能夠發送二進制數據。同時它尚未同源限制,客戶端能夠與任意服務器通訊。還有一點 WebSocket 一般不使用 XMLHttpRequest,所以,當咱們每次須要從服務器獲取更多的信息時,無需發送頭部數據。反過來講,這又減小了數據發送到服務器時須要付出的高昂的數據負載代價。對於數據大屏須要實時獲取數據,這無疑是最高效的。

佈局

數據大屏的核心就是數據的拼接,具體到展現層能夠概括成數據塊的拼接。這裏咱們採用通用的尺寸1920*108(16:9)。尺寸確立後,接下來要對展現層進行佈局和頁面的劃分。這裏的劃分,主要根據咱們以前定好的業務指標進行,核心業務指標安排在中間位置、佔較大面積;其他的指標按優先級依次在覈心指標周圍展開。通常把有關聯的指標讓其相鄰或靠近,把圖表類型相近的指標放一塊兒,這樣能減小觀者認知上的負擔並提升信息傳遞的效率。

對於這種塊狀(網格)佈局,咱們就可使用咱們強大的 CSS 佈局方案 -- Grid。它將網頁劃分紅一個個網格,能夠任意組合不一樣的網格,作出各類各樣的佈局。

安利一個grid 佈局可視化設計工具 -- CSS Grid Generator。可使用它生成對應的代碼,幫助我們快速佈局。

grid

項目結構

聊完這些通用知識咱們就能夠上手開發了。

我這裏使用了我本身開發的腳手架(hzzly-cli)來生成react項目環境。

有興趣瞭解腳手架開發的能夠看我這篇文章動手開發一個本身的項目腳手架

項目結構以下:

├── src
│   ├── assets // 資源目錄
│   ├── components // 公共組件目錄
│   │   ├── Card // Card組件
│   │   ├── Charts // 圖表組件目錄
│   │   │   ├── Bar // 柱狀圖
│   │   │   ├── ChinaMap // 中國地圖
│   │   │   ├── Funnel // 漏斗圖
│   │   │   ├── Line // 折線圖
│   │   │   ├── Pie // 餅圖
│   │   │   └── lib // 基礎圖表組件
│   │   ├── ScrollNumber // 滾動數字組件
│   │   └── SvgIcon // Icon組件
│   ├── global.scss
│   ├── index.js
│   ├── pages // 分塊結構目錄
│   ├── router // 路由
│   ├── store
│   │   ├── actions
│   │   ├── index.js
│   │   ├── reducers
│   │   ├── sagas
│   │   └── types.js
│   └── utils
│       ├── genChartData.js
│       ├── genMapData.js
│       ├── socket.js
│       └── util.js
複製代碼

知識點

Chart基礎組件封裝

這裏對echarts-for-react進一步封裝,其它圖表組件能夠直接繼承使用。

// Charts/lib/BaseChart.js
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Echarts from 'echarts-for-react';

export default class BaseChart extends PureComponent {
  static propTypes = {
    option: PropTypes.object.isRequired,
    data: PropTypes.object.isRequired,
    getOption: PropTypes.func.isRequired,
    style: PropTypes.object,
  };

  static defaultProps = {
    style: {},
  };

  componentDidMount() {
    const { runAction } = this.props;

    if (this.chartRef && runAction) {
      const chartIns = this.chartRef.getEchartsInstance();
      window.setTimeout(() => {
        runAction(chartIns);
      }, 300);
    }
  }

  render() {
    const { option, data, getOption, style } = this.props;

    const finalOption = getOption(option, data);
    const finalStyle = getStyle(style);

    return (
      <Echarts ref={ref => { this.chartRef = ref; }} style={finalStyle} option={finalOption} notMerge lazyUpdate /> ); } } function getStyle(style) { return Object.assign({ position: 'relative' }, style ); } 複製代碼

使用:

// line.js
import BaseChart from '../lib/BaseChart';
import option from './option';
import getOption from './getOption';

export default class Line extends BaseChart {
  static defaultProps = {
    option,
    getOption,
  };
}

// option.js 基礎配置
export default {
  // ...
};

// getOption.js 計算配置文件
function seriesCreator(series) {
  return series.map(e => ({
    type: 'line',
    symbol: 'circle',
    smooth: true,
    lineStyle: {
      normal: {
        width: 3,
      },
    },
    ...e,
  }));
}

export default function(option, data) {
  const { tooltip, xAxis, yAxis, yCategory, series = [], ...rest } = data;

  return {
    ...option,
    xAxis: {
      ...option.xAxis,
      ...xAxis,
    },
    tooltip: {
      ...option.tooltip,
      ...tooltip,
    },
    yAxis: {
      ...option.yAxis,
      ...yAxis,
      data: yCategory || [],
    },
    series: seriesCreator(series),
    ...rest,
  };
}
複製代碼

Socket封裝SDK

這裏對socket.io-client封裝成SDK,方便使用。

import io from 'socket.io-client';

const socket = {
  wsConn: null,
  config: {
    wsHost: '/', // wesocket host

    onConn() {},
    onDisconn() {},
    onError() {},
    onReceiveMsg() {},
  },

  init(opt) {
    socket.config = { ...socket.config, ...opt };
  },

  getWs() {
    if (socket.wsConn) {
      return socket.wsConn;
    } else {
      socket.initWs();
    }
  },

  getWsStatus() {
    return socket.wsConn ? socket.wsConn.connected : false;
  },

  initWs() {
    if (socket.getWsStatus()) {
      return socket.wsConn;
    }

    const wsUrl = socket.config.wsHost;

    socket.wsConn = io.connect(wsUrl);
    socket.wsConn.on('connect', () => {
      socket.config.onConn(socket.wsConn);
    });

    socket.wsConn.on('message', (...param) => {
      socket.config.onReceiveMsg(...param);
    });

    socket.wsConn.on('disconnect', () => {
      socket.config.onDisconn();
    });
    return socket.wsConn;
  },

  reconnect() {
    if (socket.wsConn) {
      if (socket.wsConn.disconnected) {
        // reconnect ws
      } else {
        // do nothing
      }
    } else {
      socket.initWs();
    }
  },

  disconnect() {
    if (socket.wsConn) {
      if (socket.wsConn.connected) {
        socket.wsConn.disconnect();
      }
    }
  },

  wsEmit(params) {
    if (socket.wsConn) {
      socket.wsConn.emit(params.name, params.data);
    }
  },
};

(function(global) {
  global.socket = socket;
})(window);

export { socket };
複製代碼

動態數字展現

該數據經過socket推送實時更新。

數字過渡的動態效果爲對應數位的新數字從下至上替換舊數字,若是該位數的數字沒有發生變化,則沒有過渡效果。

一、對數據進行完善並格式化

針對數字少於9位數進行前位補零並進行千分位格式化

const MAX_LEN = 9;

function toThousands(val) {
  let num = (val || 0).toString();
  while (num.length < MAX_LEN) {
    num = `0${num}`;
  }
  let result = '';
  while (num.length > 3) {
    result = `,${num.slice(-3)}${result}`;
    num = num.slice(0, num.length - 3);
  }
  if (num) {
    result = num + result;
  }
  return result.toString().split('');
}
複製代碼

二、過渡動畫

利用樣式控制過渡動畫,在第一步中咱們對數字進行了格式化,而後咱們針對每一位數字進行比較,當數字不相等的時候添加active類,最後對active類添加動畫。

// 循環渲染每一位數字
<li className={`${oldNumber[i] !== newNumber[i] ? 'active' : ''}`}>
  <span className="num">{oldNumber[i]}</span>
  <span className="num">{newNumber[i]}</span>
</li>
複製代碼
.active {
  .num {
    animation: move 1.5s;
    animation-fill-mode: forwards; // 讓動畫結束後保持最後一幀
  }
}

@keyframes move {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-100%);
  }
}
複製代碼

背景線性粒子

這裏我使用了我本身封裝的組件,能夠對應框架來安裝引用:

說明

一、項目框架目錄結構採用筆者本身搭建的webpack環境:webpack-template

二、關於適配和兼容性暫時還未完善,若是後期有時間會慢慢去完善

三、此項目爲筆者調研時的實踐,由於時間有限,一些功能還不善,設計和佈局都是本身的一些想象與參考

四、此項目做爲開源學習使用,謝絕用於商業應用

參考

相關文章
相關標籤/搜索