# superset二次开发

## 背景

大数据时代商业智能（BI）和数据可视化诉求更为强烈，淘宝大屏更是风靡全球！数据可视化是大数据『最后一公里』。 \
&#x20;数据可视化分析工具大致分为3类，开源、商业和传统bi工具。 业界目前比较流行的 \
&#x20;开源BI工具：Superset、metabase、Redash、Cboard、d3js、Spagobi等， \
&#x20;商业BI工具：帆软、tableau、PowerBI、SmartBI、QlinkView、QuickBI、fineBI、阿里dataV等 \
&#x20;传统BI工具：Congos、BIEE、BO、MicroStrategydeng等。

## superset简介

Superset是一款轻量级的BI工具，由Airbnb的数据部门开源。截止目前，github累计31.3K个star。

## 开发部署

官方文档：<http://superset.apache.org/>

### 环境

superset： 0.36.0

windows10

拉取镜像需配置代理:<http://f1361db2.m.daocloud.io>

#### docker部署安装步骤

docker search superset docker pull amancevice/superset docker run -d -p 8088:8088 --name superset amancevice/superset:latest --生成容器ID 60946ede506b docker exec -it -u root 60946ede506b fabmanager create-admin --app superset docker exec -it 60946ede506b superset db upgrade docker exec -it 60946ede506b superset load\_examples docker exec -it 60946ede506b superset init docker exec -it 60946ede506b superset runserver

#### mac下部署步骤

* 启动后端 superset

pip install -r requirements-dev.txt -i 国内源url

python -m flask run 默认端口起的是 **5000**

* 启动前端 superset-frontend

npm install

npm run dev-server

默认端口是 **9000**

**本地启动部署 (mac\win)**

mac 启动无需修改，windows启动需要修改两处。

1. 在webpack.config.js中新增代码

```
{
        test: /(\.jsx|\.js)$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime'],
          },
        },
        exclude: /node_modules/,
      },
```

&#x20;  2\. 在package.json中"dev-server"命令前加入 cross-env

通过该参数来设置环境

## 二次开发已实现的功能

### v1.0

* 增加和具体用户相关联的权限访问，不同用户看到的数据不同
* 添加数值单位，在原有支持K的基础上增加中文 千,万,亿等
* 新增两张自定义图表，top Bar （排行榜条形图）和 Mix line Bar （混合柱状条形图）
* 汉化支持

### v2.0

* 可以自由配置上钻下卷的链条
* 图表下钻，支持多级下钻
* 图表上卷，支持多级上卷
* 图表联动，在对一张图表配置了联动后，类似下钻的点击会影响到其他图表

### v3.0

* 给FilterBox新增级联属性，支持类似‘省份-区域-城市’这样的层级筛选
* 添加统一登录，接入ldap账号体系内部实现无差别登陆，默认为普通用户权限

### v4.0

* 添加集成多个dashboard的门户层，根据层级菜单将多个dashboard加进来
* 增加漏斗图(Funnel)

### v5.0

* 用antv改写了top bar,添加配色组件
* 在top bar中支持指定分组值用某个具体颜色，例如支付方式‘支付宝’蓝色，'微信'绿色
* 修复了top N在作用于多个分组的bug
* pivot table合计有误 bugfix
* 设置pivot table首行冻结

### v6.0

