echarts

ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

ECharts 提供了常规的折线图、柱状图、散点图、饼图、K线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。

2018年3月全球著名开源社区Apache宣布百度ECharts进入Apache孵化器。

1、用法

在kbone里应用ECharts需要注意的是:为了拿到正确的 zrX/zrY,不走正常的 touch 事件,需要自己定义 canvas 的事件监听。

2、案例

kbone-advanced 目录下创建 10-kbone-echarts 目录,本案例在这个目录下完成。

2.1 创建 package.json

cd 10-kbone-echarts
npm init -y

编辑 package.json:

{
  "scripts": {
    "mp": "cross-env NODE_ENV=production webpack --config build/webpack.mp.config.js --progress --hide-modules"
  },
  "dependencies": {
    "echarts": "^4.6.0",
    "vue": "^2.5.11"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.0",
    "babel-preset-stage-3": "^6.24.1",
    "cross-env": "^5.0.5",
    "css-loader": "^0.28.7",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^1.1.4",
    "html-webpack-plugin": "^4.0.0-beta.5",
    "mini-css-extract-plugin": "^0.5.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "stylehacks": "^4.0.3",
    "url-loader": "^3.0.0",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3",
    "mp-webpack-plugin": "latest"
  }
}

安装依赖包:

npm install

2.2 配置 webpack

10-kbone-echarts/build 目录下创建 webpack.mp.config.js,内容如下:

const path = require('path')
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin')
const MpPlugin = require('mp-webpack-plugin') // 用于构建小程序代码的 webpack 插件

const isOptimize = true // 是否压缩业务代码,开发者工具可能无法完美支持业务代码使用到的 es 特性,建议自己做代码压缩

module.exports = {
  mode: 'production',
  entry: {
    index: path.resolve(__dirname, '../src/index/main.mp.js'),
    bar: path.resolve(__dirname, '../src/bar/main.mp.js'),
    scatter: path.resolve(__dirname, '../src/scatter/main.mp.js'),
    pie: path.resolve(__dirname, '../src/pie/main.mp.js'),
    line: path.resolve(__dirname, '../src/line/main.mp.js'),
    funnel: path.resolve(__dirname, '../src/funnel/main.mp.js'),
    gauge: path.resolve(__dirname, '../src/gauge/main.mp.js'),
    k: path.resolve(__dirname, '../src/k/main.mp.js'),
    radar: path.resolve(__dirname, '../src/radar/main.mp.js'),
    heatmap: path.resolve(__dirname, '../src/heatmap/main.mp.js'),
    tree: path.resolve(__dirname, '../src/tree/main.mp.js'),
    treemap: path.resolve(__dirname, '../src/treemap/main.mp.js'),
    sunburst: path.resolve(__dirname, '../src/sunburst/main.mp.js'),
    map: path.resolve(__dirname, '../src/map/main.mp.js'),
    graph: path.resolve(__dirname, '../src/graph/main.mp.js'),
    boxplot: path.resolve(__dirname, '../src/boxplot/main.mp.js'),
    parallel: path.resolve(__dirname, '../src/parallel/main.mp.js'),
    sankey: path.resolve(__dirname, '../src/sankey/main.mp.js'),
    themeriver: path.resolve(__dirname, '../src/themeriver/main.mp.js'),
  },
  output: {
    path: path.resolve(__dirname, '../dist/mp/common'), // 放到小程序代码目录中的 common 目录下
    filename: '[name].js', // 必需字段,不能修改
    library: 'createApp', // 必需字段,不能修改
    libraryExport: 'default', // 必需字段,不能修改
    libraryTarget: 'window', // 必需字段,不能修改
  },
  target: 'web', // 必需字段,不能修改
  optimization: {
    runtimeChunk: false, // 必需字段,不能修改
    splitChunks: { // 代码分隔配置,不建议修改
      chunks: 'all',
      minSize: 1000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 100,
      maxInitialRequests: 100,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    },

    minimizer: isOptimize ? [
      // 压缩CSS
      new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.(css|wxss)$/g,
        cssProcessor: require('cssnano'),
        cssProcessorPluginOptions: {
          preset: ['default', {
            discardComments: {
              removeAll: true,
            },
            minifySelectors: false, // 因为 wxss 编译器不支持 .some>:first-child 这样格式的代码,所以暂时禁掉这个
          }],
        },
        canPrint: false
      }),
      // 压缩 js
      new TerserPlugin({
        test: /\.js(\?.*)?$/i,
        parallel: true,
      })
    ] : [],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ],
      },
      {
        test: /\.vue$/,
        loader: [
          'vue-loader',
        ],
      },
      {
        test: /\.js$/,
        use: [
          'babel-loader'
        ],
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [{
          loader: 'url-loader',
          options: {
            esModule: false,
            limit: true,
            emitFile: false,
          },
        }],
      },
    ]
  },
  resolve: {
    extensions: ['*', '.js', '.vue', '.json']
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.isMiniprogram': process.env.isMiniprogram, // 注入环境变量,用于业务代码判断
    }),
    new MiniCssExtractPlugin({
      filename: '[name].wxss',
    }),
    new VueLoaderPlugin(),
    new MpPlugin(require('./miniprogram.config.js')),
  ],
}

10-kbone-echarts/build 目录下创建 miniprogram.config.js,内容如下:

module.exports = {    
    origin: 'https://test.miniprogram.com',    
    entry: '/',    
    router: {        
        index: ['/'],
        bar: ['/bar'],
        scatter: ['/scatter'],
        pie: ['/pie'],
        line: ['/line'],
        funnel: ['/funnel'],
        gauge: ['/gauge'],
        k: ['/k'],
        radar: ['/radar'],
        heatmap: ['/heatmap'],
        tree: ['/tree'],
        treemap: ['/treemap'],
        sunburst: ['/sunburst'],
        map: ['/map'],
        graph: ['/graph'],
        boxplot: ['/boxplot'],
        parallel: ['/parallel'],
        sankey: ['/sankey'],
        themeriver: ['/themeriver'],
    },
    redirect: {        
        notFound: 'index',        
        accessDenied: 'index',
  },
  generate: {
    // 构建完成后是否自动安装小程序依赖。'npm':使用 npm 自动安装依赖
        autoBuildNpm: 'npm'
    },
    app: {
        navigationBarTitleText: 'miniprogram-project',
    },
    projectConfig: {
        appid: '',
    projectname: 'kbone-echarts',
    },
    packageConfig: {
        author: 'wechat-miniprogram',
    },
}

2.3 创建入口组件

10-kbone-echarts/src/index 下创建 main.mp.js 文件,内容如下:

import Vue from 'vue'
import App from './App.vue'

export default function createApp() {
  const container = document.createElement('div')
  container.id = 'app'
  document.body.appendChild(container)

  return new Vue({
    el: '#app',
    render: h => h(App)
  })
}

10-kbone-echarts/src/index 下创建 App.js 文件,内容如下:

<template>
  <div class="panel">
    <div class="chart-with-img" v-for="chart in charts" :key="chart.id">
      <img :src="chart.icon" mode="aspectFit" @click="open(chart)" />
      {{chart.name}}
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      charts: [
        {id: 'bar', name: '柱状图', icon: require(`../images/bar.png`)},
        {id: 'scatter', name: '散点图', icon: require(`../images/scatter.png`)},
        {id: 'pie', name: '饼图', icon: require(`../images/pie.png`)},
        {id: 'line', name: '折线图', icon: require(`../images/line.png`)},
        {id: 'funnel', name: '漏斗图', icon: require(`../images/funnel.png`)},
        {id: 'gauge', name: '仪表盘', icon: require(`../images/gauge.png`)},
        {id: 'k', name: 'K 线图', icon: require(`../images/k.png`)},
        {id: 'radar', name: '雷达图', icon: require(`../images/radar.png`)},
        {id: 'heatmap', name: '热力图', icon: require(`../images/heatmap.png`)},
        {id: 'tree', name: '树图', icon: require(`../images/tree.png`)},
        {id: 'treemap', name: '矩形树图', icon: require(`../images/treemap.png`)},
        {id: 'sunburst', name: '旭日图', icon: require(`../images/sunburst.png`)},
        {id: 'map', name: '地图', icon: require(`../images/map.png`)},
        {id: 'graph', name: '关系图', icon: require(`../images/graph.png`)},
        {id: 'boxplot', name: '箱型图', icon: require(`../images/boxplot.png`)},
        {id: 'parallel', name: '平行坐标图', icon: require(`../images/parallel.png`)},
        {id: 'sankey', name: '桑基图', icon: require(`../images/sankey.png`)},
        {id: 'themeRiver', name: '主题河流图', icon: require(`../images/themeRiver.png`)}
      ],
    }
  },
  methods: {
    open(chart) {
      window.open(`/${chart.id}`);
    },
  },
}
</script>

<style>
.panel {
  display: block;
  margin-top: 10px;
}

.panel .chart-with-img {
  display: inline-block;
  width: 25%;
  margin: 10px 4%;
  text-align: center;
}

.chart-with-img image {
  box-sizing: border-box;
  width: 100%;
  height: 100px; 
  padding: 20px;
  background-color: #f3f3f3;
  border: 1px solid #eee;
  display: block;
  margin: 0 auto;
  margin-bottom: 10px;
}
</style>

2.4 创建 bar 组件

由于篇幅原因,这里只给一个bar的例子。在 10-kbone-echarts/src/bar 下创建 main.mp.js 文件,内容如下:

import Vue from 'vue'
import App from './App.vue'

export default function createApp() {
  const container = document.createElement('div')
  container.id = 'app'
  document.body.appendChild(container)

  return new Vue({
    el: '#app',
    render: h => h(App)
  })
}

10-kbone-echarts/src/bar 下创建 App.vue 文件,内容如下:

<template>
  <div class="container">
    <canvas
      ref="canvas"
      type="2d"
      :width="width"
      :height="height"
    >
    </canvas>
  </div>
</template>