* 支持多个LDAP配置的域账号登录

  [流程图](https://www.processon.com/view/link/5fd178e6e401fd06ddb78340)

### v7.0

* 增加bar chart，pie chart的antV图表

## 源码改动处

### config.py

```
数据源配置 SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@192.168.16.47:3306/superset'

sql打印，SQLALCHEMY_ECHO = True

关闭CSRF验证 WTF_CSRF_ENABLED = False

调整查询LIMIT QUERY_SEARCH_LIMIT = 1000
```

### superset/views/core.py

```
新增接口/iframe/<token>/<dashboard_id>

新增页面 /templates/superset/dashboard_hf.html
```

### 新增自定义图表

#### 新增数据接口（以TopBar为例）

```
新增Class TopBar继承BaseViz

class TopBar(BaseViz):

    """DIY insert chart"""

    viz_type = "top_bar"
    verbose_name = _("top bar View")
    credits = 'a <a href="https://github.com/airbnb/superset">Superset</a> original'
    is_timeseries = False
    enforce_numerical_metrics = False

    def query_obj(self):
        d = super().query_obj()
        fd = self.form_data # form_data中包含界面左侧组件内容1
        if not fd.get('all_columns'): #这个字段对应×××组件，不为空
            raise Exception('Choose Columns')

        if fd.get('all_columns'):
            d['columns'] = fd.get('all_columns') # all_columns是左侧组件名，后面会提到

        ## 修改top bar 返回数据条数，以免数据过多前段卡死
        if fd.get('chooseN'):
            d["row_limit"] = fd.get("chooseN") * 100
        else :
            d["row_limit"]=100
        return d

    def get_data(self, df: pd.DataFrame) -> VizData:
        # df是pandas的DataFrame类型
        data = {'plot_data':np.array(df).tolist(),'legend':'数据的指标名称'}
        return data

    def json_dumps(self, obj, sort_keys=False):
        return json.dumps(
            obj, default=utils.json_iso_dttm_ser, sort_keys=sort_keys, ignore_nan=True
        )
```

#### TopBar图表组件

文件目录

```
- /visualizations/TopBar/images 可选图表的缩略图
- .../ReactTopBar.js   
- .../TopBar.js
- .../TopBarChartPlugin.js
- .../transformProps.js
```

依赖关系 TopBar -> ReactTopBar -> TopBarChartPlugin -> transformProps 以下依次为各个文件的源码

&#x20;ReactTopBar.js&#x20;

````
 import { reactify } from '@superset-ui/chart'; import Component from './TopBar'; export default reactify(Component); ``` --- TopBar.js ``` import echarts from 'echarts'; import d3 from 'd3'; import PropTypes from 'prop-types'; import { Chart } from '@antv/g2'; import { getColor, groupbysum } from '../MixLineBar/MixLineBarUtil'; const propTypes = { width: PropTypes.number, height: PropTypes.number, }; // 检查类型，其中data包含viz.py中返回的数据，width和height为图表宽高 function TopBar(element, props) { const { chartData, chooseN, colorScheme, labelColors, yAxisFormat, yAxisHide, barStacked, xseries, isBreakDown, orderBars, showLegend, metricLabel, width, height, } = props; let yData = chartData.plot_data; const divid = 'top_bar_' + new Date().getTime(); if (yData.length == 0) { // 没有数据的处理 const div = d3.select(element, props); var html = "No Data"; html = '暂无数据'; div.html(html); return; } const index = yData[0].length - 1; function ascend(x, y) { return y[yData[0].length - 1] - x[yData[0].length - 1]; } const antData = []; //排序以topN为主 if (orderBars) { yData.sort(function([x, y], [x1, y1]) { return x.localeCompare(x1); }); } if (chooseN != undefined) { //按照xserise分组求和取top yData = groupbysum(yData, xseries, chooseN); } // 记录最大刻度值 const maxV = {}; let maxValue = 0; for (let i = 0; i maxValue) { maxValue = yData[i][n]; } } else { // 第 xseries 列为label ant_item.label = yData[i][xseries]; if (isBreakDown) { // 其他列为分组 ant_item.type = yData[i][1 - xseries]; } } } antData.push(ant_item); } if (barStacked) { for (const k in maxV) { if (maxV[k] > maxValue) { maxValue = maxV[k]; } } } const div = d3.select(element, props); var html = ""; div.html(html); // 给echarts添加div // AntV实现 const chart = new Chart({ container: divid, autoFit: true, height: 500, }); chart.data(antData); chart .coordinate() .transpose() .scale(1, -1); chart.scale(metricLabel, { min: 0, max: maxValue * 1.1, }); chart.axis(metricLabel, { position: 'right', }); chart.axis('label', { label: { offset: 12, }, }); chart.tooltip({ shared: true, showMarkers: false, }); let adjust_type = 'dodge'; if (barStacked) { adjust_type = 'stack'; } if (!showLegend) { chart.legend(false); } // 创建图形，由type 和 value两个属性决定图形x-y chart .interval() .position('label*' + metricLabel) .label(!yAxisHide ? metricLabel : '', { style: { fill: '#8d8d8d', fontSize: '14', }, offset: 10, }) .color({ fields: [isBreakDown ? 'type' : metricLabel], values: isBreakDown ? getColor(colorScheme) : getColor(colorScheme)[0], callback:(val)=>{ return labelColors[val] }, }) .adjust([ { type: adjust_type, marginRatio: 0, }, ]); if (yAxisFormat != null && yAxisFormat != 'SMART_NUMBER') { let yAxisFormat_ = yAxisFormat; if (yAxisFormat_ == 'zh') { yAxisFormat_ = '.0万亿'; } else if (yAxisFormat_ == '.2zh') { yAxisFormat_ = '.2万亿'; } let flag0 = yAxisFormat_.indexOf('+') >= 0; const flag1 = yAxisFormat_.indexOf(',') >= 0; const flag2 = yAxisFormat_.indexOf('.') >= 0; const flag3 = yAxisFormat_.indexOf('%') >= 0; const flag3_1 = yAxisFormat_.indexOf('万亿') >= 0; let suffix = ''; // 数据格式 chart.scale(metricLabel, { formatter(num) { let result = []; let counter = 0; if (num 8) { num /= 100000000; suffix = '亿'; } else if (num.toString().split('.')[0].length > 4) { num /= 10000; suffix = '万'; } } if (flag3) { // 开启百分比 num *= 100; } num = (num || 0).toString().split(''); result = num; let flag4 = num.indexOf('.'); if (flag2) { // 保留小数点后几位 const ii = yAxisFormat_[yAxisFormat_.indexOf('.') + 1]; const a = parseFloat(num.join('')); const b = ( Math.round(a * Math.pow(10, ii)) / Math.pow(10, ii) ).toFixed(ii); num = b.toString().split(''); result = num; } flag4 = num.indexOf('.'); if (flag1) { result = []; for (var i = flag4 - 1; i >= 0; i--) { counter++; result.unshift(num[i]); // 开启千分位 if (!(counter % 3) && i != 0) { result.unshift(','); } } for (var i = flag4; i TopBarChartPlugin.js ``` import { t } from '@superset-ui/translation'; import { ChartMetadata, ChartPlugin } from '@superset-ui/chart'; import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; const metadata = new ChartMetadata({ name: t('Top Bar'), description: 'diy', credits: ['http://echarts.baidu.com/examples/editor.html?c=scatter-effect'], thumbnail, }); export default class TopBarChartPlugin extends ChartPlugin { constructor() { super({ metadata, transformProps, loadChart: () => import('./ReactTopBar.js'), }); } } ``` --- transformProps.js ``` export default function transformProps(chartProps) { const {width, height, queryData, formData} = chartProps; console.log(chartProps); var xseries_name = formData["groupby"]; var breakdown_name = formData["columns"]; var index1 = queryData.query.indexOf(xseries_name); var index2 = queryData.query.indexOf(breakdown_name); var xseries = 0; if (breakdown_name.length > 0) { xseries = 1; if (index1 0; const metricLabel = formData.metrics[0].label; const chartData = queryData.data; const { chooseN, colorScheme, zoomData, yAxisFormat, yAxisHide, barStacked, showLegend, orderBars, labelColors, } = formData; return { chartData, chooseN, colorScheme, labelColors, zoomData, yAxisFormat, yAxisHide, barStacked, xseries, isBreakDown, orderBars, showLegend, metricLabel, width, height, }; }
````

&#x20;TopBar的演示图如下

### 中国地图

```
CountryMap.js  'ISO'替换成 'NL_NAME_1' ,
Line:110 'NAME_2'替换为NL_NAME_1  ,'NAME_1'替换为NL_NAME_1
将china.geojson文件中的'黑龙江省'和繁体字换一下位置。
在data.forEach(..)里修改colorMap[d.country_id]=..改为colorMap[d.country_id.replace('省')]=..
在const colorFn = d => {..}
修改为 colorFn = d=>{
var namearr = d.properties.NL_NAME_1.split('|')
colorMap[namearr]||'none'
}
```

&#x20;地图演示

### 类似地，添加Mix Line Bar、Funnel

&#x20;Mix Line Bar演示

<div align="left"><img src="/files/-MYmHN9C2TEHMktN4xiK" alt=""></div>

借助echarts已有的api，在control panel中新增了两个功能

* 堆积图 （配置同一个stack值）
* x轴缩放（解决分组过多时柱子细小看不清问题）

&#x20;Funnel演示

<div align="left"><img src="/files/-MYmKmJp3XJGJqB9hXJ3" alt=""></div>

&#x20;\## 图表的下钻、上卷 以下钻饼图为例，修改superset-ui 中的NVD3Vis.js 添加两个点击事件，

````
chart.pie.dispatch.on('elementClick',function(ele){clicked(ele,uid)}) ``` --- xiazuan.js ``` import {xiazuan} from "./reducer/xiazuanReducer"; import {shangjuan} from "./reducer/shangjuanReducer"; var ischart = true; var chart_id = {}; export const clicked = function clicked(d,uid) { // var elearr = d.element.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.getAttribute("id").split('-') // console.log(elearr) if(ischart){ import('../explore/index').then(e=>{ e.dashStore.dispatch(xiazuan(chart_id[uid],true,-1,d.data)) }) }else{ import('../dashboard/index').then(e=>{ e.dashStore.dispatch(xiazuan(chart_id[uid],true,-1,d.data)) }) } }; export const clicked2 = function clicked2(uid) { if(ischart){ import('../explore/index').then(e=>{ e.dashStore.dispatch(shangjuan(chart_id[uid],true,-1)) }) }else{ import('../dashboard/index').then(e=>{ e.dashStore.dispatch(shangjuan(chart_id[uid],true,-1)) }) } } export function import_store (element,vizType,uid) { var viz_type=["pie","dist_bar"]; var intype= viz_type.indexOf(vizType); if( intype'+chart_id) return chart_id[uid] } ``` --- /action/xiazuanAction.js ``` import {postChartFormData} from '../../chart/chartAction'; export const initLink = {}; export function xiazuanAction(chartKey, force, dashboardId, dData) { return (dispatch, getState) => { const charts = getState().charts; var chart = (getState().charts || {})[chartKey]; var fd = chart.latestQueryFormData; if (!fd || Object.keys(fd).length === 0) { return; } console.log('xiazuan') if ((fd.xiazuan != null && fd.xiazuan.length > 0) || fd.liandong == true) { // 序列的下钻链路 var curr = fd.groupby[0]; var nextnode = getNext(curr, fd.xiazuan); console.log('nextnode=>' + nextnode); if (nextnode != null) { // 进行单个图表的下钻 console.log('chartkey>>' + chartKey); if (typeof initLink[chartKey] === 'undefined') { initLink[chartKey] = []; } initLink[chartKey].push(dData.x); fd.groupby.pop(); fd.groupby.push(nextnode); fd.adhoc_filters.push(buildFilter(curr, dData.x)); dispatch(postChartFormData(fd, force, 10000, chart.id, dashboardId)); } if (fd.liandong) { // 如果有联动，触发其他同数据源图表的变化 const dataSource = fd.datasource; const id = chart.id; for (const chartOne in charts) { if ( charts[chartOne].formData.datasource === dataSource && charts[chartOne].id !== id ) { chart = charts[chartOne]; fd = chart.latestQueryFormData; if (!fd || Object.keys(fd).length === 0) { return; } console.log('触发序列联动') if (fd.adhoc_filters.length > 0 && fd.adhoc_filters[fd.adhoc_filters.length - 1].subject == curr) { //如果 点击的维度还是 最后联动追加的维度，则只改变值 fd.adhoc_filters[fd.adhoc_filters.length - 1].comparator = dData.x; } else { fd.adhoc_filters.push(buildFilter(curr, dData.x)); } dispatch(postChartFormData(fd, force, 10000, chart.id, dashboardId)); } } } } else if ((fd.xiazuan2 != null && fd.xiazuan2.length > 0) || fd.liandong2 == true) { // 带有分解的下钻链路 var curr2 = fd.columns[0]; var nextnode2 = getNext(curr2, fd.xiazuan2); console.log('nextnode2=>' + nextnode2) if (nextnode2 != null) { console.log('chartkey>>' + chartKey); if (typeof initLink[chartKey] === 'undefined') { initLink[chartKey] = []; } initLink[chartKey].push(dData.key); fd.columns.pop(); fd.columns.push(nextnode2); fd.adhoc_filters.push(buildFilter(curr2, dData.key)); dispatch(postChartFormData(fd, force, 10000, chart.id, dashboardId)); } else if (!fd.liandong2) { return; } if (fd.liandong2) { const dataSource = fd.datasource; const id = chart.id; for (const chartOne in charts) { if ( charts[chartOne].formData.datasource === dataSource && charts[chartOne].id !== id ) { chart = charts[chartOne]; fd = chart.latestQueryFormData; if (!fd || Object.keys(fd).length === 0) { return; } console.log('触发分解联动') if (fd.adhoc_filters.length > 0 && fd.adhoc_filters[fd.adhoc_filters.length - 1].subject == curr2) { //如果 点击的维度还是 最后联动追加的维度，则只改变值 fd.adhoc_filters[fd.adhoc_filters.length - 1].comparator = dData.key; } else { fd.adhoc_filters.push(buildFilter(curr2, dData.key)); } dispatch(postChartFormData(fd, force, 10000, chart.id, dashboardId)); } } } } }; } /** * 下钻衍生的过滤条件 * @param name * @param val */ function buildFilter(name, val) { const adhoc_filter = {}; adhoc_filter.clause = 'WHERE'; adhoc_filter.comparator = val; adhoc_filter.expressionType = 'SIMPLE'; adhoc_filter.fromFormData = true; adhoc_filter.isExtra = false; adhoc_filter.operator = '=='; adhoc_filter.subject = name; return adhoc_filter; } /** * 获取链路的指定节点的下一节点 * @param curr 当前节点 * @param link 链路 */ function getNext(curr, link) { const index = link.indexOf(curr); if (index == -1 || index == link.length - 1) { return null; } return link[index + 1]; } ``` --- /reducer/xiazuanReducer.js ``` import * as actions from '../action/xiazuanAction'; export function xiazuan(chartList = 1, force = false, dashboardId,dData) { return (dispatch, getState) => { // eslint-disable-next-line no-undef dispatch(actions.xiazuanAction(chartList, force, dashboardId,dData)); }; }
````

饼图演示

![](/files/-MYmHN98HIM9Kl6ph9O9)

左下角显示已经下钻的层级，支持多层级下钻，点击左下角显示的层级为上卷到上一层。

图表使用实践

### 导入数据源、表

添加数据源，支持多数据源，导入数据源后从已有的表中添加一张，可以设置cache，支持SQL修改

### 创建charts

选择一个数据源，选择一个图表类型

### SQL lab编写SQL

选择数据库、模式，直接写SQL模式发起查询

### 添加图表到dashboard

将制作好的图表添加到已有或新建的dashboard，完成看板制作

### 丰富dashboard

添加已有图表到dashboard中，自定义调整位置和大小； 支持图表联动配置； 支持过滤器指定图表变化； 支持markdown，可以添加视频； 支持修改css样式，夜间模式；

## 源码分析

这里以WordCloud为例。主要涉及到的js有如下 /src/explore/controlPanels/WordCloud 这是控制层 ，里面定义了charts左侧的组件。

superset已有的组件整理如下：

* series&#x20;
* metric
* ...

  太多了。。

组件的代码 罗列几个属性

* requiresTime 是否需要时间
* controlPanelSections 为左侧组件列表，label的值为组件名称，实现代码在 /src/explore/controls.jsx

  (在ui-plugin里，则是对应NVD3Controls.tsx 或其他图表包下的Controls)
* controlOverrides 是你需要覆盖已有组件里的属性值
* sectionOverrides  看Dual Line里的这块内容

当然，如果现有组件不能满足需求，可以自定义组件，自定义组件的代码放到和其他组件一样的位置 /src/explore/controls.jsx

注册左侧组件层 setupPlugins.ts registerValue('top\_bar',TopBar) 注册图表type，有顺序 VizTypeControl.jsx DEFAULT\_ORDER=\[top\_bar,....]

/visualizations 下新建一个文件夹取名自己的图表名称 TopBar

* TopBar.jsx  react组件，主要实现图表的样式，检测数据类型（prop-types）
* TOpBar.less 类似CSS
* ReactTopBar.js(或者是)
* TopBarChartPlugin.js 继承图表插件父类ChartPlugin，构造方法里有 metaData(super-ui库的ChartMetaData对象【 界面上的图表名称显示为"Top Bar"，描述等】)，loadChart为加载React组件TopBar

  &#x20;ps：如果是在ui-plugin里，则是对应TopBar/index.js
* transformProps.js 这是转换类

加载顺序 ： TopBarChartPlugin -> ReactTopBar -> transformProps -> TOpBar

## 技术栈学习了解

* python flask框架
* 熟悉Flask—login组件
* 常用ES6 语法 [语法传送门](https://www.jianshu.com/p/c827598cd8b7)
* react语言熟悉,了解React的html与js混用的写法，了解组件，了解react的生命周期 [react入门](http://www.ruanyifeng.com/blog/2015/03/react.html)
* 摘出几句敲黑板的话，加深一下理解： 1、HTML 语言直接写在 JavaScript 语言之中，不加任何引号，这就是 JSX 的语法，它允许 HTML 与 JavaScript 的混写 2、React 允许将代码封装成组件（component），然后像插入普通 HTML 标签一样，在网页中插入这个组件。 3、组件并不是真实的 DOM 节点，而是存在于内存之中的一种数据结构，叫做虚拟 DOM （virtual DOM）只有当它插入文档以后，才会变成真实的 DOM 。根据 React 的设计，所有的 DOM 变动，都先在虚拟 DOM 上发生，然后再将实际发生变动的部分，反映在真实 DOM上，这种算法叫做 DOM diff ，它可以极大提高网页的性能表现。 4、
* react prop-types 检测类型
* nvd3，编写svg图形代码 &#x20;

对于代码的修改最好先在线平台操作学习，改完即可知道哪里变了

[agular-nvd3](http://krispo.github.io/angular-nvd3/#/)\
[plnkr](http://plnkr.co/edit/lBKFld?p=preview\&preview)\
[vd3文档](http://nvd3-community.github.io/nvd3/examples/documentation.html)

* 对图表下钻开发, 需要对redux有所了解，需要知道redux的一些概念：

[B站学习视频](https://www.bilibili.com/video/BV1Kt411k7Xz?from=search\&seid=16472628578011502750) [demo学习](https://github.com/prs1022/reace-redux-first-app) [根据文档创建demo](https://create-react-app.dev/docs/getting-started/)

* State  数据，就是一个对象。redux中的state是不能直接修改的，只能通过action来修改，相当于我们在单例中定义setter方法。
* Action  redux 将每一个更改动作描述为一个action，要更改state中的内容，你需要发送action。一个action是一个简单的对象，用来描述state发生了什么变更。

```
const INCREMENT = 'INCREMENT'
const incrementAction = {"type": INCREMENT, "count": 2}
```

上面这就是一个action，说白了就是一个对象，根据里面的type判断做什么操作。

* Reducer  数据state，指示action都有了那么就是实现了。reducer就是根据action来对state进行操作。

  \`\`\`

  const calculate = (state: ReduxState = initData, action: Action ) => {

  &#x20; switch (action.type) {

  ```
    case INCREMENT:
        return {num: state.num + action.count}
    case REDUCE:
        return {num: state.num - action.count}
    default:
        return state
  ```

  &#x20; }

  }

export {calculate}

```
通过reducer操作后返回一个新的state，比如这里根据action的type分别对state.num进行加减。

- Store  store就是整个项目保存数据的地方，并且只能有一个,可以理解为一个数据库。创建store就是把所有reducer给它。
```

import { createStore, combineReducers } from "redux"; import { calculate } from "./calculate";

// 全局你可以创建多个reducer 在这里统一在一起 const rootReducers = combineReducers({calculate}) // 全局就管理一个store export const store = createStore(rootReducers)

```
- dispatch  store.dispatch()是组件发出action的唯一方法。
```

store.dispatch(incrementAction);

```
通过store调用incrementAction，那么就直接把store里的数据修改了。

通常是同步的，通过redux中间件 redux-thunk可以实现异步
,如果用了中间件，所有action都会有个dispatch参数

调用action的方式
```

store.dispatch(async function(dispatch){ dispatch({ type:'', payload: })//派发一个action })

````
通过dispatch进行一个方法的派发

store里的dispatch方法关联到组件

通过connect连接组件和redux数据，传递state数据和dispatch方法

总的来说，redux像是一个全局状态树，可以使得代码更加清晰



```text
\star 在react项目中使用Echarts
````

* npm安装echarts-for-react npm install --save echarts-for-react npm install echarts --save
* echarts体积巨大，使用模块化加载

```
import echarts from 'echarts/lib/echarts' //必须
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/grid'
import 'echarts/lib/chart/bar'
```

* 组件化开发的福音，react组件模块化加载

```
import { pieOption, barOption, lineOption, scatterOption, mapOption, radarOption, candlestickOption } from './optionConfig/options'
const PieReact = asyncComponent(() => import(/* webpackChunkName: "PieReact" */'./EchartsDemo/PieReact'))  //饼图组件
const BarReact = asyncComponent(() => import(/* webpackChunkName: "BarReact" */'./EchartsDemo/BarReact')) //柱状图组件
const LineReact = asyncComponent(() => import(/* webpackChunkName: "LineReact" */'./EchartsDemo/LineReact'))  //折线图组件
const ScatterReact = asyncComponent(() => import(/* webpackChunkName: "ScatterReact" */'./EchartsDemo/ScatterReact'))  //散点图组件
const MapReact = asyncComponent(() => import(/* webpackChunkName: "MapReact" */'./EchartsDemo/MapReact'))  //地图组件
const RadarReact = asyncComponent(() => import(/* webpackChunkName: "RadarReact" */'./EchartsDemo/RadarReact')) //雷达图组件
const CandlestickReact = asyncComponent(() => import(/* webpackChunkName: "CandlestickReact" */'./EchartsDemo/CandlestickReact')) //k线图组件
```

* echarts-for-react 是一个非常简单的针对于ReaCt的Echarts封装插件

  用法：

  import ReactEcharts from 'echarts-for-react'
* celery 并行分布式框架

  定时发送报表邮件功能用到，celery是python开发的分布式任务调度模块。

  celery本身不包含消息服务，而是利用第三方broker，可以用redis或mq，官方推荐mq。

  我们在superset中用的是redis，只需要简单的配置，然后启动celery即可。

  一些常用的命令如下：

  celery -A task worker --loglevel=DEBUG -P eventlet

  > eventlet是在windows系统需要加上的

因为启动都是命令行查看，这里推荐一个监控工具flower， 安装非常简单pip install flower， 启动命令 在启动celery后执行 celery flower --address=0.0.0.0 --port=5555 --broker=redis\://localhost:6379/1


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rensong.gitbook.io/superset/master.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