<script>
import echarts from 'echarts'
import {getChart} from '../chart'

const systemInfo = wx.getSystemInfoSync()

export default {
  name: 'App',
  data() {
    return {
      width: systemInfo.windowWidth,
      height: systemInfo.windowHeight,
    }
  },
  mounted() {
    getChart(this.$refs.canvas, echarts, {
      width: this.width,
      height: this.height,
    }).then(this.initChart)
  },
  methods: {
    initChart(chart) {      
      chart.setOption({
        color: ['#37a2da', '#32c5e9', '#67e0e3'],
        tooltip: {
          trigger: 'axis',
          axisPointer: { // 坐标轴指示器,坐标轴触发有效
            type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
          },
          confine: true,
        },
        legend: {
          data: ['热度', '正面', '负面'],
        },
        grid: {
          left: 20,
          right: 20,
          bottom: 15,
          top: 40,
          containLabel: true,
        },
        xAxis: [{
          type: 'value',
          axisLine: {
            lineStyle: {
              color: '#999',
            },
          },
          axisLabel: {
            color: '#666',
          },
        }],
        yAxis: [{
          type: 'category',
          axisTick: { show: false },
          data: ['汽车之家', '今日头条', '百度贴吧', '一点资讯', '微信', '微博', '知乎'],
          axisLine: {
            lineStyle: {
              color: '#999',
            },
          },
          axisLabel: {
            color: '#666',
          },
        }],
        series: [{
          name: '热度',
          type: 'bar',
          label: {
            normal: {
              show: true,
              position: 'inside',
            },
          },
          data: [300, 270, 340, 344, 300, 320, 310],
          itemStyle: {
            // emphasis: {
            //   color: '#37a2da',
            // },
          },
        }, {
          name: '正面',
          type: 'bar',
          stack: '总量',
          label: {
            normal: {
              show: true,
            },
          },
          data: [120, 102, 141, 174, 190, 250, 220],
          itemStyle: {
            // emphasis: {
            //   color: '#32c5e9',
            // },
          },
        }, {
          name: '负面',
          type: 'bar',
          stack: '总量',
          label: {
            normal: {
              show: true,
              position: 'left',
            },
          },
          data: [-20, -32, -21, -34, -90, -130, -110],
          itemStyle: {
            // emphasis: {
            //   color: '#67e0e3',
            // },
          },
        }],
      })
    },
  },
}
</script>

<style>
.container {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
}
</style>

2.5 定制canvas

在Kbone中应用echarts,需要重新定制一下canvas。在 /src 下创建 chart.js 文件,内容如下:

function wrapTouch(evt) {
  for (let i = 0, len = evt.touches.length; i < len; ++i) {
    const touch = evt.touches[i]
    touch.offsetX = touch.x
    touch.offsetY = touch.y
  }
  return evt
}

export function getChart(canvas, echarts, options) {
  return new Promise((resolve, reject) => {
    // 使用新的 canvas 接口:https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html
    canvas.$$prepare().then(canvas => {
      // 避免走 document.createElement('canvas') 接口
      echarts.setCanvasCreator(() => canvas)

      // echarts.env.wxa = false // 去除小程序环境判断,为了走正常的 tooltip 创建逻辑
      // echarts.env.domSupported = true // 补充 dom 支持判断,让其走正常的 touch 事件处理,但是此处因为没有同步 getBoundingClientRect,会导致 zrX/zrY 计算错误,故注释

      canvas.addEventListener('canvastouchstart', evt => {
        if (chart && evt.touches.length > 0) {
          const touch = evt.touches[0]
          const handler = chart.getZr().handler
          handler.dispatch('mousedown', {
            zrX: touch.x,
            zrY: touch.y,
          })
          handler.dispatch('mousemove', {
            zrX: touch.x,
            zrY: touch.y,
          })
          handler.processGesture(wrapTouch(evt), 'start')
        }
      })

      canvas.addEventListener('canvastouchmove', evt => {
        if (chart && evt.touches.length > 0) {
          const touch = evt.touches[0]
          const handler = chart.getZr().handler
          handler.dispatch('mousemove', {
            zrX: touch.x,
            zrY: touch.y,
          })
          handler.processGesture(wrapTouch(evt), 'change')
        }
      })

      canvas.addEventListener('canvastouchend', evt => {
        if (chart) {
          const touch = evt.changedTouches ? evt.changedTouches[0] : {}
          const handler = chart.getZr().handler
          handler.dispatch('mouseup', {
            zrX: touch.x,
            zrY: touch.y,
          })
          handler.dispatch('click', {
            zrX: touch.x,
            zrY: touch.y,
          })
          handler.processGesture(wrapTouch(evt), 'end')
        }
      })

      // 为了拿到正确的 zrX/zrY,不走正常的 touch 事件
      canvas.addEventListener = () => {}

      const chart = echarts.init(canvas, null, {
        width: options.width || 300,
        height: options.height || 150,
      })

      resolve(chart)
    }).catch(reject)
  })
}

2.6 小程序端效果预览

npm run mp

results matching ""

    No results matching ""